现在,你应该对实体框架中基本的建模有了一定的了解,本章将帮助你解决许多常见的、复杂的建模问题,并解决你可能在现实中遇到的建模问题。
本章以多对多关系开始,这个类型的关系,无论是在现存系统还是新项目的建模中都非常普遍。接下来,我们会了解自引用关系,并探索获取嵌套对象图的各种策略。最后,本章以继承的高级建模和实体条件结束。
问题
你想获取链接表的键,链接表链接两个多对多关联的实体。
解决方案
假设你一个包含Event和Organizer实体和它们之前多对多关联的模型,如图6-1所示。
图6-1Event和Organizer实体和它们之前多对多关联的模型
正如我们在第二章演示的,多对多关联代表的是数据库中的一个中间表,这个中间表叫做链接表(译注:也称关联表,但使用这个词会容易与关系两边的表的描述(关联表)产生混淆,所以这里使用链接表一词)。链接表包含关系两边的外键(如图6-2)。当链接表中没有额外的列时,实体框架在导入关联表时,向导会在两个关联表间生成一个多对多的关联。链接表不被表示为一个实体,而是被表示成一个对多对多的关联。
图6-2数据库关联图,展示链接表EventOrganizer包含两个关联表Event和Oranizer的外键
为了获取实体键EventId,和OrganizerId,我们可以使用嵌套的from从句,或者SelectMany()方法。如代码清单6-1所示。
代码清单6-1.使用嵌套from从句和SelectMany()方法获取链接表
1using(varcontext=newRecipe1Context())2{3varorg=newOrganizer{Name="CommunityCharity"};4varevt=newEvent{Name="Fundraiser"};5org.Events.Add(evt);6context.Organizers.Add(org);7org=newOrganizer{Name="BoyScouts"};8evt=newEvent{Name="EagleScoutDinner"};9org.Events.Add(evt);10context.Organizers.Add(org);11context.SaveChanges();12}1314using(varcontext=newRecipe1Context())15{16varevsorg1=fromevincontext.Events17fromorganizerinev.Organizers18selectnew{ev.EventId,organizer.OrganizerId};19Console.WriteLine("Usingnestedfromclauses...");20foreach(varpairinevsorg1)21{22Console.WriteLine("EventId{0},OrganizerId{1}",23pair.EventId,24pair.OrganizerId);25}2627varevsorg2=context.Events28.SelectMany(e=>e.Organizers,29(ev,org)=>new{ev.EventId,org.OrganizerId});30Console.WriteLine("\nUsingSelectMany()");31foreach(varpairinevsorg2)32{33Console.WriteLine("EventId{0},OrganizerId{1}",34pair.EventId,pair.OrganizerId);35}36}代码清单6-1的输出如下:
Usingnestedfromclauses...EventId31,OrganizerId87EventId32,OrganizerId88UsingSelectMany()EventId31,OrganizerId87EventId32,OrganizerId88原理
在数据库中,链接表是表示两张表间多对多关系的通常做法。因为它除了定义两张表间的关系之外,就没有别的作用了,所以实体框架使用一个多对多关联来表示它,不是一个单独的实体。
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表的实体:
1、创建一个POCO实体类WorkerTak,如代码清单6-2所示;
2、使用类型为ICollection
3、使用类型为ICollection
4、在上下文对象DbContext的派生类中添加一个类型为DbSet
最终模型如代码清单6-2所示。
代码清单6-2.包含WorkerTask的最终数据模型
1[Table("Worker",Schema="Chapter6")]2publicclassWorker3{4[Key]5[DatabaseGenerated(DatabaseGeneratedOption.Identity)]6publicintWorkerId{get;set;}7publicstringName{get;set;}89[ForeignKey("WorkerId")]10publicvirtualICollection
在应用程序开发生命周期中,开发人员经常会在最开始的无载荷多对多关联上增加一个载荷。在本节中,我们演示了如何将一个多对多关联表示为一个单独的实体,以方便添加额外的标量属性。
很多开发人员认为,多对多关联最终都会包含载荷,于是他们为链接表创建了一个合成键(synthetickey),来代替传统的外键构成的组合键(compositekey)形式。
下面是我们的新模型,已经没有一个简单的方式来导航多对多关联。新模型中是两个一对多的关联,这需要增加一级,链接实体。代码清单6-3演示了插入和查询需要增加的额外工作。
代码清单6-13.插入和获取Task和Worker实体
1using(varcontext=newRecipe2Context())2{3context.Database.Log=content=>Debug.Print(content);4varworker=newWorker{Name="Jim"};5vartask=newTask{Title="FoldEnvelopes"};6varworkertask=newWorkerTask{Task=task,Worker=worker};7context.WorkerTasks.Add(workertask);8task=newTask{Title="MailLetters"};9workertask=newWorkerTask{Task=task,Worker=worker};10context.WorkerTasks.Add(workertask);11worker=newWorker{Name="Sara"};12task=newTask{Title="BuyEnvelopes"};13workertask=newWorkerTask{Task=task,Worker=worker};14context.WorkerTasks.Add(workertask);15context.SaveChanges();16}1718using(varcontext=newRecipe2Context())19{20Console.WriteLine("WorkersandTheirTasks");21Console.WriteLine("=======================");22foreach(varworkerincontext.Workers)23{24Console.WriteLine("\n{0}'stasks:",worker.Name);25foreach(varwtinworker.WorkerTasks)26{27Console.WriteLine("\t{0}",wt.Task.Title);28}29}30}代码清单6-3输出如下:
WorkersandTheirTasks=======================Jim'stasks:FoldEnvelopesMailLettersSara'stasks:BuyEnvelopes
你有一张自引用的多对多关系的表,你想为这张表及它的关系建模。
假设你的表拥有一个使用链接表的自引用有关系,如图6-4所示。
图6-4一个与自己多对多的关系表
按下面的步骤为此表建模:
1、在你的项目中创建一个继承自DbContext的类Recipe3Context;
2、使用代码清单6-4中的代码,在你的项目中添加一个POCO实体类Product;
代码清单6-4.创建一个POCO实体类Product
1[Table("Product",Schema="Chapter6")]2publicclassProduct3{4publicProduct()5{6RelatedProducts=newHashSet
4、在Recipe3Context中重写上下文对象DbContext的方法OnModelCreating,创建自引用的多对多关系映射,如代码清单6-5所示。
代码清单6-5.重写上下文对象DbContext的方法OnModelCreating,创建自引用的多对多关系映射
1protectedoverridevoidOnModelCreating(DbModelBuildermodelBuilder)2{3base.OnModelCreating(modelBuilder);45modelBuilder.Entity
正如你看到的那样,实体框架很容易就支持了一个自引用的多对多关联。我们在Product类中创建了两个导航属性,RelatedProducts和OtherRelatedProducts,并在DbContext的派生类中将其映射到底层的数据库中。
代码清单6-6,插入与获取一些关联的products。为了获取给定Product的所有关联Products,我们遍历了两导航属性RelatedProducts和OtherelatedProducts。
代码清单6-6.获取关联产品
using(varcontext=newRecipe3Context()){varproduct1=newProduct{Name="Pole",Price=12.97M};varproduct2=newProduct{Name="Tent",Price=199.95M};varproduct3=newProduct{Name="GroundCover",Price=29.95M};product2.RelatedProducts.Add(product3);product1.RelatedProducts.Add(product2);context.Products.Add(product1);context.SaveChanges();}using(varcontext=newRecipe3Context()){varproduct2=context.Products.First(p=>p.Name=="Tent");Console.WriteLine("Product:{0}...{1}",product2.Name,product2.Price.ToString("C"));Console.WriteLine("RelatedProducts");foreach(varprodinproduct2.RelatedProducts){Console.WriteLine("\t{0}...{1}",prod.Name,prod.Price.ToString("C"));}foreach(varprodinproduct2.OtherRelatedProducts){Console.WriteLine("\t{0}...{1}",prod.Name,prod.Price.ToString("C"));}}代码清单6-6的输出如下:
Product:Tent...$199.95RelatedProductsGroundCover...$29.95Pole...$12.97
在代码清单6-6中,只获取第一层的关联产品。传递关系(transitverelationship)是一个跨越了多层的关系,像一个层次结构。如果我们假设“关联产品(relatedproducts)"关系是可传递的,那么,我们可能需要使用传递闭包(transitiveclosure)形式(译注:这个概念有点绕,大家仔细理解。传递闭包、即在数学中,在集合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.关联产品关系的传递闭包
在代码清单6-7中,我们使用Load()方法(见第五章)来确保关联产品的集合被加载。不幸的是,这意味着,将会有更多的数据库交互。我们可能会想到,预先从Product表中加载出所有的行,并希望Relationshipspan(关联建立)能帮我们建立好关联。但是,Relationshipspan不会为实体集合建立关联(译注:导航属性为集合的情况),只会为实体引用建议关联。因为我们的关系是多对多(实体集合),所以,我们不能依靠relationshipspan来帮我们解决这个问题,只能依靠Load()方法。
代码清单6-7的输出如下。代码块的第一部分,插入关系,我们可以看到,Pole关联Ten,Ten关联GroundCover。Pole的关联产品的传递闭包包含,Ten,GroudCover,和Pole。Pole被包含的原因是,它在Pole与Ten的关系中的另一端。
ProductsrelatedtoPoleTentGroundCoverPole
这一篇讲了三个主题,内容有点多,第三个主题的内容又有点绕,翻译时也费了不少脑力。感谢你阅读。下篇再见!