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
那么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; } }
然后我们把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 }
那么我们现在使用Include加载里面的导航属性,可以看到饥饿加载出来的导航属性为代理类型
我把Store商店类和BaseEntity贴出来
public class Store:BaseEntity { public string Name { get; set; } public string Address { get; set; } public virtual ICollection<Commodity> Commodities { get; set; } }
public class BaseEntity { public BaseEntity() { this.Id = Guid.NewGuid().ToString(); this.AddTime = DateTime.Now; } public string Id { get; set; } public DateTime AddTime { get; set; } }
现在我们对上一步稍微修改下查询,我们把Store类中的virtual去掉,我们看到不管是实体本身,还是里面导航属性的类型都已经不是代理类型
我们再来看一下,我有个Book类,它和谁都没有关系
public class Book:BaseEntity { public string Name { get; set; } public int PageSize { get; set; } }
查询所有book,没有代理类型
那么为book类中随便一个属性加上virtual,再查询,可以看到没有代理类型
既然这样,那么我们将Commodity类中的Store属性去掉virtual,然后在Name属性用virtual修饰。可以看到,不管查集合还是单个实体,都没有代理类型
那么我们就来得出结论了,如果你的类型里面的导航属性(非基元类型的属性)被vitrual修饰,那么查询出的这个实体或者集合就是代理类型,包括导航属性也是代理类型。
实现快照追踪与代理追踪
上面试验的有点多,可能忘记了前面的问题。上面的测试虽然有代理类型,但是,这就并不代表就是使用的代理追踪。
因为代理追踪不会全盘扫描快照,进行对比。那么EF里面是哪个方法专管扫描比较这个事情呢?答案就是:DetectChanges()
所以我们只要试验某一个实体,对他的属性进行了修改,并且实体状态放生了变化。如果没有调用DetectChanges()方法就是使用的代理追踪,如果调用了DetectChanges()则是快照追踪
所以这里,就要开始调试EF源码了,我今天调了一下,实在是看不懂,但是找到了这个DetectChanges,我在里面加了句Console
我们主要面向的是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; } }
那么我们先添加一个订单验证一下,看看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);
那我们现在对一个查询出来的产品数据,修改它的属性,调用Entry获取它的状态,如果调用了DetectChanges并且状态改变,那么就是快照追踪;如果状态改变并且没有调用DetectChanges则说明使用的是代理追踪
var order = ctx.Orders.FirstOrDefault(); Console.WriteLine(ctx.Entry(order).State); order.OrderNO = "dfdf"; Console.WriteLine(ctx.Entry(order).State);
上面的说明是快照跟踪。同时也说明了另一个问题,我查询的是第一个订单,订单里面的产品集合我是用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);
可以看到状态改变,并且没有调用DetectChanges()方法,这就是代理追踪。
现在我只是实现了代理追踪的方式,但是对于快照和代理的性能还一无所知,那么明天我就来简单弄一下。为什么说简单弄一下,因为作者给出的结论就是:这两种追踪方式性能上基本没什么区别,EF团队也没有在代理追踪上花太多功夫,这是一个没什么用的东西。
而且我自己也实在对这个东西不了解,面对源码完全是懵的。
代理追踪与延迟加载的关系
最后来看一下代理追踪和延迟加载的关系,他们之间有什么关系呢?
首先我们看看下面的查询,我们关闭代理:ProxyCreationEnabled = false;接着查询第一个产品
ctx.Configuration.ProxyCreationEnabled = false; var prod = ctx.Products.FirstOrDefault(); Console.WriteLine(JsonConvert.SerializeObject(prod,set));
序列化出的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" }
这是为什么?关闭延迟加载不应该是设置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));
{ "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" }
关闭代理
// 关闭代理 ctx.Configuration.ProxyCreationEnabled = false; var prod = ctx.Products.Include("Order").First(); Console.WriteLine(JsonConvert.SerializeObject(prod, set));
{ "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" }
同时关闭延迟和代理,序列化出来的JSON和仅关闭代理是一样的
// 那我们来同时关闭延迟和代理 ctx.Configuration.LazyLoadingEnabled = false; ctx.Configuration.ProxyCreationEnabled = false; var prod = ctx.Products.Include("Order").First(); Console.WriteLine(JsonConvert.SerializeObject(prod,set));
{ "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" }
我们上面的都是关闭延迟加载,序列化的结果都是,导航属性里面的导航属性是为空的
那么如果我不关闭延迟加载,直接使用Netonsoft.json的忽略循环引用的配置来序列化,则会发现不一样。他是导航属性里面的导航属性还有值
// 忽略循环引用 JsonSerializerSettings set = new JsonSerializerSettings(); set.ReferenceLoopHandling = ReferenceLoopHandling.Ignore; var res = ctx.Products.FirstOrDefault(); Console.WriteLine(JsonConvert.SerializeObject(res,set));
{ "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" }
最后总算要结束了,今天在调试EF源码的时候碰到了一个有意思的东西,我已经制作成了GIF图。
什么东西呢?就是我们在调试程序的时候,比如我们要查看某一个变量里面的情况,那么鼠标移上去,会出现下拉框显示关于这个变量的信息。
可是我碰到的这个还真不是简单的显示,你看我这个,我每次移动上去触发显示详情,EF就会发起一次查询。