翻譯的初衷以及爲何選擇《Entity Framework 6 Recipes》來學習,請看本系列開篇html
如今,你應該對實體框架中基本的建模有了必定的瞭解,本章將幫助你解決許多常見的、複雜的建模問題,並解決你可能在現實中遇到的建模問題。數據庫
本章以多對多關係開始,這個類型的關係,不管是在現存系統仍是新項目的建模中都很是廣泛。接下來,咱們會了解自引用關係,並探索獲取嵌套對象圖的各類策略。最後,本章以繼承的高級建模和實體條件結束。閉包
問題框架
你想獲取連接表的鍵,連接表連接兩個多對多關聯的實體。ide
解決方案學習
假設你一個包含Event和Organizer實體和它們以前多對多關聯的模型,如圖6-1所示。ui
圖6-1 Event和Organizer實體和它們以前多對多關聯的模型spa
正如咱們在第二章演示的,多對多關聯表明的是數據庫中的一箇中間表,這個中間表叫作連接表(譯註:也稱關聯表,但使用這個詞會容易與關係兩邊的表的描述(關聯表)產生混淆,因此這裏使用連接表一詞)。連接表包含關係兩邊的外鍵(如圖6-2)。當連接表中沒有額外的列時,實體框架在導入關聯表時,嚮導會在兩個關聯表間生成一個多對多的關聯。連接表不被表示爲一個實體,而是被表示成一個對多對多的關聯。翻譯
圖6-2 數據庫關聯圖,展現連接表EventOrganizer包含兩個關聯表Event 和 Oranizer的外鍵code
爲了獲取實體鍵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使用數據庫中連接表來實現。並使用與第一種方法同樣的方式來枚舉結果集。
問題
你想將連接表表示成一個實體,而不是一個多對多關聯。
解決方案
假設在你的數據庫中,表Worker和Task以前有一個多對多關係,如圖6-3所示。
圖6-3 表Worker和Task以前有一個多對多關係
WorkerTask表只包含支持多對多關係的外鍵,再無別的列了。
按下面的步驟,將關聯轉換成一個表明WorkerTask表的實體:
一、建立一個POCO實體類WorkerTak,如代碼清單6-2所示;
二、使用類型爲ICollection<WorkerTask>的屬性WorkerTasks替換POCO實體Worker的屬性Tasks;
三、使用類型爲ICollection<WorkerTask>的屬性WorkerTasks替換POCO實體Task的屬性Workers;
四、在上下文對象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-4所示。
圖6-4 一個與本身多對多的關係表
按下面的步驟爲此表建模:
一、在你的項目中建立一個繼承自DbContext的類Recipe3Context;
二、使用代碼清單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 }
三、在上下文對象Recipe3Context中添加一個類型爲DbSet<Product>的屬性;
四、在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/