Entity Framework 查漏補缺 (二)

數據加載

 以下這樣的一個lamda查詢語句,不會立馬去查詢數據庫,只有當須要用時去調用(如取某行,取某個字段、聚合),纔會去操做數據庫,EF中自己的查詢方法返回的都是IQueryable接口。html

連接:IEnumerable和IQueryable接口說明sql

其中聚合函數會影響數據加載,諸如: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 )關聯的數據spa

using (var context = new TestDB())
{
    var obj = context.Place.Where(t => t.PlaceID == 9).FirstOrDefault();
}

能夠看到Population爲null線程

只有用到Population對象時,EF纔會發起到數據庫的查詢;code

固然導航數據必須標記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的默認事務處理機制基本知足使用。

除此以外,如下兩種狀況怎麼使用事務:

  1. 數據分階段保存,屢次調用saveChanges()
  2. 使用多個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線程內惟一

 

連接:dbcontext實例建立問題

 

併發

在實際場景中,併發是很常見的事,同條記錄同時被不一樣的兩個用戶修改

在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被覆蓋

相關文章
相關標籤/搜索