zoukankan      html  css  js  c++  java
  • 《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("
    Using 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("
    {0}'s tasks:", worker.Name);
    25                     foreach (var wt in worker.WorkerTasks)
    26                     {
    27                         Console.WriteLine("	{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("	{0} ... {1}", prod.Name, prod.Price.ToString("C"));
                    }
                    foreach (var prod in product2.OtherRelatedProducts)
                    {
                        Console.WriteLine("	{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("	{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/

  • 相关阅读:
    AC日记——Little Elephant and Numbers codeforces 221b
    AC日记——Little Elephant and Function codeforces 221a
    AC日记——Mice and Holes codeforces 797f
    AC日记——Sliding Window poj 2823
    Poj 2976 Dropping tests(01分数规划 牛顿迭代)
    Bzoj 1968: [Ahoi2005]COMMON 约数研究
    洛谷 P2424 约数和
    Hdu Can you find it?(二分答案)
    SPOJ GSS1
    Bzoj 2243: [SDOI2011]染色(树链剖分+线段树)
  • 原文地址:https://www.cnblogs.com/VolcanoCloud/p/4527549.html
Copyright © 2011-2022 走看看