學習:C#綜合揭秘——Entity Framework 併發處理詳解 帖子筆記 ,該帖子使用的是objectContext , 一、併發相關概念 併發的類型: 第一種模式稱為悲觀式併發,即當一個用戶已經在修改某條記錄時,系統將拒絕其他用戶同時修改此記錄。第二種模式稱為樂觀式併發,即系統允許多個用戶同... ...
學習:C#綜合揭秘——Entity Framework 併發處理詳解 帖子筆記 ,該帖子使用的是objectContext ,
一、併發相關概念
併發的類型:
第一種模式稱為悲觀式併發,即當一個用戶已經在修改某條記錄時,系統將拒絕其他用戶同時修改此記錄。
第二種模式稱為樂觀式併發,即系統允許多個用戶同時修改同一條記錄,系統會預先定義由數據併發所引起的併發異常處理模式,去處理修改後可能發生的衝突。常用的樂觀性併發處理方法有以下幾種:
1、保留最後修改的值。
2、保留最初修改的值。
3、合併多次修改的值。
二、模型屬性的併發處理選項
如下圖模型設計器中TimeStamp欄位為啟用併發
<EntityType Name="UserAccout"> <Key> <PropertyRef Name="Id" /> </Key> <Property Name="Id" Type="Int32" Nullable="false" annotation:StoreGeneratedPattern="Identity" /> <Property Name="FirstName" Type="String" Nullable="false" /> <Property Name="LastName" Type="String" Nullable="false" /> <Property Name="AuditFileds" Type="OrderDB.AuditFields" Nullable="false" /> <Property Name="Timestamp" Type="DateTime" Nullable="false" ConcurrencyMode="Fixed" annotation:StoreGeneratedPattern="Computed" /> </EntityType>
併發模式:ConcurencyMode 有兩個成員:
None : 在寫入時從不驗證此屬性。 這是預設的併發模式。
Fixed: 在寫入時始終驗證此屬性。
當模型屬性為預設值 None 時,系統不會對此模型屬性進行檢測,當同一個時間對此屬性進行修改時,系統會以數據合併方式處理輸入的屬性值。
當模型屬性為Fixed 時,系統會對此模型屬性進行檢測,當同一個時間對屬性進行修改時,系統就會激發OptimisticConcurrencyException 異常。
三、悲觀併發
四、樂觀併發
為瞭解決悲觀併發所帶來的問題,ADO.NET Entity Framework 提供了更為高效的樂觀併發處理方式。相對於LINT to SQL , ADO.NET Entity Framework 簡化了樂觀併發的處理方式,它可以靈活使用合併數據、保留初次輸入數據、保留最新輸入數據(3種方式)等方式處理併發衝突。
4.1 以合併方式處理併發數據
總結:當模型屬性的 ConcurencyMode 為預設值 None ,一旦同一個對象屬性同時被修改,系統將以合併數據的方式處理併發衝突,這也是 Entity Framework 處理併發衝突的預設方式。
合併處理方式如下:
(1)當同一時間針對同一個對象屬性作出修改,系統將保存最新輸入的屬性值。
(2)當同一時間對同一對象的不同屬性作出修改,系統將保存已被修改的屬性值。下麵用兩個例子作出說明:
運行結果:
#region (4.1)測試不設置任何併發測試時,當產生併發EF的處理方法 delegate void MyDelegate(Address addressValue); public StringBuilder sb = new StringBuilder(); public Address GetAddress(int id) { using (OrderDBContainer context = new OrderDBContainer()) { IQueryable<Address> list = context.AddressSet.Where(x => x.Id == id); return list.First(); } } /// <summary> /// 修改方法 /// </summary> /// <param name="addressValue"></param> public void UpdateAddress(Address addressValue) { using (OrderDBContainer context = new OrderDBContainer()) { //顯示輸入新數據的信息 Display("Current", addressValue); var obj = context.AddressSet.Where(x => x.Id == addressValue.Id).First(); if (obj != null) context.Entry(obj).CurrentValues.SetValues(addressValue); //虛擬操作,保證數據能同時加入到上下文當中 Thread.Sleep(100); context.SaveChanges(); } } /// <summary> /// 顯示實體當前屬性 /// </summary> /// <param name="message"></param> /// <param name="addressValue"></param> public void Display(string message, Address addressValue) { String data = string.Format("{0}\n Address Message:\n Id:{1} Address1:{2} " + "address2:{3} \r\n ", message, addressValue.Id, addressValue.Address1, addressValue.Address2 ); sb.AppendLine(data); } /// <summary> /// (1)測試使用EF預設的機制,當配置併發控制時,系統是使用的合併的方式 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void button3_Click(object sender, EventArgs e) { //在更新數據前顯示對象信息 var beforeObj = GetAddress(1); Display("Before", beforeObj); //更新Person的SecondName,Age兩個屬性 Address _address1 = new Address(); _address1.Id = 1; _address1.Address1 = "古溪"; _address1.Address2 = beforeObj.Address2; _address1.AuditFields.InsertDate = beforeObj.AuditFields.InsertDate; _address1.AuditFields.UpdateDate = beforeObj.AuditFields.UpdateDate; _address1.City = beforeObj.City; _address1.Zip = beforeObj.Zip; _address1.State = beforeObj.State; //更新Person的FirstName屬性 Address _address2 = new Address(); _address2.Id = 1; _address2.Address1 = beforeObj.Address1; _address2.Address2 = "江蘇"; _address2.AuditFields.InsertDate = beforeObj.AuditFields.InsertDate; _address2.AuditFields.UpdateDate = beforeObj.AuditFields.UpdateDate; _address2.City = beforeObj.City; _address2.Zip = beforeObj.Zip; _address2.State = beforeObj.State; //使用非同步方式同時更新數據 MyDelegate myDelegate = new MyDelegate(UpdateAddress); myDelegate.BeginInvoke(_address1, null, null); myDelegate.BeginInvoke(_address2, null, null); Thread.Sleep(1000); //在更新數據後顯示對象信息 var afterObj = GetAddress(1); Display("After", afterObj); this.textBox1.Text = sb.ToString(); } /// <summary> /// 先插入幾條數據等著測試 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void BtnSaveAddress_Click(object sender, EventArgs e) { using (OrderDBContainer db = new OrderDBContainer()) { Address address = new Address(); address.Address1 = "古溪鎮"; address.Address2 = "安鎮"; address.State = "2"; address.City = "無錫"; address.AuditFields.InsertDate = DateTime.Now; address.AuditFields.UpdateDate = DateTime.Now; address.Zip = "21415"; db.AddressSet.Add(address); db.SaveChanges(); } } /// <summary> /// 還原成初始值,準備再次測試 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void button5_Click(object sender, EventArgs e) { using (OrderDBContainer db = new OrderDBContainer()) { Address _address = db.AddressSet.Where(x => x.Id == 1).First(); _address.Address1 = "aaa"; _address.Address2 = "bbb"; db.SaveChanges(); } } #endregion
備註:實踐過程中遇到的問題
在多線程中EF修改事件的解決方案,使用attach不可以:
使用Entry也報錯
最終參考如下帖子
/// <summary> /// 修改方法 /// </summary> /// <param name="addressValue"></param> public void UpdateAddress(Address addressValue) { using (OrderDBContainer context = new OrderDBContainer()) { //顯示輸入新數據的信息 Display("Current", addressValue); var obj = context.AddressSet.Where(x => x.Id == addressValue.Id).First(); if (obj != null) context.Entry(obj).CurrentValues.SetValues(addressValue); //虛擬操作,保證數據能同時加入到上下文當中 Thread.Sleep(100); context.SaveChanges(); } }
引用:“以合併數據的方式處理併發衝突固然方便快節,但在業務邏輯較為複雜的系統下並不適合使用此處理方式。比如在常見的Order、OrderItem的表格中,OrderItem 的單價,數量會直接影響Order的總體價格,這樣使用合併數據的方式處理併發,有可能引起邏輯性的錯誤。此時,應該考慮以其他方式處理併發衝突。”。
其他什麼方式呢?【待補充】
4.1 刪除與更新操作同時運行(非框架自動處理能力,開發自行修改狀態手動增加的)
Entity Framework 能以完善的機制靈活處理同時更新同一對象的操作,但一旦刪除操作與更新操作同時運行時,就可能存在邏輯性的異常。
例如:兩個客戶端同時載入了同一個對象,第一個客戶端更新了數據後,把數據再次提交。但在提交前,第二個客戶端已經把資料庫中的已有數據刪除。
此時,上下文中的對象處於不同的狀態下,將會引發 OptimisticConcurrencyException 異常(ObjectContext 與DBContext兩種方式下,異常不一樣,具體要根據測試結果自己判斷)。
遇到此異常時,可以用 try(OptimisticConcurrencyException){...} catch {...} 方式捕獲異常,然後更改對象的State 屬性。把EntityState 更改為 Added ,被刪除的數據便會被再次載入。若把 EntityState 更改為 Detached 時,數據便會被順利刪除。下麵把對象的 EntityState 屬性更改為 Added 作為例子。
代碼如下:處理結果前後ID變化了(或許這就是有些架構師使用手動創建的GUID的方式,而不使用自增的原因之一吧,因為數據刪除後再創建就回不到之前的ID了,不是太靈活,使用GUID再結合數據版本(dataVison)欄位,timeStamp基本上控制數據的併發已經足夠啊。
//更新對象 public int UpdateWithConcurrent(int num, Address addressValue) { int returnValue = -1; using (OrderDBContainer context = new OrderDBContainer()) { var obj = context.AddressSet.Where(x => x.Id == addressValue.Id).First(); //顯示對象所處狀態 DisplayState("Before Update", obj); try { if (obj != null) context.Entry(obj).CurrentValues.SetValues(addressValue); //虛擬操作,保證數據已經在資料庫中被非同步刪除 Thread.Sleep(300); context.SaveChanges(); returnValue = obj.Id; } catch (Exception) { //針對異常要做相應的判斷,因為我只測試了刪除的情況,就寫死直接修改成Added 了 //正確的是要區分到底是修改還是刪除 OptimisticConcurrencyException ex //把對象的狀態更改為 Added context.Entry(obj).State = System.Data.Entity.EntityState.Added; context.SaveChanges(); returnValue = obj.Id; } } return returnValue; }
併發時的異常類型:
ID發生了變化
4.3 當發生數據併發時,保留最終(最新:最後一次)輸入的數據
要驗證輸入對象的屬性,必須先把該屬性的 ConcurencyMode 設置為 Fixed,這樣系統就會實時檢測對象屬性的輸入值 。
當該屬性被同時更新,系統便會激發 OptimisticConcurrencyException 異常。捕獲該異常後,可以使用 ObjectContext.Refresh (RefreshMode,object) 刷新上下文中該對象的狀態,當 RefreshMode 為 ClientWins 時,系統將會保持上下文中的現在有數據,即保留最新輸入的對象值。此時再使用ObjectContext.SaveChanges, 系統就會把最新輸入的對象值加入資料庫當中。
在下麵的例子當,系統啟動前先把 Person 的 FirstName、SecondName 兩個屬性的 ConcurencyMode 屬性設置為Fixed,使系統能監視這兩個屬性的更改。所輸入的數據只在FirstName、SecondName 兩個值中作出修改。在數據提交前先以 DisplayProperty 方法顯示資料庫最初的數據屬性,在數據初次更新後再次調用 DisplayProperty 顯示更新後的數據屬性。在第二次更新數據時,由調用ObjectContext.SaveChanges時,資料庫中的數據已經被修改,與當前上下文ObjectContext 的數據存在衝突,系統將激發OptimisticConcurrencyException 異常,此時把引發異常的對象屬性再次顯示出來。對異常進行處理後,顯示資料庫中最終的對象值。
觀察測試結果,可見當RefreshMode狀態為ClientWins時,系統將會保存上下文當中的對象屬性,使用此方法可以在發生併發異常時保持最新輸入的對象屬性。
4.4 當發生數據併發時,保留最早(最初:最早一次)輸入的數據
把對象屬性的 ConcurencyMode 設置為 Fixed 後,同時更新該屬性,將會激發 OptimisticConcurrencyException 異常。此時使用 ObjectContext.Refresh (RefreshMode,object) 刷新上下文中該對象的狀態,當 RefreshMode 為 StoreWins 時,系統就會把數據源中的數據代替上下文中的數據。
因為初次調用 SaveChanges,數據可以成功保存到資料庫。但是在 ObjectContext 並未釋放時,再次使用 SaveChanges 非同步更新數據,就會引發OptimisticConcurrencyException 併發異常。當 RefreshMode 為 StoreWins 時,系統就會保留初次輸入的數據屬性。
此例子與上面的例子十分相似,只是把 RefreshMode 改為 StoreWins 而已。在業務邏輯較為複雜的的系統當中,建議使用此方式處理併發異常。在保留最初輸入的數據修改屬性後,把屬性返還給客戶,讓客戶進行對比後再決定下一步的處理方式。
觀察測試結果,可見當 RefreshMode 狀態為 StoreWins 時,系統將會以數據源中的數據代替上下文當中的對象屬性。在業務邏輯較為複雜的的系統當中,建議使用此方式處理併發異常。
鏈接: https://pan.baidu.com/s/1gfu6fZl 密碼: fyb3
練習的源碼,有糾正的錯誤的朋友記得分享