《Entity Framework 6 Recipes》中文翻譯系列 (30) ------ 第六章 繼承與建模高級應用之多對多關聯


翻譯的初衷以及為什么選擇《Entity Framework 6 Recipes》來學習,請看本系列開篇

第六章  繼承與建模高級應用

  現在,你應該對實體框架中基本的建模有了一定的了解,本章將幫助你解決許多常見的、復雜的建模問題,並解決你可能在現實中遇到的建模問題。

  本章以多對多關系開始,這個類型的關系,無論是在現存系統還是新項目的建模中都非常普遍。接下來,我們會了解自引用關系,並探索獲取嵌套對象圖的各種策略。最后,本章以繼承的高級建模和實體條件結束。

 

6-1  獲取多對多關聯中的鏈接表

問題

  你想獲取鏈接表的鍵,鏈接表鏈接兩個多對多關聯的實體。

解決方案

  假設你一個包含Event和Organizer實體和它們之前多對多關聯的模型,如圖6-1所示。

圖6-1 Event和Organizer實體和它們之前多對多關聯的模型

 

  正如我們在第二章演示的,多對多關聯代表的是數據庫中的一個中間表,這個中間表叫做鏈接表(譯注:也稱關聯表,但使用這個詞會容易與關系兩邊的表的描述(關聯表)產生混淆,所以這里使用鏈接表一詞)。鏈接表包含關系兩邊的外鍵(如圖6-2)。當鏈接表中沒有額外的列時,實體框架在導入關聯表時,向導會在兩個關聯表間生成一個多對多的關聯。鏈接表不被表示為一個實體,而是被表示成一個對多對多的關聯。

圖6-2 數據庫關聯圖,展示鏈接表EventOrganizer包含兩個關聯表Event 和 Oranizer的外鍵

  為了獲取實體鍵EventId,和OrganizerId,我們可以使用嵌套的from從句,或者 SelectMany()方法。如代碼清單6-1所示。

代碼清單6-1. 使用嵌套from從句和SelectMany()方法獲取鏈接表

 1  using (var context = new Recipe1Context())
 2             {
 3                 var org = new Organizer { Name = "Community Charity" };
 4                 var evt = new Event { Name = "Fundraiser" };
 5                 org.Events.Add(evt);
 6                 context.Organizers.Add(org);
 7                 org = new Organizer { Name = "Boy Scouts" };
 8                 evt = new Event { Name = "Eagle Scout Dinner" };
 9                 org.Events.Add(evt);
10                 context.Organizers.Add(org);
11                 context.SaveChanges();
12             }
13 
14             using (var context = new Recipe1Context())
15             {
16                 var evsorg1 = from ev in context.Events
17                               from organizer in ev.Organizers
18                               select new { ev.EventId, organizer.OrganizerId };
19                 Console.WriteLine("Using nested from clauses...");
20                 foreach (var pair in evsorg1)
21                 {
22                     Console.WriteLine("EventId {0}, OrganizerId {1}",
23                                        pair.EventId,
24                                        pair.OrganizerId);
25                 }
26 
27                 var evsorg2 = context.Events
28                                      .SelectMany(e => e.Organizers,
29                                         (ev, org) => new { ev.EventId, org.OrganizerId });
30                 Console.WriteLine("\nUsing SelectMany()");
31                 foreach (var pair in evsorg2)
32                 {
33                     Console.WriteLine("EventId {0}, OrganizerId {1}",
34                                        pair.EventId, pair.OrganizerId);
35                 }
36             }

代碼清單6-1的輸出如下:

Using nested from clauses...
EventId 31, OrganizerId 87
EventId 32, OrganizerId 88
Using SelectMany()
EventId 31, OrganizerId 87
EventId 32, OrganizerId 88

原理

  在數據庫中,鏈接表是表示兩張表間多對多關系的通常做法。因為它除了定義兩張表間的關系之外,就沒有別的作用了,所以實體框架使用一個多對多關聯來表示它,不是一個單獨的實體。

  Event和Organizer間的多對多關聯,允許你從Event實體簡單地導航到與它關聯的organizers,從Organizer實體導航到所有與之關聯的events。然而,你只想獲取鏈接表中的外鍵,這樣做,可能是因為這些鍵有它自身的含義,或者你想使用這些外鍵來操作別的實體。這里有一個問題,鏈接表沒有被表示成一個實體,因此直接查詢它,是不可能的。在代碼清單6-1中,我們演示了兩種方式來獲取底層的外鍵,不需要實例化關聯兩邊的實體。

  第一種方法是,使用嵌套的from從句來獲取organizers和它們的每一個event。使用Event實體對象上的導航屬性Organizers,並憑借底層的鏈接表來枚舉每個event上的所有organizers。我們將結果重塑到包含兩個實體鍵屬性的匿名對象中。最后,我們枚舉結果集,並在控制台中輸出這一對實體鍵。

  第二中方法是,我們使用SelectMany()方法,投影organizers和他們的每一個event到,包含實體對象envets和organizers的鍵的匿名對象中。和嵌套的from從句一樣,通過導航屬性Organizers使用數據庫中鏈接表來實現。並使用與第一種方法一樣的方式來枚舉結果集。


 

6-2  將鏈接表表示成一個實體

問題

  你想將鏈接表表示成一個實體,而不是一個多對多關聯。

解決方案

  假設在你的數據庫中,表Worker和Task之前有一個多對多關系,如圖6-3所示。

圖6-3 表Worker和Task之前有一個多對多關系

   WorkerTask表只包含支持多對多關系的外鍵,再無別的列了。

  按下面的步驟,將關聯轉換成一個代表WorkerTask表的實體:

    1、創建一個POCO實體類WorkerTak,如代碼清單6-2所示;

    2、使用類型為ICollection<WorkerTask>的屬性WorkerTasks替換POCO實體Worker的屬性Tasks;

    3、使用類型為ICollection<WorkerTask>的屬性WorkerTasks替換POCO實體Task的屬性Workers;

    4、在上下文對象DbContext的派生類中添加一個類型為DbSet<WorkerTask>的屬性;

  最終模型如代碼清單6-2所示。

代碼清單6-2.包含WorkerTask的最終數據模型

 1  [Table("Worker", Schema="Chapter6")]
 2     public class Worker
 3     {
 4         [Key]
 5         [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
 6         public int WorkerId { get; set; }
 7         public string Name { get; set; }
 8 
 9         [ForeignKey("WorkerId")]
10         public virtual ICollection<WorkerTask> WorkerTasks { get; set; } 
11     }
12 
13     [Table("Task", Schema = "Chapter6")]
14     public class Task
15     {
16         [Key]
17         [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
18         public int TaskId { get; set; }
19 
20         [Column("Name")]
21         public string Title { get; set; }
22 
23         [ForeignKey("TaskId")]
24         public virtual ICollection<WorkerTask> WorkerTasks { get; set; } 
25     }
26 
27     [Table("WorkerTask", Schema = "Chapter6")]
28     public class WorkerTask
29     {
30         [Key]
31         [Column(Order = 1)]
32         public int WorkerId { get; set; }
33         
34         [Key]
35         [Column(Order = 2)]
36         public int TaskId { get; set; }
37 
38         [ForeignKey("WorkerId")]
39         public virtual Worker Worker { get; set; }
40 
41         [ForeignKey("TaskId")]
42         public virtual Task Task { get; set; }
43     }

原理

  在應用程序開發生命周期中,開發人員經常會在最開始的無載荷多對多關聯上增加一個載荷。在本節中,我們演示了如何將一個多對多關聯表示為一個單獨的實體,以方便添加額外的標量屬性。

  很多開發人員認為,多對多關聯最終都會包含載荷,於是他們為鏈接表創建了一個合成鍵(synthetic key),來代替傳統的外鍵構成的組合鍵(composite key)形式。

  下面是我們的新模型,已經沒有一個簡單的方式來導航多對多關聯。新模型中是兩個一對多的關聯,這需要增加一級,鏈接實體。代碼清單6-3演示了插入和查詢需要增加的額外工作。

代碼清單6-13. 插入和獲取Task和Worker實體

 1 using (var context = new Recipe2Context())
 2             {
 3                 context.Database.Log = content => Debug.Print(content);
 4                 var worker = new Worker { Name = "Jim" };
 5                 var task = new Task { Title = "Fold Envelopes" };
 6                 var workertask = new WorkerTask { Task = task, Worker = worker };
 7                 context.WorkerTasks.Add(workertask);
 8                 task = new Task { Title = "Mail Letters" };
 9                 workertask = new WorkerTask { Task = task, Worker = worker };
10                 context.WorkerTasks.Add(workertask);
11                 worker = new Worker { Name = "Sara" };
12                 task = new Task { Title = "Buy Envelopes" };
13                 workertask = new WorkerTask { Task = task, Worker = worker };
14                 context.WorkerTasks.Add(workertask);
15                 context.SaveChanges();
16             }
17 
18             using (var context = new Recipe2Context())
19             {
20                 Console.WriteLine("Workers and Their Tasks");
21                 Console.WriteLine("=======================");
22                 foreach (var worker in context.Workers)
23                 {
24                     Console.WriteLine("\n{0}'s tasks:", worker.Name);
25                     foreach (var wt in worker.WorkerTasks)
26                     {
27                         Console.WriteLine("\t{0}", wt.Task.Title);
28                     }
29                 }
30             }

代碼清單6-3 輸出如下:

Workers and Their Tasks
=======================
Jim's tasks:
    Fold Envelopes
    Mail Letters
Sara's tasks:
    Buy Envelopes

 

6-3  自引用的多對多關系建模

問題

  你有一張自引用的多對多關系的表,你想為這張表及它的關系建模。

解決方案

  假設你的表擁有一個使用鏈接表的自引用有關系,如圖6-4所示。

圖6-4 一個與自己多對多的關系表

 

  按下面的步驟為此表建模:

    1、在你的項目中創建一個繼承自DbContext的類Recipe3Context;

    2、使用代碼清單6-4中的代碼,在你的項目中添加一個POCO實體類 Product;

代碼清單6-4. 創建一個POCO實體類Product

 1  [Table("Product", Schema = "Chapter6")]
 2     public class Product
 3     {
 4         public Product()
 5         {
 6             RelatedProducts = new HashSet<Product>();
 7             OtherRelatedProducts = new HashSet<Product>();
 8         }
 9 
10         [Key]
11         [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
12         public int ProductId { get; set; }
13         public string Name { get; set; }
14         public decimal Price { get; set; }
15 
16         //自己(本product)的關聯Products
17         public virtual ICollection<Product> RelatedProducts { get; set; }
18 
19         //與自己(本product)關聯的Products
20         public virtual ICollection<Product> OtherRelatedProducts { get; set; } 
21 
22     }

    3、在上下文對象Recipe3Context中添加一個類型為DbSet<Product>的屬性;

    4、在Recipe3Context中重寫上下文對象DbContext的方法OnModelCreating,創建自引用的多對多關系映射,如代碼清單6-5所示。

代碼清單6-5. 重寫上下文對象DbContext的方法OnModelCreating,創建自引用的多對多關系映射 

 1    protected override void OnModelCreating(DbModelBuilder modelBuilder)
 2         {
 3             base.OnModelCreating(modelBuilder);
 4 
 5             modelBuilder.Entity<Product>()
 6                         .HasMany(p => p.RelatedProducts)
 7                         .WithMany(p => p.OtherRelatedProducts)
 8                         .Map(m =>
 9                                  {
10                                      m.MapLeftKey("ProductId");
11                                      m.MapRightKey("RelatedProductId");
12                                      m.ToTable("RelatedProduct", "Chapter6");
13                                  });
14         }

原理

  正如你看到的那樣,實體框架很容易就支持了一個自引用的多對多關聯。我們在Product類中創建了兩個導航屬性,RelatedProducts和OtherRelatedProducts,並在DbContext的派生類中將其映射到底層的數據庫中。

  代碼清單6-6,插入與獲取一些關聯的products。為了獲取給定Product的所有關聯Products,我們遍歷了兩導航屬性RelatedProducts和OtherelatedProducts。

  產品Tent(帳篷)與產品Ground Cover(地被植物)通過Ten的導航屬性RelatedProducts相關聯,因為我們將Ground Cover添加到Ten的導航屬性RealteProducts集合中。產品Pole(桿)與產品Ten(帳篷)通過Ten的導航屬性OtherRelatedProducts相關聯,因為我們將Ten添加到Pole的導航屬性RelatedProducts集合中。這個關聯具體有雙向性。在一個方向上,它是一個關聯產品,在另一個方向上,這又一個被關聯的產品。

代碼清單6-6. 獲取關聯產品

using (var context = new Recipe3Context())
            {
                var product1 = new Product { Name = "Pole", Price = 12.97M };
                var product2 = new Product { Name = "Tent", Price = 199.95M };
                var product3 = new Product { Name = "Ground Cover", Price = 29.95M };
                product2.RelatedProducts.Add(product3);
                product1.RelatedProducts.Add(product2);
                context.Products.Add(product1);
                context.SaveChanges();
            } using (var context = new Recipe3Context())
            {
                var product2 = context.Products.First(p => p.Name == "Tent");
                Console.WriteLine("Product: {0} ... {1}", product2.Name,
                                   product2.Price.ToString("C"));
                Console.WriteLine("Related Products");
                foreach (var prod in product2.RelatedProducts)
                {
                    Console.WriteLine("\t{0} ... {1}", prod.Name, prod.Price.ToString("C"));
                }
                foreach (var prod in product2.OtherRelatedProducts)
                {
                    Console.WriteLine("\t{0} ... {1}", prod.Name, prod.Price.ToString("C"));
                }
            }

代碼清單6-6的輸出如下:

Product: Tent ... $199.95
Related Products
    Ground Cover ... $29.95
    Pole ... $12.97

 

  在代碼清單6-6中,只獲取第一層的關聯產品。傳遞關系(transitve relationship)是一個跨越了多層的關系,像一個層次結構。如果我們假設“關聯產品(related products)"關系是可傳遞的,那么,我們可能需要使用傳遞閉包(transitive closure)形式(譯注:這個概念有點繞,大家仔細理解。傳遞閉包、即在數學中,在集合 X 上的二元關系 R 的傳遞閉包是包含 R 的 X 上的最小的傳遞關系。例如,如果 X 是(生或死)人的集合而 R 是關系“為父子”,則 R 的傳遞閉包是關系“x 是 y 的祖先”。再比如,如果 X 是空港的集合而關系 xRy 為“從空港 x 到空港 y 有直航”,則 R 的傳遞閉包是“可能經一次或多次航行從 x 飛到 y”)。無論有多少層,傳遞閉包都將包含所有的關聯產品。在電商務應用中,產品專家創建第一層的關聯產品,額外層級的關聯產品可以通過傳遞閉包推導出來 。最終的結果是,這些應用在你處理訂單時會有類似這樣的提示“……你可能感興趣的還有……”。

  在代碼清單6-7中,我們使用遞歸方法來處理傳遞閉包。在遍歷導航屬性RelatedProducts和OtherrelatedProduxts時,我們要格外小心,不要陷入一個死循環中。如果產品A關聯產品B,然后產品B又關聯產品A,這樣,我們的應用就會陷入無限遞歸中。為了阻止這種情況的發生,我們使用一個Dictionary<>來幫助我們處理已遍歷過的路徑。

代碼清單6-7.關聯產品關系的傳遞閉包

 1 public static void Run()
 2         {
 3             using (var context = new Recipe3Context())
 4             {
 5                 var product1 = new Product { Name = "Pole", Price = 12.97M };
 6                 var product2 = new Product { Name = "Tent", Price = 199.95M };
 7                 var product3 = new Product { Name = "Ground Cover", Price = 29.95M };
 8                 product2.RelatedProducts.Add(product3);
 9                 product1.RelatedProducts.Add(product2);
10                 context.Products.Add(product1);
11                 context.SaveChanges();
12             }
13 
14             using (var context = new Recipe3Context())
15             {
16                 var product1 = context.Products.First(p => p.Name == "Pole");
17                 Dictionary<int, Product> t = new Dictionary<int, Product>();
18                 GetRelated(context, product1, t);
19                 Console.WriteLine("Products related to {0}", product1.Name);
20                 foreach (var key in t.Keys)
21                 {
22                     Console.WriteLine("\t{0}", t[key].Name);
23                 }
24             }
25             
26         }
27 
28         static void GetRelated(DbContext context, Product p, Dictionary<int, Product> t)
29         {
30             context.Entry(p).Collection(ep => ep.RelatedProducts).Load();
31             foreach (var relatedProduct in p.RelatedProducts)
32             {
33                 if (!t.ContainsKey(relatedProduct.ProductId))
34                 {
35                     t.Add(relatedProduct.ProductId, relatedProduct);
36                     GetRelated(context, relatedProduct, t);
37                 }
38             }
39             context.Entry(p).Collection(ep => ep.OtherRelatedProducts).Load();
40             foreach (var otherRelated in p.OtherRelatedProducts)
41             {
42                 if (!t.ContainsKey(otherRelated.ProductId))
43                 {
44                     t.Add(otherRelated.ProductId, otherRelated);
45                     GetRelated(context, otherRelated, t);
46                 }
47             }
48         }

 

  在代碼清單6-7中,我們使用Load()方法(見第五章)來確保關聯產品的集合被加載。不幸的是,這意味着,將會有更多的數據庫交互。我們可能會想到,預先從Product表中加載出所有的行,並希望Relationship span(關聯建立)能幫我們建立好關聯。但是,Relationship span不會為實體集合建立關聯(譯注:導航屬性為集合的情況),只會為實體引用建議關聯。因為我們的關系是多對多(實體集合),所以,我們不能依靠relationship span來幫我們解決這個問題,只能依靠Load()方法。

  代碼清單6-7的輸出如下。代碼塊的第一部分,插入關系,我們可以看到,Pole關聯Ten,Ten關聯Ground Cover。Pole的關聯產品的傳遞閉包包含,Ten,Groud Cover,和Pole。Pole被包含的原因是,它在Pole與Ten的關系中的另一端。

Products related to Pole
    Tent
    Ground Cover
    Pole

 

  這一篇講了三個主題,內容有點多,第三個主題的內容又有點繞,翻譯時也費了不少腦力。感謝你閱讀。下篇再見!

 

  

實體框架交流QQ群:  458326058,歡迎有興趣的朋友加入一起交流

謝謝大家的持續關注,我的博客地址:http://www.cnblogs.com/VolcanoCloud/

 


免責聲明!

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



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