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

How to represent a relationship between a parent and multiple child properties of the same type?

考虑以下两个实体*:

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)
}

如代码所示,parent 需要有一个主要的 child 和一个次要的 child;并且(最好)当 parent 被删除时,两个 children 也应该被删除。

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

如果 parent 只有一个 child,我可以通过转动 [=15] 的主键轻松使用 one-to-one 关系(1 to 0..1) =] 分为 PK 和 FK。显然,这不适用于当前情况,因为一方面,一个主键不能有多个值。

感觉这种情况很常见,应该有一个 well-known 解决方案。然而,翻了无数帖子,还是没有找到。我能找到的最接近的东西是 但它不起作用,因为:

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

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

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

    ...它抛出 DbUpdateException 消息:

    Unable to determine a valid ordering for dependent operations. Dependencies may exist due to foreign key constraints, model requirements, or store-generated values.

如何解决这个问题?


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

此代码已在 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; }

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

而不是 one-to-one 关系,我创建了一个很好的旧 one-to-many 关系(Parent 有很多 Child)在 属性 =14=] class 存储类型 (primary/secondary),并在 Parent class 中创建未映射的属性以替换原始导航属性。

下面是问题示例的代码:

Child class:

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 class:

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 });

显然,这不会强制执行 parent 可以拥有的 children 的最小或最大数量,或者它们必须是 ChildType 的数量。不过这是我能想到的最好的了。

创建此类关系的方法有两种:

  1. Parent有两个外键指向各自的children,接近你所做的。
  2. Children 有指向 parent 的外键,这将是一个简单的一对多。

第一个选项很容易将一个parent限制为两个child人,并且很容易区分主次child。

第二个选项,如果需要区分主从,则需要在child objects中添加类型。使用 parent 的 ID 和类型作为唯一索引将允许您将 children 限制为单个主 child 每个 parent.

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

我倾向于 children 具有外键和类型。所以沿着这些方向:

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; }
}

对实体进行一些小修改并使用流畅的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; }

试试这个方法,

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);
      }
       
    }
  }
}

子 class 应包含父 class 的外键,在本例中,我为一对一关系使用了复合键。

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);
}

您可以根据需要扩展它。