如何表示同一类型的父属性和多个子属性之间的关系?

Posted

技术标签:

【中文标题】如何表示同一类型的父属性和多个子属性之间的关系?【英文标题】:How to represent a relationship between a parent and multiple child properties of the same type? 【发布时间】:2021-09-25 21:11:36 【问题描述】:

考虑以下两个实体*

public class Parent

    public int Id  get; set; 
    public string Name  get; set; 

    public virtual Child PrimaryChild  get; set; 
    public virtual Child SecondaryChild  get; set; 


public class Child

    public int Id  get; set; 
    public string Name  get; set; 

    // Foreign keys to be added here or
    // in the other class (that's what I'm trying to figure out)

如代码所示,父母需要有一个主要孩子和一个次要孩子;并且(最好)当父级被删除时,两个子级也应该被删除。

甚至可以用EF表示这种关系吗?

如果父母只有一个孩子,我可以通过将Child 的主键转换为 PK 和 FK 来轻松使用一对一关系 (1 to 0..1)。显然,这不适用于当前的情况,因为一个主键不能有多个值。

感觉这是一种足够普遍的情况,它应该有一个众所周知的解决方案。但是,查了无数个帖子,还是没有找到。我能找到的最接近的东西是this answer,但它不起作用,因为:

    它创建* to 0..1 关系。

    当我忽略这个事实并尝试执行以下代码时:

    var c1 = new Child  Name = "C1" ;
    var c2 = new Child  Name = "C2" ;
    context.Parents.Add(new Parent  Name = "P1", PrimaryChild = c1, SecondaryChild = c2 );
    

    ...它抛出了一个带有消息的 DbUpdateException:

    无法确定相关操作的有效顺序。由于外键约束、模型要求或存储生成的值,可能存在依赖关系。

如何解决这个问题?


*这是一个简化的例子。在现实生活场景中,父实体和其他子实体之间存在关系,每个子实体有两个或多个引用。

【问题讨论】:

我想知道您真正需要这种关系的商业案例是什么。我肯定会选择关系 1 to Many,因为它提供了进行不同收集操作的机会。如果你真的想将孩子的数量限制为 2,我会添加约束或一些验证逻辑 @RedgoodBreaker 谢谢。是的,我认为你是对的。我想得越多,我就越相信一对多(与一群孩子)是要走的路。 【参考方案1】:

此代码已在 Visual Studio 中测试并正常运行

var c1 = new Child  Name = "C1" ;
var c2 = new Child  Name = "C2" ;
_context.Parents.Add(new Parent  Name = "P1", PrimaryChild = c1, SecondaryChild = c2 );

var parents = _context.Parents
               .Include(i => i.PrimaryChild)
               .Include(i => i.SecondaryChild)
               .ToList();

要尝试它,你必须向你的类添加关系

public class Parent

    [Key]
    public int Id  get; set; 
    public string Name  get; set; 

    public int? PrimaryChildId  get; set; 
    public int? SecondaryChildId  get; set; 

    [ForeignKey(nameof(PrimaryChildId))]
    [InverseProperty("PrimaryChildParents")]
    public virtual Child PrimaryChild  get; set; 

    [ForeignKey(nameof(SecondaryChildId))]
    [InverseProperty("SecondaryChildParents")]
    public virtual Child SecondaryChild  get; set; 


public class Child

    [Key]
    public int Id  get; set; 
    public string Name  get; set; 

    [InverseProperty(nameof(Parent.PrimaryChild))]
    public virtual ICollection<Parent> PrimaryChildParents  get; set; 

    [InverseProperty(nameof(Parent.SecondaryChild))]
    public virtual ICollection<Parent> SecondaryChildParents  get; set; 

如果您想进行一对一,并且因为您使用的是 EF6 您可以为外键添加一些约束

[Index("IX_Parents_PrimaryChildId", 1, IsUnique = true)]
public int? PrimaryChildId  get; set; 
      
[Index("IX_Parents_SecondaryChildId", 2, IsUnique = true)]
public int? SecondaryChildId  get; set; 

【讨论】:

"或者如果你有一对一的最后两行可以" 那行不通。更新数据库时,它会抛出异常:Introducing FOREIGN KEY constraint 'FK_dbo.Parents_dbo.Children_SecondaryChildId' on table 'Parents' may cause cycles or multiple cascade paths。此外,c1c2 正在同时创建。要使用.Id,我必须先创建孩子,保存更改并获取 ID。另一方面,使用集合允许将一个孩子链接到多个父母。 @ICloneable 我不知道网络使用的是什么版本。它不适用于旧的网络版本 比什么更老?如标签所示,我正在使用 .NET 4.7.2 和 EF6.x。这是 EF Core 的答案吗? @ICloneable 这是一个普遍的答案。您只能对核心使用一对一,其他版本使用多对多 首先,我没有在没有警告的情况下更新问题。它从一开始就被标记为 EF6。我只是明确表示孩子们也被添加了(我认为这很明显)并且我在评论中向你提到了这一点。现在,如果您没有注意标签并为 EF Core 提供了答案,那我的错怎么办?如果你的票数接近,你能告诉我我的问题到底有什么不清楚的地方吗?无论如何,感谢您提供帮助。【参考方案2】:

我想出了一个老套的解决方案,但我仍然希望有一个更好的解决方案。

我创建了一个很好的旧的一对多关系,而不是一对一关系(Parent 有很多 Child),并在 Child 类中使用一个属性来存储类型(primary/ secondary),并在Parent 类中创建了未映射的属性来替换原来的导航属性。

问题示例的代码如下所示:

Child类:

public enum ChildType  Primary, Secondary 

public class Child

    public int Id  get; set; 
    public string Name  get; set; 

    public ChildType ChildType  get; set; 

    [ForeignKey("Parent")]
    public int ParentId  get; set; 
    public virtual Parent Parent  get; set; 

Parent类:

public class Parent

    public Parent()
    
        Children = new HashSet<Child>();
    
    public int Id  get; set; 
    public string Name  get; set; 

    public virtual ICollection<Child> Children  get; set; 

    [NotMapped]
    public Child PrimaryChild
    
        get
        
          return Children.SingleOrDefault(c => c.ChildType == ChildType.Primary);
        
        set
        
         var existingChild = 
                Children.SingleOrDefault(c => c.ChildType == ChildType.Primary);
            if (existingChild != null) Children.Remove(existingChild);

            Children.Add(value);
        
    

    [NotMapped]
    public Child SecondaryChild
    
        get
        
            return Children.SingleOrDefault(c => c.ChildType == ChildType.Secondary);
        
        set
        
            var existingChild = 
                Children.SingleOrDefault(c => c.ChildType == ChildType.Secondary);
            if (existingChild != null) Children.Remove(existingChild);

            Children.Add(value);
        
    

用法:

var c1 = new Child  Name = "C1", ChildType = ChildType.Primary ;
var c2 = new Child  Name = "C2", ChildType = ChildType.Secondary ;
context.Parents.Add(new Parent  Name = "P1", PrimaryChild = c1, SecondaryChild = c2 );

显然,这并不强制规定父母可以拥有的孩子的最小或最大数量或 ChildType 他们必须是什么。这是我能想到的最好的了。

【讨论】:

我认为这是你能做的最好的(不知道为什么它被否决了)。即使在普通 SQL 中,在两个表之间实现两个排他的 1:1 关系也很丑陋,而 EF6 中的 1:1 实现没有提供任何句柄来对它们进行建模。 @GertArnold 谢谢。是的,我认为它可能不会比这更好。只是想确保我没有遗漏一些明显的东西;因此,赏金。 “不知道为什么它被否决了” 我也不是 :) 一个被否决的人(可能)来自另一个回答者,因为他无缘无故地生气了。我真的不在乎选票;只是对这个问题的反对意见减少了被查看的机会,这也是我开始赏金的另一个原因。再次感谢您的保证。【参考方案3】:

有两种方法可以创建这种关系:

    Parent 对各自的孩子有两个外键,与您所做的接近。 子项对父项具有外键,这将是一个简单的一对多。

第一个选项很容易将父母限制为两个孩子,并且很容易区分主要和次要孩子。

第二个选项,如果您需要区分主要和次要对象,则需要为子对象添加一个类型。使用父母的 Id 和类型作为唯一索引将允许您将孩子限制为每个父母一个主要和一个次要孩子。

现在就删除父级而言,如果您对关系进行级联删除,它将自动发生。我知道在第二个选项中,简单的一对多,级联删除将按预期工作。第一个选项,基本上是两个一对一的关系,您可以设置级联删除,但我怀疑您需要确保它是单向级联。例如如果父项被删除,则子项也会被删除,但如果子项被删除,则父项应该保留。

我会倾向于拥有外键和类型的孩子。所以沿着这些思路:

public class Parent

    public int Id  get; set; 
    public string Name  get; set; 

    public virtual ICollection<Child> Children  get; set; 


public class Child

    public int Id  get; set; 
    public string Name  get; set; 

    [Index("IX_Child_Parent_Type", 1, IsUnique = true)]
    public int ParentId  get; set; 
    public virtual Parent Parent  get; set; 
    
    [Index("IX_Child_Parent_Type", 2, IsUnique = true)]
    public int ChildTypeId  get; set; 
    public virtual ChildType Type  get; set; 


public class ChildType

    public int Id  get; set; 
    public string Name  get; set; 

另一个选项如下所示:

public class Parent

    public int Id  get; set; 
    public string Name  get; set; 

    public virtual Child PrimaryChild  get; set; 
    public virtual Child SecondaryChild  get; set; 


public class Child

    public int Id  get; set; 
    public string Name  get; set; 

【讨论】:

唯一索引是对我当前解决方案的一个很好的补充。如果您愿意分享第一个建议的工作原理,我将不胜感激。这就是我一开始试图做的,但我无法让它发挥作用。我不确定在这种情况下会是什么类型的关系。 ChildType 和 Parent 不能是表的列,所以不能使用索引。 已添加其他选项。 来自另一个人的回答? @LarryDukek 第二个选项不能按原样工作。它基本上与问题中的代码完全相同。不过,我赞成唯一索引的想法。感谢您的帮助:)【参考方案4】:

对实体进行一些小修改,并使用 fluent api 指定一对多关系

public class Parent

    public int Id  get; set; 
    public string Name  get; set; 

    public virtual IEnumerable<Child> Children  get; set; 


public class Child

    public int Id  get; set; 
    public string Name  get; set; 
    public bool IsPrimary get; set;

protected override void OnModelCreating(ModelBuilder modelBuilder)
    
       modelBuilder.Entity<Parent>()
            .HasKey(p => p.Id)
            .WithMany(p => p.Child)
            .HasForeignKey(s => s.ParentId);
    
    
    public DbSet<Child> Children get; set; 

【讨论】:

这或多或少与我在my answer 中所做的相同(父母有一个孩子的集合,而孩子有一个属性来指示它是主要的还是次要的)。仅供参考,我并没有否决你的答案。【参考方案5】:

试试这个方法,

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleApp2

  // this class will represent a person, a parent or a child since they have the same attributes
  class Person
  
    protected int _id;
    protected string _name; 
    public int Id  get => _id; 
    public string Name  get => _name; 
    // Any object wether it's a parent or a child must be instantiated with those properties
    public Person(int id, string name)
    
      this._id = id;
      this._name = name;
    
  

  // this represents a parent which is a persone but have two persons reprenseting it's children. the children can't be instanciated alone.
  class Parent : Person
  
    private Person _primaryChild;
    private Person _secondaryChild;
    public Person PrimaryChild  get => _primaryChild; 
    public Person SecondaryChild  get => _secondaryChild; 

    // this creates the parent with it's children. one the parent dispose its children will be deleted.
    public Parent(Person parent, Person primaryChild, Person secondaryChild) : base( parent.Id,  parent.Name)
    
      // this primaryChild enforce that a parent must have two different children to represents a primary and a secondry child.
      if(primaryChild.Id != secondaryChild.Id)
      
        this._id = parent.Id;
        this._name = parent.Name;
        this._primaryChild = primaryChild;
        this._secondaryChild = secondaryChild;
      
      else
      
        throw new Exception("Children must not be tweens.");
      
    
  

  class Program
  
    static void Main(string[] args)
    
      // instanciating a person does'nt represent an actor in the process. A parent must be instianciated.
      try
      
        // creating a new parent with its related children
        var parent = new Parent(new Person(1, "Parent"), new Person(1, "ChildOne"), new Person(2, "ChildTwo"));
        // diplaying children names
        Console.WriteLine($"Primary Child is parent.PrimaryChild.Name. Secondary Child is parent.SecondaryChild.Name.");
      
      catch (Exception e)
      

        Console.WriteLine(e.Message);
      
       
    
  

【讨论】:

感谢您的回答,但就我而言,父母和孩子不能从同一个类派生(它们完全不同)。【参考方案6】:

子类应该包含父类的外键,在本例中,我使用复合键来表示一对一关系。

public class Child

    [Key]
    [Column(Order = 1)]
    public int Id  get; set; 
    public string Name  get; set; 

    [Key, ForeignKey("Parent")]
    [Column(Order = 2)]
    public int ParentId  get; set; 

然后在您的上下文的 OnModelCreating 中,您需要设置约束。

protected override void OnModelCreating(DbModelBuilder modelBuilder) 

        modelBuilder.Entity<Parent>()
                .HasRequired(e => e.PrimaryChild)
                .WithRequiredPrincipal(e => e.Parent);

        modelBuilder.Entity<Parent>()
                .HasRequired(e => e.SecondaryChild)
                .WithRequiredPrincipal(e => e.Parent);

         modelBuilder.Entity<Child>()
            .HasKey(e => e.ParentId);

您可以根据需要扩展它。

【讨论】:

这基本上是我一开始就尝试过的,但它不起作用。你试过了吗?它失败并显示“指定的架构无效”消息。 是的,我已经尝试过类似的方法并且可以正常工作。也许您需要让子类中的 Id 来自父类中的 Id,然后将 ChildId 作为复合键以使其唯一。

以上是关于如何表示同一类型的父属性和多个子属性之间的关系?的主要内容,如果未能解决你的问题,请参考以下文章

Laravel/Eloquent - 创建与可能具有或不具有属性的父模型相关的多个子模型的关系

同一类型的多个导航属性的关系配置?

IfcRelDefines

关系型数据库-关系模型R(U,D,dom,F)解析

java的多态

获取多个实体之间没有关系