用.Net兩年了,也積累了一些知識和經驗,覺得應該做出點自己的東西,而並不只是給別人打工。
所以決定利用最新發布的VS2008(Orcas) Beta2也加入到WEB 2.0的大潮中來,一來是學以所用,二來在實踐中掌握最新的技術。
現在流行在開發階段給項目起個Code Name,我也來湊湊熱鬧,就叫Pluto,以紀念不久前被剝奪九大行星資格的我們天蠍座的守護星——冥王星
平時有自己的工作,只能利用不多的業余時間開發,所以預計(爭取)在VS2008正式發布之際,Pluto也能開發完成。
在這裡,我會記錄下開發Pluto中的一些事情。
WEB 2.0的網站少不了數據庫、數據訪問,也是一切操作之本,而VS 2008中最大的亮點之一Linq也恰巧是做這個的,所以我的開發從Linq、從數據庫開始。網上關於Linq的教學鋪天蓋地,我不准備重復,我只寫下我遇到的問題。
Linq,更新數據怎麼就那麼費勁?
Linq的全稱是Language Integrated Query ,也就是說Linq是以一個查詢語言的方式出現在我們面前的。在查詢方面Linq做了不少的優化,我們不用在費盡心思去拼裝SQL語句、組裝實體等,所有操作在Linq裡都是強類型的,我們用C#代碼輕松地寫出漂亮的SQL語句。
那麼做為一個查詢語言,Linq在數據更新方面又是怎麼表現的呢?通常來說Linq的更新會以以下的方式出現(絕大部分教程中都是這麼寫的)
1var ctx = new MyDataContext();
2var user = ctx.Users.Where(u => u.UserId == userId).Single();
3user.UserName = "New User Name";
4ctx.SubmitChanges();
這些是C#代碼,但是背後做了什麼呢?Linq會為我們生成類似一下的SQL語句
1--第一步,查詢
2SELECT UserId, UserName, FirstName, LastName, CreatTime From User WHERE UserId = @userId
3
4--第二部,更新
5UPDATE User SET UserName = @newUserName
6WHERE UserId = @oldUserId, userName = @oldUserName, FirstName = @oldFirstName, LastName = @oldLastName
發現了什麼?首先Linq會取出所有的字段,在user.UserName = "New User Name"的時候,記錄下UserName字段被更新過了,UPDATE時會只更新UserName,但是把之前所有字段的值放在WHERE語句裡來做為條件。
Are you kidding?! 這樣的效率實在是太差了吧?!
拋開效率問題,接下來我們看另外一種更新,有個某個字段記錄頁面被訪問的次數,平時我們會用
1UPDATE POST SET Views = Views + 1 WHERE PostId = @PostId
但是如果我們寫下如下C#代碼
1var ctx = MyDataContext();
2var post = ctx.Posts.Where(p => p.PostId = @postId).Single();
3post.Views++
4ctx.SubmitChanges();
Linq會怎麼做呢?和上面一樣!取出所有字段,把View加一,用所有字段做為條件(包括Views),更新回去。
設想一下,這樣一個被頻繁使用的計數器,兩次操作出現SELECT與UPDATE交叉情況的可能性很大,那麼後者還能更新成功麼?
微軟就是這樣解釋的,如果在你更新過程中,有其他人更新了這一行,那麼這一行也就不是你所需要的那一行了,為了防止這樣的沖突,所以把所有字段都放在WHERE語句中,這是by design的。
你可以通過其他方法進行更新數據,然而在目前版本,這個方法也表現的不怎麼樣。
System.Data.Linq.Table<T>有一個Attach方法,帶有三個重載,用來直接更新數據的,我們來一個一個的來看看。
Attach(T entity)
1var ctx = new MyDataContext();
2var newUser = new User();
3newUser.UserId = new Guid("xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx");//假設作為參數傳進來的
4newUser.UserName = "New User Name";
5ctx.Users.Attach(newUser);
6ctx.SubmitChanges();
運行完全沒有任何效果,SQL Profiler無任何記錄。
Attach(T entity, T original)
1var ctx = new MyDataContext();
2var newUser = new User();
3newUser.UserId = new Guid("xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx");//假設作為參數傳進來的
4newUser.UserName = "New User Name";
5var user = ctx.User.Where(u => u.UserId = newUser.UserId).Single();
6ctx.Users.Attach(newUser, user);
7ctx.SubmitChanges();
運行時提示: Cannot add an entity with a key that is already in use.
Attch(T entity, bool asmodified)
1var ctx = new MyDataContext();
2var newUser = new User();
3newUser.UserId = new Guid("xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx");//假設作為參數傳進來的
4newUser.UserName = "New User Name";
5ctx.Users.Attach(newUser, true);
6ctx.SubmitChanges();
運行時提示:An entity can only be attached as modified without original state if it declares a version member or does not have an update check policy.
怎麼辦?提示中說"declares a version member ",通常來說是指SQL SERVER中TimeStamp類型的字段,在你所需要更新的表中加上一個字段,並標記為TimeStamp就可以了。但是這樣做,對於我們來說仍然是個浪費,並且WHERE語句中仍然會出現TimeStamp的限制。
你還可以通過在字段上設置UpdateCheck.Never屬性來避免更新檢查,但是如果數據表更新、新增存儲過程,需要重新生成dbml的話,你需要手動重新設置一遍。
Linq甚至沒有一個類似Web引用中Update Web Reference的操作來讓你方便的在數據表更新後更新dbml,並且在這個版本都不會提供,你所能做的只有刪除原來的表,刷新Server Exploer,重新拖拽到dbml的設計視圖中,或者,寫個腳本,讓SQLMETAL來幫你完成這些。
結論:
Linq雖然做為一個查詢語言出現,但是在數據更新方面也是做了不少工作的,尤其是一些CHECK的工作,但對於寫慣SQL的我們來說,還是很不習慣,甚至覺得,這些工作你不替我做才好呢。
在沒有更好解決辦法的前提下,在更新操作上,老老實實的寫SQL語句或者存儲過程應該是個不壞的選擇。