Entity Framework 查漏補缺 (二)
資料載入
如下這樣的一個lamda查詢語句,不會立馬去查詢資料庫,只有當需要用時去呼叫(如取某行,取某個欄位、聚合),才會去操作資料庫,EF中本身的查詢方法返回的都是IQueryable介面。
其中聚合函式會影響資料載入,諸如:toList(),sum(),Count(),First()能使資料立即查詢載入。
IQueryable中的Load方法
一般情況,我們都是使用ToList或First來完成預先載入資料操作。但在EF中還可以使用Load() 方法來顯式載入,將獲取的資料放到EF Context中,快取起來備用。和ToList()很像,只是它不建立列表只是把資料快取到EF Context中而已,開銷較少。
using (var context = new TestDB()) { context.Place.Where(t=>t.PlaceID==9).Load(); }
VS中的方法說明:
延遲載入
用之前的Place類和People為例
Place物件如下:
public class Place { [Key] public int PlaceID { get; set;} public string Provice { get; set; } public string City { get; set; } //導航屬性 public virtual List<People> Population { get; set; } }
下面查詢,不會主動去查詢出導航屬性(Population )關聯的資料
using (var context = new TestDB()) { var obj = context.Place.Where(t => t.PlaceID == 9).FirstOrDefault(); }
可以看到Population為null
只有用到Population物件時,EF才會發起到資料庫的查詢;
當然導航資料必須標記virtual,配置延遲載入
//導航屬性 public virtual Place Place { get; set; }
要注意的事:在延遲載入條件下,經常以為導航資料也載入了,從而在迴圈中去遍歷導航屬性,造成多次訪問資料庫。
立即載入
除了前面所說的,使用聚合函式(sum等)外來立即預載入資料,還可以使用Include方法
在上面的查詢中,想要查詢place以及關聯的Population資料如下:
using (var context = new TestDB()) { var obj = context.Place.Where(t => t.PlaceID == 9).Include(p=>p.Population).FirstOrDefault(); }
事務
在EF中,saveChanges()預設是開啟了事務的,在呼叫saveChanges()之前,所有的操作都在同一個事務中,同一次資料庫連線。若使用同一DbContext物件,EF的預設事務處理機制基本滿足使用。
除此之外,以下兩種情況怎麼使用事務:
- 資料分階段儲存,多次呼叫saveChanges()
- 使用多個DbContext物件(儘量避免)
第一種情況:顯式事務
using (var context = new TestDB()) { using (var tran=context.Database.BeginTransaction()) { try { context.Place.Add(new Place { City = "beijing", PlaceID = 11 }); context.SaveChanges(); context.People.Add(new People { Name = "xiaoli" }); context.SaveChanges(); tran.Commit(); } catch (Exception) { tran.Rollback(); } } }
注意的是,不呼叫commit()提交,沒有異常事務也不會預設提交。
第二種情況:TransactionScope分散式事務
- 引入System.Transactions.dll
- Windows需要開啟MSDTC
- TransactionScope也於適用於第一種情況。這裡只討論連線多個DBcontext的事務使用
- 需要呼叫Complete(),否則事務不會提交
- 在事務內,報錯會自動回滾
using (var tran = new TransactionScope()) { try { using (var context = new TestDB()) { context.Place.Add(new Place { City = "5555"}); context.SaveChanges(); } using (var context2 = new TestDB2()) { context2.Student.Add(new Student { Name="li"}); context2.SaveChanges(); } throw new Exception(); tran.Complete(); } catch (Exception) { } }
上面程式碼在同一個事務內使用了多個DBcontext,會造次多次連線關閉資料庫
題外話
如是多個DBcontext連著是同一個資料庫的話,可以將一個己開啟的資料庫連線物件傳給它,並且需要指定EF在DbContext物件銷燬時不關閉資料庫連線。
DbContext物件改造,增加過載建構函式;;傳入兩個引數
- 資料庫連線DbConnection
- contextOwnsConnection=false(DbContext物件銷燬時不關閉資料庫連線):
public class TestDB2 : DbContext { public TestDB2():base("name=Test")
{ } public TestDB2(DbConnection conn, bool contextOwnsConnection) : base(conn, contextOwnsConnection) { } public DbSet<Student> Student { get; set; } }
事務程式碼如下:
using (TransactionScope scope = new TransactionScope()) { String connStr = ……; using (var conn = SqlConnection(connStr)) { try { conn.Open(); using (var context1 = new MyDbContext(conn, contextOwnsConnection: false)) { …… context1.SaveChanges(); } using (var context2 = new MyDbContext(conn, contextOwnsConnection: false)) { …… context2.SaveChanges(); }
scope.Complete(); } catch (Exception e) { } finally { conn.Close(); } } }
DBcontent執行緒內唯一
併發
在實際場景中,併發是很常見的事,同條記錄同時被不同的兩個使用者修改
在EF中有兩種常見的併發衝突檢測
方法一:ConcurrencyCheck特性
可以指定物件的一個或多個屬性用於併發檢測,在對應屬性加上ConcurrencyCheck特性
這裡我們指定Student 物件的屬性Name
public class Student { [Key] public int ID { get; set; } [ConcurrencyCheck] public string Name { get; set; } public int Age { get; set; } }
用個兩個執行緒同時去更新Student物件,模擬使用者併發操作
static void Main(string[] args) { Task t1 = Task.Run(() => { using (var context = new TestDB2()) { var obj = context.Student.First(); obj.Name = "LiMing"; context.SaveChanges(); } }); Task t2 = Task.Run(() => { using (var context = new TestDB2()) { var obj = context.Student.First(); obj.Age = 26; context.SaveChanges(); } }); Task.WaitAll(t1,t2); }
併發衝突報錯:
查看了sql server profiler,發現加了[ConcurrencyCheck]的屬性名和值將出現在Where子句中
exec sp_executesql N'UPDATE [dbo].[Students] SET [Age] = @0 WHERE (([ID] = @1) AND ([Name] = @2)) ',N'@0 int,@1 int,@2 nvarchar(max) ',@0=26,@1=1,@2=N'WANG'
很顯然:
t2再修改Age,根據併發檢測屬性Name的值已被改變,有其他使用者在修改同一條資料,併發衝突。
為每個實體類都單獨地設定檢測屬性實在太麻煩,應該由資料庫來設定特殊欄位值並維護更新會更好,下面就是另一種方法
方法二:timestamp
建立一個基類Base,指定一個特殊屬性值,SQL Server中相應的欄位型別為timestamp,自己專案中的實體類都可以繼承它,
public class Base { [Timestamp] public byte[] RowVersion { get; set; } }
Student先基礎base類,每次更新Student資料,RowVersion 欄位就會由資料庫生成一個新的值,根據這個特殊欄位來檢測併發衝突;實體類不再去考慮設定那個屬性值和更新。
併發處理
同時更新併發,EF會丟擲:DbUpdateConcurrencyException
兩個更新執行緒如上:t1和t2
處理一
Task t1 = Task.Run(() => { using (var context = new TestDB()) { try { var obj = context.Student.First(); obj.Name = "LiMing2"; context.SaveChanges(); } catch (DbUpdateConcurrencyException ex) { //從資料庫重新載入資料並覆蓋當前儲存失敗的物件 ex.Entries.Single().Reload(); context.SaveChanges(); } } });
也就是說,t1併發衝突更新失敗,會重新從資料庫拉取物件覆蓋當前失敗的物件,t1原本的更新被作廢,於此同時的其他使用者併發操作,如t2的更新將會被儲存下來
處理二
Task t1 = Task.Run(() => { using (var context = new TestDB()) { try { var obj = context.Student.First(); obj.Name = "LiMing2"; context.SaveChanges(); } catch (DbUpdateConcurrencyException ex) { var entry = ex.Entries.Single(); entry.OriginalValues.SetValues(entry.GetDatabaseValues()); context.SaveChanges(); } } });
從資料庫重新獲取值來替換儲存失敗的物件的屬性原始值,再次提交更改,資料庫就不會因為當前更新操作獲取的原始值與資料庫裡現有值不同而產生異常(如檢測屬性的值已成一樣),t1的更新操作就能順利提交,其他併發操作如t2被覆蓋