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);
            });

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM