zoukankan      html  css  js  c++  java
  • 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
  • 相关阅读:
    按升序合并如下两个list, 并去除重复的元素
    python数据结构
    驼峰式命名改下划线命名
    求某个数出现的次数超过了总数的一半
    翻转字符串
    复习
    RESTful
    Flask wtforms
    数据库连接池(DBUtils)
    iOS
  • 原文地址:https://www.cnblogs.com/jinshan-go/p/10301710.html
Copyright © 2011-2022 走看看