EntityFramework之你不知道的那些事(七)

前言

前面一系列几乎都是循序渐进式的进行叙述,似乎脚步走得太快了,于是我开始歇一歇去追寻一些我所不太了解的细枝末节,在此过程中也屡次碰壁,但是唯有如此才能更好的成长,不是吗!希望此文对你亦有帮助。

属性私有化 

我们之前有点太循规蹈矩对于模型的建立,所以你才不会遇到问题(当然我也是),也许你大概也这样做过,当建立实体时我们都是建立公有的(public)的,那为什么不试试私有(private)的呢?建立一个学生(Student)类并给其一个私有属性,如下:

    public class Student
    {
        public string Id { get; set; }
        public string Name { get; set; }
        private string TestPrivate { get; set; }
        public string FlowerId { get; set; }
        public virtual Flower Flower { get; set; }
     
    }

当初始化数据库生成表时,着实令我惊讶了一番,该私有属性并未映射进来。如下:

我依然不相信,于是我将其该属性访问换成受保护的(protected)的,结果依然是岿然不变。

那问题来了,这样的需求可能是存在的,要是在类中我们不想暴露有些属性想将其设为私有的,我们该如何使其映射到数据库表中呢?

在EF Code First中有一个特性叫自定义Code First约定(Custom Code First Conventions),也就是我们可以自定义配置实体的模式以及规则,然后将其注册到 OnModelCreating 方法中即可。

上述只是讲述进行注册,但是我们需要在上述方法中得到属性集合并设置其属性为 BindingFlags.NonPublic|BindingFlags.Instance ,这就是我们整个完整的想法。基于此,我们开始进行第一次尝试:

            modelBuilder.Properties().Configure(p =>
            {
                var privatePropertity = p.ClrPropertyInfo;
p.HasColumnName(privatePropertity.Name); });

/*看到上述有个Properties方法,但是令人厌恶的是此方法无参数,也就是无法获得私有的属性,所以在约定中进行注册尝试失败*/

想到映射是基于 EntityTypeConfiguration<TEntity> 实现,将其单独放到一个类里,想必是不可能了,因为无法访问其私有属性,于是将其放在Student里,就可以访问私有属性了,那进行第二次尝试,在Student类中进行改造如下:

    public class Student
    {
        public string Id { get; set; }
        public string Name { get; set; }
        private string TestPrivate { get; set; }
        public string FlowerId { get; set; }
        public virtual Flower Flower { get; set; }

        public  class StudentMap : EntityTypeConfiguration<Student>
        {
            public StudentMap()
            {
                ToTable("Student");
                HasKey(key => key.Id);
                HasRequired(p => p.Flower).WithMany(p => p.Students).HasForeignKey(p => p.FlowerId);
                Property(p => p.TestPrivate);
            }
        }

    }

接下来就是验证的时刻了,结果通过,如下:

 根据上述EF就属性私有化的问题,我将其描述如下

默认情况下,EF Code First仅仅只映射实体中的公有属性,但是如果你想映射一个属性访问符为受保护的或者私有的,则配置类必须要嵌套在实体类中。

上述虽然能够满足要求,看着也就差强人意,因为实体类不是太干净,并且该实体类也不满足POCO了,并且不利于解耦,想想怎么去改善下即能映射又能使配置类在实体类中,既然只能在本类中访问到该私有属性,那考虑用部分类来实现,所以我们进行第三次尝试来作为改善。我们将Student类原有的一切不动,只是将其标记为部分类(partial)即可,保持其为POCO,在另一个部分类中获得其私有属性,当映射时来访问该部分类即可,基于此,我们给出完整代码:

学生部分类:

    public partial class Student
    {
        public string Id { get; set; }
        public string Name { get; set; }
        private string TestPrivate { get; set; }
        public string FlowerId { get; set; }
        public virtual Flower Flower { get; set; }

    }

获得学生类中私有属性的学生部分类:

    public partial class Student
    {

        public class PrivatePropertyExtension
        {
            public static readonly Expression<Func<Student, string>> test_private = p => p.TestPrivate;
        }
    }

接着在映射中访问该私有属性学生部分类:

    public  class StudentMap :EntityTypeConfiguration<Student>
    {
        public StudentMap()
        {
            ToTable("Student");
            HasKey(key => key.Id);
            HasRequired(p => p.Flower).WithMany(p => p.Students).HasForeignKey(p => p.FlowerId);
            Property(Student.PrivatePropertyExtension.test_private);
        }

    }

经过这么小小的改善后,如果有多个私有属性,只需要将其添加到私有属性的扩展中去即可。

惊喜发现

当你看到上述时是不是感觉已经接近完美了,其实还有一劳永逸的办法【特此声明:非为了制造意外,刚开始在其过程中,我也是这样做的,当我继续向下学习过程中,偶然发现EF居然还是有根本上的解决办法,写到这里只是为了说明我学习的过程,仅此而已。】在 OnModelCreating 方法中去注册的想法是正确的,只是未找对方法。只需如下一点代码就可以解决所有实体中的非公有属性都会映射到数据库中。

            modelBuilder.Types().Configure(d =>
            {
                var nonPublicProperties = d.ClrType.GetProperties(BindingFlags.NonPublic | BindingFlags.Instance);
                foreach (var p in nonPublicProperties)
                {
                    d.Property(p).HasColumnName(p.Name);
                }
            });

对于上述我们最终将属性私有化总结如下:

默认情况下,EF Code First仅仅只映射实体中的公有属性,但是如果你想映射实体中的所有非公有属性,只需通过上述操作然后在 OnModelCreating 中注册即可

【注意】上述是在EF 6.0中,如果你EF版本6.0之前可以试试通过下面来进行注册【本人未进行验证,查的资料】

      modelBuilder.Entities().Configure(c =>
      {
          var nonPublicProperties = c.ClrType.GetProperties(BindingFlags.NonPublic|BindingFlags.Instance);

          foreach (var p in nonPublicProperties)
          {
            c.Property(p).HasColumnName(p.Name);
          }
     });

属性internal

上述我们只是测试属性private、protected的情况,似乎落了internal,接下我们来看看,将如下属性设置成internal

  internal string TestPrivate { get; set; }

当我们再次映射时,是不会映射到数据库的,默认情况下,属性能够被映射到数据库必须是public的也就是你无需通过显示指定,当属性被internal修饰时,必须显示指定映射该属性,否则不予映射。在学生映射类显示指定如下:

        public StudentMap()
        {
            ToTable("Student");
            HasKey(key => key.Id);
            HasRequired(p => p.Flower).WithMany(p => p.Students).HasForeignKey(p => p.FlowerId);
            Property(p => p.TestPrivate);
        }

鉴于此,我们总结如下:

当属性被internal修饰时,若想该属性被映射到数据库必须显示指定映射该属性,否则不予映射。

至此属性被修饰符映射的情况就已全部被囊括了。通过上述【惊喜发现】则用这个就多余了,所以就当学习了。

变更追踪

原来不知道变更追踪还有两种形式,在查阅资料过程中才明白过来,这里就稍微详细的来叙述下。【这一部分,个人不是很有信心能说的很好,若有不妥之处,望指出】

快照式变更追踪(Snapshot based Change Tracking)

在【我为EF正名】这个小节中,对此跟踪只是稍微讲了其用途,接下来我们详细的来讨论下该方法。

快照式变更追踪实际上使用的是我们定义的POCO而非从该实体中派生出来的代理类(子类),这是一种很简单的变更追踪方案!下面我们一起来学习下(所用类依然是上述学生类):

            using (var ctx = new EntityDbContext())
            {var entity = ctx.Set<Student>().FirstOrDefault(d => d.Name == "xpy0929");

                Console.WriteLine(ctx.Entry(entity).State);

                entity.Name = "xpy0928";

                Console.WriteLine(ctx.Entry(entity).State);

             }

上述我们查出xpy0929的学生并将其姓名修改为xpy0928,同时打印修改前后的状态。在进行此操作之前我们在上下文中配置如下:

        public EntityDbContext()
            : base("name=DBConnectionString")
        {
this.Configuration.AutoDetectChangesEnabled = false; }

那问题来了,修改前后状态会变还是不变呢?结果打印如下:

接下来我们将此句  this.Configuration.AutoDetectChangesEnabled = false; 去掉再试试看,结果就又变了,如下:

那问题来了,只是关闭了AutoDetectChangesEnabled为什么会出现不同结果呢?

当你对实体进行更改后,此更改不能自动与Entry中的State状态保持同步,因为在POCO实体和EF之间没有能够自动通知的机制,所以即使你显示修改了其值其状态依然为Unchanged。但是当你令  this.Configuration.AutoDetectChangesEnabled = true; 此时就明确了可以调用 DetectChanges 方法,有人到这里估计就得说,尽瞎说,在上下文中哪有DetectChanges方法,肯定没有,因为不在DbContext中,而在ObjectContext上下文中,如下:

  var obj = ((IObjectContextAdapter)ctx).ObjectContext;

  obj.DetectChanges();

因为默认情况下在调用Entry方法时就自动会调用DetectChanges方法,所以其值就进行了修改(Modified)【不知道DetectChanges是什么,请参看前面文章】。

快照式变更追踪利用的是POCO,当查询数据时EF上下文便捕获了数据的快照,当调用DetectChanges方法时,会扫描上下中所有实体并将当前值和快照中的值进行比较,然后作出相关的行为。但是基于上述应意识到它的缺点,实体对象的改变与EF的ObjectStateManager之间是无法进行同步的。(所以这也就解释了在EF中,将AutoDetectChangesEnabled默认启动的原因。)

代理式变更追踪(Notification based Change Tracking with Proxies)

首先还是看官方关于代理的定义:代理是依赖注入的一种形式,其附加功能被注入到实体中,在POCO情况下,因为它们无法进行创建或执行查询,或告诉状态管理器关于发生的改变,但是代理是动态生成的能够覆盖各种属性的一个实体的派生类型,以至于能使额外的代码能运行在该派生类型上,在延迟加载的情况下,其表现在对导航属性的加载。

上面一大堆废话,还不如点实在的,创建一个Flower类的代理类,一目了然

                var obj = ((IObjectContextAdapter)ctx).ObjectContext;

                var entity = obj.CreateObject<Flower>();

如图动态代理类Flower后面紧跟着的大概是哈希码:

接下来,把上述所给数据以及类和相关配置依然不变,我们只是将Student进行修改如下(将所有属性加上virtual)【此时此类将为代理类】

    public class Student
    {
        public virtual int Id { get; set; }
        public virtual string Name { get; set; }
        public virtual int FlowerId { get; set; }
        public virtual Flower Flower { get; set; }

    }

那问题来了,这个操作之后,是否如快照式跟踪状态一样,不会改变呢?

这是什么情况,不是令 this.Configuration.AutoDetectChangesEnabled = false; 了吗,也就是不会调用DetectChanges方法了,应该是UnChanged才对,对吧??当你将类中所有属性设为virtual后,此时与上文中打交道的非POCO而是其派生类即动态代理类,此时实体将总是被跟踪的,并且在状态上与EF是保持同步的,因为当发生改变时,代理类会通知EF来改变值以及关系的修正。总体上来看,这可能使得其更加高效一点,因为ObjectStateManager(对象状态管理器)知道未发生改变的话就会跳过原始值和当前值之间的比较。

所以上述EF上下文能够通过代理类知道数据进行了修改,然后更新其状态。当然,我们也可以在配置中来禁止创建代理类,如下:

        public EntityDbContext()
            : base("name=DBConnectionString")
        {

            this.Configuration.ProxyCreationEnabled = false;
            
        }

基于此我们可以将数据的更新作出如下描述:

当变更追踪代理关闭后,在查询结果返回之前缓存了该实体的副本也就是利用快照式跟踪,数据更新后调用SaveChanges时会回调DetecChanges来比较其差异并更新到数据库。当未关闭变更追踪代理时,数据更新后,EF上下文知道变更追踪代理当前正在执行,于是将不会创建该实体副本,变更追踪代理会立即修改其值以及进行关系修正最后更新数据到数据库而不会等到调用SaveChanges才更改。

之前一直觉得要加载一个实体中关联的另一个实体只能用Include,后来在dudu老大Where查询条件中用到的关联实体不需要Include的这篇文章中提到当查询条件含有实体的话则将会自动加载该实体,动手一试果然如此。这句话说的没错,但是你们有没有想过,是在什么前提下,这才是正确的呢?(也许当你用时就出错了,难道是人品的问题吗,绝非偶然,存在即合理)下面我们来演示这种情况。

 var list = ctx.Set<Student>().Where(d => d.Flower.Remark == "so bad").ToList();

我们查询出学生的数据,查询条件为关联的实体Flower,这样就自动加载实体Flower了,似乎一切都挺合理,我们通过快速监视来看看如下图分析分析:

查询居然为空,难道dudu老大骗我们吗?No,you wrong,细心的你不知道发现什么别的东西没,你看看查询出来的实体Student的类型居然是ConsoleApplication1.Student,想必到了这里,你应该知道答案了,就是我们在上面演示时,关闭了变更追踪代理,所以这里的实体类不是代理类,导致你查询出来的关联实体为空。不信,我将其关闭,给出下面实体乱七八糟的实体类型的结果你看:

关于此就一句话来解释变更追踪代理的主要作用: 变更追踪代理是非常严格关于集合属性的类型 。同时当我们序列化实体时,非常容易导致循环引用,这时可以考虑关闭变更追踪代理。

那问题来了,当延迟加载时为什么导航属性必须要加上修饰符virtual呢?

因为动态生成的实体的派生类即代理类会指向EF并覆盖修饰符为virtual的导航属性,当EF被触发时,使得这些属性能够被监听和延迟加载。这也就说明了延迟加载必须满足三个条件

this.Configuration.ProxyCreationEnabled = true;

this.Configuration.LazyLoadingEnabled = true;

导航属性修饰符必须为Virtual

那问题又来了,我们什么时候应该用代理式变更追踪呢,它的优缺点又在哪里呢?

代理式变更追踪的优点

  1. 被跟踪的实体,当数据发生改变时会立即被检测到并进行关系修正而不是等到DetectChanges方法被调用
  2. 在某些场景下,性能也会有所提高

代理式变更追踪的缺点

启用变更追踪代理的规则具有严格性和限制性

  1. 实体类必须是public并且不能是密封类
  2. 所有的属性必须为访问修饰符为public或者protected并且修饰符必须为virtual且访问器有get和set
  3. Collection导航属性保证声明为为ICollection<T>(当然也可以为IList<T>等,建议为IColletion<T>以免出现意想不到的结果)
  4. 在某些场景下,性能也可能会比较差

代理式变更追踪的性能

值得注意的是,在深入一些具体的性能场景下,对于许多应用程序来说利用代理式变更追踪和快照式变更追踪来跟踪实体,这两者在性能上没有什么显著的区别。我们只需谨记一条法则:

在一个应用程序的上下文实例中跟踪成百上千个实体的同时你将会看到不同,但是对于大多数应用程序来说使用简单的快照式变更追踪就已经好了。

变更追踪代理提供最大的性能优势是当追踪很多实体时,只更改实体中少数,这是因为我之前有讲过快照式变更追踪会扫描和比较实体,而变更追踪代理无需进行扫描而是每个实体被更改时会被立即检测。

 

现在我们从反面来想,在每个被追踪的实体上都有修改,此时需要对每个实体的改变作出反应这样就产生了额外的代价,这种额外的代价就是设置下实体的属性通常还是非常快的,当然,这也意味着要通过额外的代码来记录状态的变化,但是很快实体多了,这影响就显而易见了。

就以下简单的实体类型我们来进行分析:

    public class SimpleClass
    {
        public int id { get; set; }

        public int property1 { get; set; }

        public int property2 { get; set; }

        public string property3 { get; set; }

        public string property4 { get; set; }

    }

我们通过修改10000个被追踪的实体的数据并调用SaveChanges来考虑其性能,代码如下:

                ctx.Database.Log = Console.WriteLine;

                Stopwatch watch = new Stopwatch();

                watch.Start();

                ctx.Set<SimpleClass>().ToList().ForEach(d =>
                {

                    var prop1 = d.property1;
                    d.property1 = d.property2;
                    d.property2 = prop1;

                    var prop3 = d.property3;
                    d.property3 = d.property4;
                    d.property4 = prop3;
                });
ctx.SaveChanges(); watch.Stop(); Console.WriteLine(watch.ElapsedMilliseconds);

 此时更新10000条数据需要92115毫秒即92秒,如下:

此时将SimpleClass所有属性加上Virtual,再来看看更新需要90460毫秒即90秒

【注意】根据笔记本配置不同可能结果会略有差异,但是可以得出结论的是即使将其变为变更追踪代理类其性能也没有多大提升。

在一个应用程序中,在调用SaveChanges方法之前如果对一个属性作出多处更改,此时使用快照式变更追踪的时间比代理式变更追踪的时间就稍微短一点点,但是至此代理式变更追踪的性能会越来越差。

 

接下来我们再来看一种情况,在应用程序中假如我们将属性设置到值那么代理式变更追踪的性能又会如何呢?假设这样一个情景我们现在要序列化类,此时肯定必然要将代理类DTO然后序列化,鉴于此我们给出如下代码:

代理类:

    public class SimpleClass
    {
        public virtual int id { get; set; }

        public virtual int property1 { get; set; }

        public virtual int property2 { get; set; }

        public virtual string property3 { get; set; }

        public virtual string property4 { get; set; }
    }

进行DTO然后序列化该代理类

           using (var ctx = new EntityDbContext())
            {

                ctx.Database.Log = Console.WriteLine;

                Stopwatch watch = new Stopwatch();

                watch.Start();

                ctx.Set<SimpleClass>().ToList().ForEach(d =>
                {
                    var simple = new SimpleClass();
                    simple.id = d.id;
                    simple.property1 = d.property1;
                    simple.property2 = d.property2;
                    simple.property3 = d.property3;
                    simple.property4 = d.property4;
                    new Program().Clone(simple);
                });

                ctx.SaveChanges();

                watch.Stop();

                Console.WriteLine(watch.ElapsedMilliseconds);

深度复制方法:

        public object Clone<TEntity>(TEntity t) where TEntity : class
        {
            using (MemoryStream stream = new MemoryStream())
            {
                XmlSerializer xml = new XmlSerializer(typeof(TEntity));
                xml.Serialize(stream, t);
                stream.Seek(0, SeekOrigin.Begin);
                return xml.Deserialize(stream) as TEntity;
            }

        }

我们测试依据代理式更追踪来序列化10000个实体的时间为1.713秒,如下:

接下来我们去掉所有属性的virtual,再来测试利用快照式变更追踪的时间为1.667秒

【注】可能跟我笔记本配置低(cpu为i3,内存 4g)有关,理论上来说快照式变更追踪的所需时间应该比代理式变更追踪的时间要少很多,为何这样说呢?因为基于快照式变更追踪数据没有发生任何变化,我们只是进行了赋值而已,所以不会调用SaveChanges时不会对数据库进行写的操作,但是代理式变更追踪只要对属性进行了写的操作就会标记为已修改,所以所花时间应该更多。

总结

对于代理式变更追踪个人而言似乎没有什么可用之处,似乎在园子里也没看见园友们将所有属性标记为virtual作为代理类来用,再者EF团队在代理式变更追踪方面并没有过多的去投入,因为我是查资料看到了这个,所以就纯属了解了,知道有这个东西就ok了。

所以我的建议是:用virtual只是用于导航属性的延迟加载,如果你想用代理式变更追踪除非你真正的理解了它的问题并有一个合理的理由去使用它(暂时我是没发现它好在什么地方),当然这也就意味着当用快照式变更追踪遇到了性能瓶颈而考虑用代理式变更追踪。(大部分情况下还是不会)。

基类映射

在EF 6.0以上版本中在派生类中无需手动添加对基类的映射,当派生于基类时,只需对派生类进行配置即可,基类属性自动进行映射。

假设这样一个场景:我们定义一个基类Base,将此类中的Id作为Blog和Post类中主键。一个Blog当然对应多个Post。

基类:

    public class Base
    {
        public int Id { get; set; }

        public byte[] Token { get; set; }
    }

Blog和Post类

    public class Blog : Base
    {
        public string Name { get; set; }

        public string Url { get; set; }

        public virtual ICollection<Post> Posts { get; set; }
    }
public class Post : Base { public string Title { get; set; } public string Content { get; set; } public int BlogId { get; set; } public virtual Blog Blog { get; set; } }

相关映射:

        public BlogMap()
        {
            ToTable("Blog");
            HasMany(p => p.Posts).WithRequired(p => p.Blog).HasForeignKey(p => p.BlogId);
        }

        public PostMap()
        {
            ToTable("Post");
        }

上述我们未对基类进行任何映射,但是生成表时基类的Id将作为派生类Blog和Post的主键以及Token的映射,如下:

但是在EF 6.0之前无法配置一个不能映射的基类,如果你想配置一个定义在基类上的属性,此时你可以一劳永逸通过配置来进行完成,如下:

            modelBuilder.Types<Base>().Configure(c =>
            {
                c.HasKey(e => e.Id);
                c.Property(e => e.Token);
            });

 

posted @ 2015-09-04 14:24  Jeffcky  阅读(10190)  评论(30编辑  收藏  举报