數據載入 如下這樣的一個lamda查詢語句,不會立馬去查詢資料庫,只有當需要用時去調用(如取某行,取某個欄位、聚合),才會去操作資料庫,EF中本身的查詢方法返回的都是IQueryable介面。 鏈接:IEnumerable和IQueryable介面說明 其中聚合函數會影響數據載入,諸如:toList ...
數據載入
如下這樣的一個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被覆蓋