翻译的初衷以及为何选择《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/