一例一個實體物件不能由多個 IEntityChangeTracker 例項引用的解決
前幾日在檢查日誌的時候發現系統偶爾會出現 Store update, insert, or delete statement affected an unexpected number of rows (0). Entities may have been modified or deleted since entities were loaded 的錯誤,經過查詢資料,是因為隨著訪問量的增多,併發增多造成。解決辦法從微軟的msdn上找到了想改解決辦法。
Entity Framework Optimistic Concurrency Patterns 這裡給出了微軟這篇文章的連結,我採用的是微軟提出的第一種解決辦法:Resolving optimistic concurrency exceptions with Reload 其中 update 方法的寫法修改成這樣:
public bool Update(T entity) { using (var dbContext = new F()) { bool saveFailed; do { saveFailed = false; try { //把entity附加到當前dbContext dbContext.Set<T>().Attach(entity); dbContext.Entry<T>(entity).State = EntityState.Modified; return dbContext.SaveChanges() > 0; } catch (DbUpdateConcurrencyException ex) { saveFailed = true; // Update the values of the entity that failed to save from the store ex.Entries.Single().Reload(); } } while (saveFailed); return false; } }
當然,新增和刪除的相應方法也做了同樣的修改,修改後對相應的方法做測試,沒有發現什麼問題。於是就上線了。因為併發效果在本地沒法實現,實際效果需要上線後觀察。過了個週末,然後週一來檢查執行的效果,Store update, insert, or delete statement affected an unexpected number of rows (0). Entities may have been modified or deleted since entities were loaded 的錯誤沒出現,但是發現大量的 一個實體物件不能由多個 IEntityChangeTracker 例項引用的錯誤。通過定位,是update方法出現了問題。網上搜索這個錯誤出現的原因,搜尋到的大部分是說由於主從表的關係導致此問題,而且說的都雲裡霧裡,看不懂他們說的意思。但是雖然出錯的地方剛好有主從表,但我明白,問題恐怕不是這裡。因為這個錯誤大量出現是在我更改了update方法後才出現的。通過分析我更改的update方法前後的變化,更改前update方法傳入的entity例項和update方法SaveChanges 共用的是一個dbcontext, 而更改後,由於使用了 using new 了一個新的 dbcontext,這兩個 dbcontext 不一樣了,懷疑是這裡的問題。而通過把 using 去掉,也真解決了這個問題,不出現 一個實體物件不能由多個 IEntityChangeTracker 例項引用的錯誤了。但是這樣的解決不能令人滿意。因為沒有理論支撐。由於百度和google都沒有找到令人滿意的解釋,於是把目光投向了 stackoverflow 網站,這個網站還是有許多高手,一些解釋感覺很深刻。 通過不停的變換關鍵詞搜尋,終於找到了兩篇文章解釋的比較清楚。我總結了下我這個問題出現的原因是這樣的,看這兩行程式碼:
var examList = es2.Get(m => m.ExamMainGID == examMain.GID && m.QuestionLibraryGID == gid).FirstOrDefault();
es2.Update(examList);
第一行程式碼es2.Get方法使用的資料連線是_dbContext, entity framework 每一個查詢方法都會自動建立一個 IEntityChangeTracker 的跟蹤,這個跟蹤是使用的_dbContext連線建立的,他會跟蹤exmalList的例項狀態變化,比如狀態的修改,刪除等。而在執行 es2.Update(examList); 這個方法的時候,由於Update方法的實現是 using 了一個新的dbContext: using (var dbContext = new F()), 在這句:dbContext .Set<T>().Attach(entity); 就是把 entity附加到了新的dbContext物件中。按ef的規則,這裡是需要建立一個新的 IEntityChangeTracker 跟蹤器,就是這行出現了錯誤。原因就是一個例項的跟蹤器只能有一個,在這裡這個例項就是examList 。在stackoverflow 網站的這兩篇文章中也給出瞭解決辦法。有兩種,第一種是公用一個數據連線dbContext,就是我以前的解決辦法,不再重新new 一個新的資料連線上下文,而仍然使用es2.Get方法使用的資料連線。另外一種解決辦法就屬於場合用法了。即把通過 es2.Get方法獲取的例項 examList 同_dbContext 剝離開來,然後再把exmaList物件附加到新的 dbContext 物件中。具體的寫法是這樣的:
//從_dbContext中分離entity
((IObjectContextAdapter)_dbContext).ObjectContext.Detach(entity);
//把entity附加到當前dbContext
dbContext.Set<T>().Attach(entity);
dbContext.Entry<T>(entity).State = EntityState.Modified;
return _dbContext.SaveChanges() > 0;
這裡的((IObjectContextAdapter)_dbContext).ObjectContext.Detach(entity);就是用 Detach 把 entity從 _dbContext上下文資料連線剝離出來。剝離後再附加到新的資料連線中就不會再出現錯誤。這個方法是不推薦的。如果每次都這樣採用剝離的方法,這是增加程式碼量,共用Context 資料連線才是正確的方法。
這裡給出兩篇stakoverflow的連線,有興趣的可以看下: