EF6学习笔记十六:变更追踪

要专业系统地学习EF推荐《你必须掌握的Entity Framework 6.x与Core 2.0》。这本书作者(汪鹏,Jeffcky)的博客:https://www.cnblogs.com/CreateMyself/

变更追踪是什么呢?通过EF持久化数据,那么EF是怎么知道你的实体发生了变化,哪里放生了变化?你可能说是实体状态,那么它又是怎么改变实体状态呢?

你的POCO实体状态和EF之间是无法同步的,那么就需要变更追踪的机制。

变更追踪有两种方式,快照追踪和代理追踪

快照追踪的原理是,EF从数据库中获取数据,首先它会在内部对这个实体生成一个快照,也就是副本,另一个则返回给我们程序员。

当我们的POCO实体放生变化调用savechanges()持久化数据的时候,它会扫描POCO和快照进行对比,更改实体状态,这样EF就知道这个实体的变更情况。

这个比较像是MVVM架构的前端框架里面的双向绑定,专门写个方法,去执行这个检查的操作。EF里面肯定会复杂一些,首先它有延迟加载,为什么要有,因为数据是从数据库中获取的,要保证数据是新数据,所以越晚给你越好。

第二个方式是代理追踪,它就没有创建快照,而是重写你的POCO生成一个代理类,你的model里面不是会有virtual修饰的属性吗?因为它重写,动态生成,那么它就可以加入自己的代码。

当你的实体放生改变,那么不需要扫描去对比,直接就通知EF了。

那么最后会讲到他们之间的性能。你可能会说,既然少了全盘扫描对比,那么肯定是代理的性能高啊!这可就不一定了,难道代理追踪的那个通知机制就不耗性能吗?

我们先归纳一些问题。

1.怎样识别这两种方式

2.既然有两种追踪方式,那么到底怎么使用,是使用了一个就不能使用另一个呢?还是其他

3、EF默认是使用哪一种

4、他们之间的具体区别

5、怎么实现快照式跟踪,怎样实现代理跟踪

6、全盘扫描,调用的是哪个方法

7、代理追踪和延迟加载的关系

我们可以通过打印上下文的Configuration里面的具体配置,看看开启的情况,默认是都开启的

bool detect = ctx.Configuration.AutoDetectChangesEnabled;
bool proxy = ctx.Configuration.ProxyCreationEnabled;
Console.WriteLine($"detect:{detect},proxy:{proxy}");
//  detect:True,proxy:True
View Code

那么EF到底默认使用哪种方式,我实践的结论是:快照追踪

何种方式会创建为代理类型

我们来认识一下代理类,代理类的类型是System.Data.Entity.DynamicProxies后面跟一大串数字+字母
比如我们查询这个model,我有两个model,Store商店和Commodity商品,一对多关系。我们看到整个实体和里面的导航属性为代理类型。

 /// <summary>
    /// 商品类
    /// </summary>
    public class Commodity : BaseEntity
    {
        public string Name { get; set; }
        public string Unit { get; set; }
        public decimal Price { get; set; }
        public string FK_StoreId { get; set; }
        public virtual Store Store { get; set; }
    }
View Code
Picture

 然后我们把Commodity类里面的virtual去掉,在看一下,我们看到,查询出的实体不是代理类了,实体中的导航属性为null

public class Commodity : BaseEntity
    {
        public string Name { get; set; }
        public string Unit { get; set; }
        public decimal Price { get; set; }
        public string FK_StoreId { get; set; }
        public Store Store { get; set; }  //  去掉vitrual
    }
View Code
Picture

 那么我们现在使用Include加载里面的导航属性,可以看到饥饿加载出来的导航属性为代理类型

Picture

 我把Store商店类和BaseEntity贴出来

 public class Store:BaseEntity
    {
        public string Name { get; set; }
        public string Address { get; set; }
        public virtual ICollection<Commodity> Commodities { get; set; }
    }
View Code
public class BaseEntity
    {
        public BaseEntity()
        {
            this.Id = Guid.NewGuid().ToString();
            this.AddTime = DateTime.Now;
        }

        public string Id { get; set; }
        public DateTime AddTime { get; set; }
    }
View Code

 现在我们对上一步稍微修改下查询,我们把Store类中的virtual去掉,我们看到不管是实体本身,还是里面导航属性的类型都已经不是代理类型

Picture

 我们再来看一下,我有个Book类,它和谁都没有关系

public class Book:BaseEntity
    {
        public string Name { get; set; }
        public int PageSize { get; set; }
    }
View Code

查询所有book,没有代理类型

Picture

 那么为book类中随便一个属性加上virtual,再查询,可以看到没有代理类型

Picture

 既然这样,那么我们将Commodity类中的Store属性去掉virtual,然后在Name属性用virtual修饰。可以看到,不管查集合还是单个实体,都没有代理类型

Picture

 那么我们就来得出结论了,如果你的类型里面的导航属性(非基元类型的属性)被vitrual修饰,那么查询出的这个实体或者集合就是代理类型,包括导航属性也是代理类型。

实现快照追踪与代理追踪

上面试验的有点多,可能忘记了前面的问题。上面的测试虽然有代理类型,但是,这就并不代表就是使用的代理追踪。

因为代理追踪不会全盘扫描快照,进行对比。那么EF里面是哪个方法专管扫描比较这个事情呢?答案就是:DetectChanges()

所以我们只要试验某一个实体,对他的属性进行了修改,并且实体状态放生了变化。如果没有调用DetectChanges()方法就是使用的代理追踪,如果调用了DetectChanges()则是快照追踪

所以这里,就要开始调试EF源码了,我今天调了一下,实在是看不懂,但是找到了这个DetectChanges,我在里面加了句Console

Picture

 我们主要面向的是DbContext这个上下文,但是在DbContext上下文中还有一个内部的上下文叫做InternalContext,具体原理不清楚。在内部它使用的的是ObjectContext,而DetectChanges()方法就是在这里面。比如我们调用SaveChanges()方法时,其实他最终会调用DetactChanges

不只SaveChanges这一个方法会调用DetectChange,还有其他的几个方法,这几个方法,作者为我们列出来了

DbSet.Find

DbSet.Local

DbSet.Remove

DbSet.Add

DbSet.Attach

DbContext.SaveChanges

DbContext.GetValidationErrors

DbContext.Entry

DbContext.Tracker.Entries

现在我们来看一下,这是另一个项目,有三个model。和上面的一样,Order订单类、Porduct产品类、BaseEntity基类

//  基类
public class BaseEntity
    {
        public BaseEntity()
        {
            this.Id = Guid.NewGuid().ToString();
            this.AddTime = DateTime.Now;
        }
        public string Id { get; set; }
        public DateTime AddTime { get; set; }
    }

// 订单类
public class Order : BaseEntity
    {
        public  string OrderNO { get; set; }
        public  string Description { get; set; }
        public virtual ICollection<Product> Products { get; set; }
    }

//  产品类 
public class Product : BaseEntity
    {
        public string Name { get; set; }
        public decimal Price { get; set; }
        public string Unit { get; set; }
        public string FK_OrderId { get; set; }
        public virtual Order Order { get; set; }
    }
View Code

那么我们先添加一个订单验证一下,看看Entiry和SaveChanges是不是调用了DetectChanges方法。可以看到他调用了三次DetectChanges方法,Add一次,SaveChanges一次,Entry一次

Order o = new Order
{
      OrderNO = "order9999",
      Description = "xxx"
};
 ctx.Orders.Add(o);
 ctx.SaveChanges();
 Console.WriteLine(ctx.Entry(o).State);
View Code
Picture

 那我们现在对一个查询出来的产品数据,修改它的属性,调用Entry获取它的状态,如果调用了DetectChanges并且状态改变,那么就是快照追踪;如果状态改变并且没有调用DetectChanges则说明使用的是代理追踪

var order = ctx.Orders.FirstOrDefault();
Console.WriteLine(ctx.Entry(order).State);
order.OrderNO = "dfdf";
Console.WriteLine(ctx.Entry(order).State);
View Code
Picture

 上面的说明是快照跟踪。同时也说明了另一个问题,我查询的是第一个订单,订单里面的产品集合我是用virtual修改是,根据上面的结论这个实体类型和他的导航属性类型是代理类型,这就说明了,不是有了代理类型,就会使用代理追踪

代理追踪需要满足两个条件,二者缺一不可,我试了的,各位可以试一下

1、设置:AutoDetectChangesEnabled = false; 关闭自动追踪

 2、将你要设置为代理追踪的model的所有属性加上virtual,如果继承了基类也需要将基类的属性加上virtual

那么现在再来看,我们将OrderNo属性修改,此时这个属性是被virtual修饰了

ctx.Configuration.AutoDetectChangesEnabled = false;
var order = ctx.Orders.FirstOrDefault();
Console.WriteLine(ctx.Entry(order).State);
order.OrderNO = "dfdf";
Console.WriteLine(ctx.Entry(order).State);
View Code
Picture

 可以看到状态改变,并且没有调用DetectChanges()方法,这就是代理追踪。

现在我只是实现了代理追踪的方式,但是对于快照和代理的性能还一无所知,那么明天我就来简单弄一下。为什么说简单弄一下,因为作者给出的结论就是:这两种追踪方式性能上基本没什么区别,EF团队也没有在代理追踪上花太多功夫,这是一个没什么用的东西。

而且我自己也实在对这个东西不了解,面对源码完全是懵的。

代理追踪与延迟加载的关系

最后来看一下代理追踪和延迟加载的关系,他们之间有什么关系呢?

首先我们看看下面的查询,我们关闭代理:ProxyCreationEnabled = false;接着查询第一个产品

ctx.Configuration.ProxyCreationEnabled = false;
var prod = ctx.Products.FirstOrDefault();
Console.WriteLine(JsonConvert.SerializeObject(prod,set));
View Code

序列化出的JSON如下,可以看到,导航属性Order为Null

{
    "Name": "柚子",
    "Price": 5,
    "Unit": "斤",
    "FK_OrderId": "0b03be26-8c3d-40b9-bf85-7dbd877b3f4e",
    "Order": null,
    "Id": "18dec640-b54f-4593-8342-2b7393f8c018",
    "AddTime": "2019-01-20T12:33:39.53"
}
View Code

 这是为什么?关闭延迟加载不应该是设置LazyLoadingEnabled = false;  或者将virtual关键字去掉吗?为什么关闭了代理也无法延迟加载了?

其实延迟加载必须满足三个条件

1.Configuration.ProxyCreationEnabled = true;

2.Configuration.LazyLoadingEnabled = true;

3.导航属性修饰符必须为virtual

这就是他们之间的关系,更深入的关系我也说不上来,真遗憾。

其实有一个事比较奇怪,那就是我分别关闭代理和延迟,序列化出来的JSON内容一样,但是顺序不一样,一起来看下

关闭延迟

//  关闭延迟
ctx.Configuration.LazyLoadingEnabled = false;
var prod = ctx.Products.Include("Order").First();
Console.WriteLine(JsonConvert.SerializeObject(prod,set));
View Code
{
    "Order": {
        "Id": "0b03be26-8c3d-40b9-bf85-7dbd877b3f4e",
        "OrderNO": "ttttttt",
        "Description": "xxx",
        "AddTime": "2019-01-20T12:33:39.53",
        "Products": []
    },
    "Name": "柚子",
    "Price": 5,
    "Unit": "斤",
    "FK_OrderId": "0b03be26-8c3d-40b9-bf85-7dbd877b3f4e",
    "Id": "18dec640-b54f-4593-8342-2b7393f8c018",
    "AddTime": "2019-01-20T12:33:39.53"
}
View Code

关闭代理

//  关闭代理
ctx.Configuration.ProxyCreationEnabled = false;
var prod = ctx.Products.Include("Order").First();
Console.WriteLine(JsonConvert.SerializeObject(prod, set));
View Code
{
    "Name": "柚子",
    "Price": 5,
    "Unit": "斤",
    "FK_OrderId": "0b03be26-8c3d-40b9-bf85-7dbd877b3f4e",
    "Order": {
        "OrderNO": "ttttttt",
        "Description": "xxx",
        "Products": [],
        "Id": "0b03be26-8c3d-40b9-bf85-7dbd877b3f4e",
        "AddTime": "2019-01-20T12:33:39.53"
    },
    "Id": "18dec640-b54f-4593-8342-2b7393f8c018",
    "AddTime": "2019-01-20T12:33:39.53"
}
View Code

 同时关闭延迟和代理,序列化出来的JSON和仅关闭代理是一样的

//  那我们来同时关闭延迟和代理
ctx.Configuration.LazyLoadingEnabled = false;
ctx.Configuration.ProxyCreationEnabled = false;               
var prod = ctx.Products.Include("Order").First();
Console.WriteLine(JsonConvert.SerializeObject(prod,set));
View Code
{
    "Name": "柚子",
    "Price": 5,
    "Unit": "斤",
    "FK_OrderId": "0b03be26-8c3d-40b9-bf85-7dbd877b3f4e",
    "Order": {
        "OrderNO": "ttttttt",
        "Description": "xxx",
        "Products": [],
        "Id": "0b03be26-8c3d-40b9-bf85-7dbd877b3f4e",
        "AddTime": "2019-01-20T12:33:39.53"
    },
    "Id": "18dec640-b54f-4593-8342-2b7393f8c018",
    "AddTime": "2019-01-20T12:33:39.53"
}
View Code

 我们上面的都是关闭延迟加载,序列化的结果都是,导航属性里面的导航属性是为空的

那么如果我不关闭延迟加载,直接使用Netonsoft.json的忽略循环引用的配置来序列化,则会发现不一样。他是导航属性里面的导航属性还有值

//  忽略循环引用
JsonSerializerSettings set = new JsonSerializerSettings();
set.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;
var res = ctx.Products.FirstOrDefault();
Console.WriteLine(JsonConvert.SerializeObject(res,set));
View Code
{
    "Order": {
        "Id": "0b03be26-8c3d-40b9-bf85-7dbd877b3f4e",
        "OrderNO": "ttttttt",
        "Description": "xxx",
        "AddTime": "2019-01-20T12:33:39.53",
        "Products": [
            {
                "Name": "椪柑",
                "Price": 3.3,
                "Unit": "斤",
                "FK_OrderId": "0b03be26-8c3d-40b9-bf85-7dbd877b3f4e",
                "Id": "3959d99c-ab5f-4c28-a7b4-687337ca205d",
                "AddTime": "2019-01-20T12:33:39.53"
            },
            {
                "Name": "橙子",
                "Price": 4.9,
                "Unit": "斤",
                "FK_OrderId": "0b03be26-8c3d-40b9-bf85-7dbd877b3f4e",
                "Id": "cdcb9a8e-1fd2-4ec3-8351-81097b254598",
                "AddTime": "2019-01-20T12:33:39.53"
            }
        ]
    },
    "Name": "柚子",
    "Price": 5,
    "Unit": "斤",
    "FK_OrderId": "0b03be26-8c3d-40b9-bf85-7dbd877b3f4e",
    "Id": "18dec640-b54f-4593-8342-2b7393f8c018",
    "AddTime": "2019-01-20T12:33:39.53"
}
View Code

 最后总算要结束了,今天在调试EF源码的时候碰到了一个有意思的东西,我已经制作成了GIF图。

什么东西呢?就是我们在调试程序的时候,比如我们要查看某一个变量里面的情况,那么鼠标移上去,会出现下拉框显示关于这个变量的信息。

可是我碰到的这个还真不是简单的显示,你看我这个,我每次移动上去触发显示详情,EF就会发起一次查询。

Picture

 

posted @ 2019-01-22 01:46  张四海  阅读(972)  评论(0编辑  收藏  举报