微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

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

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

考虑以下两个实体*

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来表示这种关系吗?

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

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

  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:

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

如何解决这个问题?


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

解决方法

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

我使用 Parent 类中的一个属性创建了一个很好的旧一对多关系(Child 有很多 Child),而不是一对一的关系存储类型(主要/次要),并在 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。不过,这是我能想到的最好的办法。

,

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

  1. Parent 有两个外键指向各自的孩子,与您所做的很接近。
  2. 孩子有指向父母的外键,这将是一个简单的一对多。

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

第二个选项,如果您需要区分主要和次要对象,则需要向子对象添加类型。使用父项的 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; }
}
,

试试这个方法,

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

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

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

您可以根据需要扩展它。

,

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

var c1 = new Child { Name = "C1" };
var c2 = new Child { Name = "C2" };
_context.Parents.Add(new Parent { Name = "P1",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",IsUnique = true)]
public int? PrimaryChildId { get; set; }
      
[Index("IX_Parents_SecondaryChildId",IsUnique = true)]
public int? SecondaryChildId { get; set; }
,

对实体做一些小的修改,并使用 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; }

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。