【问题标题】:Complex relationship between entities with Entity Framework实体与实体框架之间的复杂关系
【发布时间】:2019-06-29 05:26:50
【问题描述】:

也许它是重复的,但我找不到任何这样的主题。

我正在使用 Entity Framework,并且我的数据库中有两个表:

public class A
{
    public virtual B B1 { get; set; }
    public virtual B B2 { get; set; }
}

public class B
{
    public virtual A A1 { get; set; }
}

B1 和 A1 或 B2 和 A1 之间没有关系。这就像 3 单向关系。您如何在 Entity Framework 中做到这一点?

我收到此错误:

保存不为其关系公开外键属性的实体时发生错误

有人知道怎么处理吗?

提前致谢

【问题讨论】:

  • 所以在传统的 SQL 术语中(EF 用于)它是 B1 和 B2 之间的多对多关系,其中 A1 是连接表?因此,您将需要 B1、B2 和 A1 的密钥,其中应该包含这些密钥。
  • 每个导航属性都需要对应的一列或多列组成被引用表的PK。
  • 可能我不是很清楚,但是A1、B1和B2没有关系。如果我设置 B1,它不应该尝试在数据库中设置 A1,但我认为这就是 Entity Framework 试图做的。
  • 这也感觉像是一个 XY 问题。您正在对哪些数据进行建模以产生这组特定的关系?
  • 是的,所以 EF 需要双向导航属性。听起来您应该在 B 中拥有 3 个 A 属性(它引用的一个和引用它的另外两个),然后在 A 中拥有 3 个 B 属性(它引用的两个和引用的另一个它)。

标签: c# entity-framework


【解决方案1】:

由于您没有指明您使用的是哪个 EF 版本,让我们看看当前的两个版本,EF 6.2.0 和 EF-core 2.2.4。

使用 EF6,这很容易。映射...

modelBuilder.Entity<A>().HasRequired(a => a.B1).WithMany();
modelBuilder.Entity<A>().HasRequired(a => a.B2).WithMany().WillCascadeOnDelete(false);
modelBuilder.Entity<B>().HasRequired(b => b.A1).WithMany().WillCascadeOnDelete(false);

...生成以下数据库模型(忽略索引):

CREATE TABLE [dbo].[A] (
    [ID] [int] NOT NULL IDENTITY,
    [B1_ID] [int] NOT NULL,
    [B2_ID] [int] NOT NULL,
    CONSTRAINT [PK_dbo.A] PRIMARY KEY ([ID])
)
CREATE TABLE [dbo].[B] (
    [ID] [int] NOT NULL IDENTITY,
    [A1_ID] [int] NOT NULL,
    CONSTRAINT [PK_dbo.B] PRIMARY KEY ([ID])
)

....其中_字段为外键,其中一个可以级联删除。

使用 ef-core,看起来并不那么简单,甚至是错误的。第一个脉冲是等效的 EF6:

modelBuilder.Entity<A>().HasOne(a => a.B1).WithMany();
modelBuilder.Entity<A>().HasOne(a => a.B2).WithMany();
modelBuilder.Entity<B>().HasOne(b => b.A1).WithMany();

但是生成的模型并不是人们所期望的:

  CREATE TABLE [B] (
      [ID] int NOT NULL IDENTITY,
      [A1ID] int NULL,
      CONSTRAINT [PK_B] PRIMARY KEY ([ID])
  );
  CREATE TABLE [A] (
      [ID] int NOT NULL,
      [B1ID] int NULL,
      CONSTRAINT [PK_A] PRIMARY KEY ([ID]),
      CONSTRAINT [FK_A_B_B1ID] FOREIGN KEY ([B1ID]) REFERENCES [B] ([ID]) ON DELETE NO ACTION,
      CONSTRAINT [FK_A_B_ID] FOREIGN KEY ([ID]) REFERENCES [B] ([ID]) ON DELETE CASCADE
  );
  ALTER TABLE [B] ADD CONSTRAINT [FK_B_A_A1ID] FOREIGN KEY ([A1ID]) REFERENCES [A] ([ID]) ON DELETE NO ACTION;

A-B 关联之一被解释为 1:1。在我看来,这是一个错误。 WithMany 指令不应留下任何解释空间。两个看似相同的映射会产生完全不同的数据库关系。这不可能。

也就是说,通过命名 FK 列很容易(但不是必需的)使 EF 走上正轨:

modelBuilder.Entity<A>().HasOne(a => a.B1).WithMany().HasForeignKey("B1_ID")
    .IsRequired();
modelBuilder.Entity<A>().HasOne(a => a.B2).WithMany().HasForeignKey("B2_ID")
    .IsRequired().OnDelete(DeleteBehavior.Restrict);
modelBuilder.Entity<B>().HasOne(b => b.A1).WithMany().HasForeignKey("A1_ID")
    .IsRequired().OnDelete(DeleteBehavior.Restrict);

生产(忽略索引):

  CREATE TABLE [B] (
      [ID] int NOT NULL IDENTITY,
      [A1_ID] int NOT NULL,
      CONSTRAINT [PK_B] PRIMARY KEY ([ID])
  );
  CREATE TABLE [A] (
      [ID] int NOT NULL IDENTITY,
      [B1_ID] int NOT NULL,
      [B2_ID] int NOT NULL,
      CONSTRAINT [PK_A] PRIMARY KEY ([ID]),
      CONSTRAINT [FK_A_B_B1_ID] FOREIGN KEY ([B1_ID]) REFERENCES [B] ([ID]) ON DELETE CASCADE,
      CONSTRAINT [FK_A_B_B2_ID] FOREIGN KEY ([B2_ID]) REFERENCES [B] ([ID]) ON DELETE NO ACTION
  );
  ALTER TABLE [B] ADD CONSTRAINT [FK_B_A_A1_ID] FOREIGN KEY ([A1_ID]) REFERENCES [A] ([ID]) ON DELETE NO ACTION;

请注意,必须明确设置外键字段(如果需要)。嗯,这只是一个实现细节。

【讨论】:

    【解决方案2】:

    如果 A1 表包含 B1_Id 和 B2_Id 指向同一个 B 表,但您希望 B 记录只与 A 关联一次,那么据我所知,映射是如何进行的,那不是可能的。没有什么可以阻止您将相同的 B 记录与 B1 或 B2 关联到不同的 A 记录上,那么 B 的 A 引用将如何合法地解决?实体反映数据状态,因此如果它在数据模式中是合法/非法的,那么实体也是如此。 A 上的 B Id 形成多对 1,或者可以形成 1 对一,但要与 A 上的 B 共享 2x FK,您需要 DB 支持 B 的备用 FK,但它不支持。

    您可以让 A 保存 B 中 2 条记录的 ID,但 B 不能将单个引用映射回 A,它必须伪造它。

    public class A
    {
       public int AId { get; set; }
    
       public virtual B B1 { get; set; }
       public virtual B B2 { get; set; }
    }
    
    public class B
    {
       public int BId { get; set; }
    
       public virtual ICollection<A> RawA1 { get; private set; } = new List<A>();
       public virtual ICollection<A> RawA2 { get; private set; } = new List<A>();
    
       [NotMapped]
       public A A
       {
          get { return RawA1.SingleOrDefault() ?? RawA2.SingleOrDefault(); }
       }
    }
    

    需要注意的是,您不能在任何转到 EF 的 Linq 表达式中使用 B.A,因为就 EF 而言,它不知道该属性。

    另一种方法是使用 AId 位于 B 上的 1 对多,然后在 A 端处理对集合的允许操作限制。存在的问题是,除非 B 记录上的属性可以明确确定,否则 B1 和 B2 订单将不可靠。 A 可以从集合中公开 B1 和 B2 未映射的属性,但必须确定要为每个元素考虑两个元素中的哪一个,或者只是公开集合。最终,您的业务逻辑必须控制一个事实,即 A 只能有 2 个 B 引用,因为数据库无法强制执行这一点,也不能控制 B 可以返回 A 的 2 对 1 关系。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-06-16
      • 1970-01-01
      • 2018-11-13
      • 1970-01-01
      相关资源
      最近更新 更多