Entity Framework Code First實體物件變動跟蹤

libingql發表於2013-10-26

  Entity Framework Code First通過DbContext.ChangeTracker對實體物件的變動進行跟蹤,實現跟蹤的方式有兩種:變動跟蹤快照和變動跟蹤代理。

  變動跟蹤快照:前面幾篇隨筆的示例都是通過實體物件變動快照跟蹤來實現資料操作的,POCO模型不包含任何邏輯去通知Entity Framework實體類屬性的變動。Entity Framework在第一次物件載入到記憶體中時進行一次快照,新增快照發生在返回一次查詢或新增一個物件到DbSet中時。當Entity Framework需要知道物件的變動時,將先把當前實體與快照中的物件進行掃描對比。實現掃描對比的方法是呼叫DbContext.ChangeTracker的DetectChanges方法。

  變動跟蹤代理:變動跟蹤代理是一種會主動通知Entity Framework實體物件發生變動的機制。如:延遲載入的實現方式。要使用變動跟蹤代理,需要在定義的類結構中,Entity Framework可以在執行時從POCO類中建立動態型別並重寫POCO屬性。動態代理就是一種動態型別,包含重寫屬性和通知Entity Framework實體物件變動的邏輯。

  Entity Framework Code First中能夠自動呼叫DbContext.ChangeTracker.DetectChanges的方法:

  ◊ DbSet.Add

  ◊ DbSet.Find

  ◊ DbSet.Remove

  ◊ DbSet.Attach

  ◊ DbSet.Local

  ◊ DbContext.SaveChanges

  ◊ DbContext.GetValidationErrors

  ◊ DbContext.Entry

  ◊ DbChangeTracker.Entries

  ◊ 任何在DbSet上進行LINQ的查詢

  1、控制什麼時間呼叫DetectChanges

  大部分的例項物件的變動調整需要在Entity Framework進行SaveChanges時才會知道,但也可以根據需要呼叫變動跟蹤獲取當前物件的狀態。

  Entity Framework Code First的DbContext.DetectChanges在檢測例項物件的變動時,大部分情況不會有效能的問題。但當有大量的例項物件在記憶體中,或DbContext有大量的操作時,自動的DetectChanges行為可能會一定程度的影響效能。Entity Framework提供關閉自動的DetectChanges的功能,在需要的時候進行手動呼叫。

using (var ctx = new PortalContext())
{
    ctx.Configuration.AutoDetectChangesEnabled = false;
}

  示例:

using (var ctx = new PortalContext())
{
    ctx.Configuration.AutoDetectChangesEnabled = false;

    var province = ctx.Provinces.Find(1);
    province.ProvinceName = "測試";

    Console.WriteLine("Before DetectChanges:{0}", ctx.Entry(province).State);

    ctx.ChangeTracker.DetectChanges();

    Console.WriteLine("After DetectChanges:{0}", ctx.Entry(province).State);
}

  程式碼執行結果:

Before DetectChanges:Unchanged
After DetectChanges:Modified

  2、獲取不帶變動跟蹤的實體查詢

  在一些情況下,我們只需要查詢返回一個只讀的資料記錄,而不會對資料記錄進行任何的修改。這種時候不希望Entity Framework進行不必要的狀態變動跟蹤,可以使用Entity Framework的AsNoTracking方法來查詢返回不帶變動跟蹤的查詢結果。

using (var ctx = new PortalContext())
{
    foreach (var province in ctx.Provinces.AsNoTracking())
    {
        Console.WriteLine(province.ProvinceName);
    }
}

  以上程式碼中使用AsNoTracking方法查詢返回無變動跟蹤的Province的DbSet,由於是無變動跟蹤,所以對返回的Province集中資料的任何修改,在SaveChanges()時,都不會提交到資料庫中。

  AsNoTracking是定義在IQueryable<T>中的擴充套件方法,所以也可以用於LINQ表示式查詢。

using (var ctx = new PortalContext())
{
    var query = from p in ctx.Provinces.AsNoTracking()
                where p.ProvinceID > 10
                select p;
    foreach (var province in query)
    {
        Console.WriteLine(province.ProvinceName);
    }
}

using (var ctx = new PortalContext())
{
    var query = from p in ctx.Provinces
                where p.ProvinceID > 10
                select p;
    query = query.AsNoTracking();

    foreach (var province in query)
    {
        Console.WriteLine(province.ProvinceName);
    }
}

  注:使用AsNoTracking需要新增引用名稱空間using System.Data.Entity。

  3、單個實體的變動跟蹤資訊及操作

  使用狀態屬性:

using (var ctx = new PortalContext())
{
    var province = ctx.Provinces.Find(10);
    DbEntityEntry<Province> entry = ctx.Entry(province);
    Console.WriteLine("Before Edit: {0}", entry.State);
    province.ProvinceName = "Test";
    ctx.ChangeTracker.DetectChanges();
    Console.WriteLine("After Edit: {0}", entry.State);
}

  注:DbEntityEntry需要引用名稱空間using System.Data.Entity.Infrastructure;

  程式碼執行結果為:

Before Edit: Unchanged
After Edit: Modified

  4、檢視物件的當前值、原始值及資料庫中的值

  通過DbEntityEntry可以獲取物件的當前、原始及在資料庫中的值,DbPropertyValues則用於儲存物件具體的屬性。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using System.Data;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;

using Portal.Models;

namespace Portal
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var ctx = new PortalContext())
            {
                var province = ctx.Provinces.Find(10);
                province.ProvinceName = "Test";

                ctx.Database.ExecuteSqlCommand("UPDATE Province SET ProvinceName = 'Testing' WHERE ProvinceID = 10");

                PrintChangeTrackingInfo(ctx, province);
            }
        }

        static void PrintChangeTrackingInfo(DbContext ctx, Province province)
        {
            var entry = ctx.Entry(province);
            Console.WriteLine(entry.State);

            Console.WriteLine("\nCurrent Values:");
            PrintPropertyValues(entry.CurrentValues);

            Console.WriteLine("\nOriginal Values:");
            PrintPropertyValues(entry.OriginalValues);

            Console.WriteLine("\nDatabase Values:");
            PrintPropertyValues(entry.GetDatabaseValues());
        }

        static void PrintPropertyValues(DbPropertyValues values)
        {
            foreach (var propertyName in values.PropertyNames)
            {
                Console.WriteLine("- {0}-{1}", propertyName, values[propertyName]);
            }
        }
    }
}

  程式碼執行的結果:

Modified

Current Values:
- ProvinceID-10
- ProvinceNo-320000
- ProvinceName-Test

Original Values:
- ProvinceID-10
- ProvinceNo-320000
- ProvinceName-測試

Database Values:
- ProvinceID-10
- ProvinceNo-320000
- ProvinceName-Testing
請按任意鍵繼續. . .

  程式碼執行所執行的SQL語句:

exec sp_executesql N'SELECT 
[Limit1].[ProvinceID] AS [ProvinceID], 
[Limit1].[ProvinceNo] AS [ProvinceNo], 
[Limit1].[ProvinceName] AS [ProvinceName]
FROM ( SELECT TOP (2) 
    [Extent1].[ProvinceID] AS [ProvinceID], 
    [Extent1].[ProvinceNo] AS [ProvinceNo], 
    [Extent1].[ProvinceName] AS [ProvinceName]
    FROM [dbo].[Province] AS [Extent1]
    WHERE [Extent1].[ProvinceID] = @p0
)  AS [Limit1]',N'@p0 int',@p0=10
UPDATE Province SET ProvinceName = 'Testing' WHERE ProvinceID = 10
exec sp_executesql N'SELECT 
[Limit1].[ProvinceID] AS [ProvinceID], 
[Limit1].[ProvinceNo] AS [ProvinceNo], 
[Limit1].[ProvinceName] AS [ProvinceName]
FROM ( SELECT TOP (2) 
    [Extent1].[ProvinceID] AS [ProvinceID], 
    [Extent1].[ProvinceNo] AS [ProvinceNo], 
    [Extent1].[ProvinceName] AS [ProvinceName]
    FROM [dbo].[Province] AS [Extent1]
    WHERE [Extent1].[ProvinceID] = @p0
)  AS [Limit1]',N'@p0 int',@p0=10

  從程式碼執行所執行的SQL語句可以看出,在最後獲取物件在資料庫中的值時,Entity Framework再一次到資料庫中去查詢物件的記錄值。

  5、修改DbPropertyValues中的值

  DbPropertyValues中的值不是隻讀,故可以在第一次載入之後進行修改。

using (var ctx = new PortalContext())
{
    ctx.Configuration.AutoDetectChangesEnabled = false;
    var province = ctx.Provinces.Find(10);
    ctx.Entry(province)
        .CurrentValues["ProvinceName"] = "測試";

    Console.WriteLine("Property Value:{0}", province.ProvinceName);
    Console.WriteLine("State:{0}", ctx.Entry(province).State);
}

  執行結果:

Property Value:測試
State:Modified

  在上面的程式碼中,儘管已經禁用了自動偵測變動,但在修改了屬性值之後,物件的屬性仍修改為Modified。實體屬性的修改是通過Change Tracker API實現的,實體的狀態在不需要呼叫DetectChanges即修改為Modified。

  若不希望實體的狀態發生改變,則實現方式為:

using (var ctx = new PortalContext())
{
    ctx.Configuration.AutoDetectChangesEnabled = false;
    var province = ctx.Provinces.Find(10);

    var _province = ctx.Entry(province).CurrentValues.Clone();
    _province["ProvinceName"] = "測試";

    Console.WriteLine("Property Value:{0}", province.ProvinceName);
    Console.WriteLine("State:{0}", ctx.Entry(province).State);
}

  執行結果:

Property Value:Test
State:Unchanged

 

相關文章