实体对象变动跟踪

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