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/

  • 相关阅读:
    P1144 最短路计数 题解 最短路应用题
    C++高精度加减乘除模板
    HDU3746 Teacher YYF 题解 KMP算法
    POJ3080 Blue Jeans 题解 KMP算法
    POJ2185 Milking Grid 题解 KMP算法
    POJ2752 Seek the Name, Seek the Fame 题解 KMP算法
    POJ2406 Power Strings 题解 KMP算法
    HDU2087 剪花布条 题解 KMP算法
    eclipse创建maven项目(详细)
    maven的作用及优势
  • 原文地址:https://www.cnblogs.com/VolcanoCloud/p/4527549.html
Copyright © 2011-2022 走看看