实体框架 - 添加具有相关实体的实体时出错

时间:2021-01-29 05:36:48

标签: entity-framework .net-core entity-framework-core asp.net-core-3.1

我有两个实体:

public class EntityA
{
    public int? Id { get; set; }
    public string Name { get; set; }
    public EntityB { get; set; }
}

public class EntityB
{
    public int? Id { get; set; }
    public string Version { get; set; }
}

我在数据库中已有 EntityB 的现有记录。我想参考 EntityA 记录之一添加新的 EntityB

var entityB = _dbContext.EntityB.FirstOrDefault(e => e.Id == 1);

var entityA = new EntityA { Name = "Test", EntityB = entityB };

_dbContext.Add(entityA);
_dbContext.SaveChanges();

当上面的代码运行时,我收到以下错误:

<块引用>

System.InvalidOperationException:实体类型“EntityB”上的属性“Id”是键的一部分,因此无法修改或标记为已修改。要使用标识外键更改现有实体的主体,首先删除依赖项并调用“SaveChanges”,然后将依赖项与新主体相关联。

在我看来,保存还试图添加 EntityB,而不仅仅是对它的引用。我确实在数据库和实体框架中指定了关系,例如在查询 EntityA 时,如果我在选择中包含 EntityB,我也会得到引用的实体(因此关系有效)。

modelBuilder.Entity<EntityA>(e =>
{
  e.HasKey(p => p.Id);
  e.HasOne(p => p.EntityB)
    .WithOne()
    .HasForeignKey<EntityB>(p => p.Id);
}

modelBuilder.Entity<EntityB>(e =>
{
  e.HasKey(p => p.Id);
}

如何保存新的 EntityA,只引用选定的 EntityB,而不是同时保存两个实体?

2 个答案:

答案 0 :(得分:1)

这会失败,因为您使用实体 A 的 PK 作为实体 B 的 FK,这会强制执行 1 对 1 的直接关系。这方面的一个示例是拥有类似 Order 和 OrderDetails 的内容,其中包含有关特定订单的其他详细信息。两者都将使用“OrderId”作为它们的 PK,而 OrderDetails 使用它的 PK 来关联回其订单。

如果相反,EntityB 更像是 OrderType 引用,则您不会使用 HasOne / WithOne 关系,因为这将要求 Order #1 仅与 OrderType #1 相关联。如果您尝试将 OrderType #2 链接到 Order #1,EF 将尝试替换 OrderType 上的 PK,这是非法的。

通常,EntityA 和 EntityB 之间的关系需要 EntityA 表上的 EntityBId 列作为 FK。这可以是 EntityA 实体中的一个属性,也可以保留为 Shadow 属性(推荐在 EntityA 将具有 EntityB 导航属性的情况下)使用上面带有 Order 和 OrderType 的示例,Order 记录将具有 OrderId (PK) 和 OrderTypeId ( FK) 到它关联的订单类型。

此映射将是:(阴影属性)

modelBuilder.Entity<EntityA>(e =>
{
  e.HasKey(p => p.Id);
  e.HasOne(p => p.EntityB)
    .WithMany()
    .HasForeignKey("EntityBId");
}

一个 OrderType 可以分配给多个 Orders,但我们没有 OrderType 上的 Orders 集合。我们使用 .HasForeignKey("EntityBId") 在 EntityA 表上设置“EntityBId”的影子属性。或者,如果我们在 EntityA 上声明 EntityBId 属性:

modelBuilder.Entity<EntityA>(e =>
{
  e.HasKey(p => p.Id);
  e.HasOne(p => p.EntityB)
    .WithMany()
    .HasForeignKey(p => p.EntityBId);
}

附带说明,导航属性应声明为 virtual。即使您不想依赖延迟加载(推荐),它也有助于确保完全支持用于更改跟踪的 EF 代理,并且延迟加载通常是运行时比抛出 NullReferenceExceptions 更好的条件。

答案 1 :(得分:1)

看起来您正在尝试使用对新表 EntityB 中的第 n 行的可选 1:1 引用来扩展 EntityA。您希望两条记录的 Id 值相同。

<块引用>

此 1:1 链接有时称为 Table Splitting
从逻辑上讲,在您的应用程序中,来自 EntityBEntityA 的记录表示相同的业务域对象。

如果您只是想创建一个常规的 1 : many 关系,那么您应该删除 HasOne().WithOne() 因为这会创建一个 1:1,您将也不要试图让 FK 回到 Id 属性。

以下建议仅适用于配置 1:1 关系

<块引用>

出于性能原因(通常是中间层性能)或安全原因,您可能会使用表拆分。但是,当我们需要使用新的元数据扩展旧架构并且存在我们无法控制的代码时,如果我们只是将额外的字段添加到现有表中,这些代码就会出现问题。

您对此的设置基本上是正确的,除了 EntityA.Id 不能为空,因为它必须有一个值的主键。

public class EntityA
{
    public int Id { get; set; }
    public string Name { get; set; }
    public EntityB { get; set; }
}
<块引用>

如果您希望在 EntityA 中存在的记录 DO NOTEntityB 中有相应的记录,那么您需要使用另一个 Id 列作为 主要键 EntityA 外键 EntityB

然后您需要通过禁用自动生成的行为来缩小与 EntityA.Id 字段的差距,以便它采用 Id 中的 EntityB 值:

modelBuilder.Entity<EntityA>(e =>
{
  e.HasKey(p => p.Id).ValueGeneratedNever();
  e.HasOne(p => p.EntityB)
    .WithOne()
    .HasForeignKey<EntityB>(p => p.Id);
}

我可能会更进一步,将 Reciprocating 或 Inverse 导航属性添加到 EntityB 这将允许我们使用更fluent 样式分配,而不是使用 _dbContext.Add()将记录添加到数据库:

public class EntityB
{
    public int Id { get; set; }
    public string Version { get; set; }
    public virtual EntityA { get; set; }
}

使用配置:

modelBuilder.Entity<EntityA>(e =>
{
    e.HasKey(p => p.Id).ValueGeneratedNever();
    e.HasOne(p => p.EntityB)
     .WithOne(p => p.EntityA)
     .HasForeignKey<EntityB>(p => p.Id);
}

这允许您添加更流畅的样式:

var entityB = _dbContext.EntityB.FirstOrDefault(e => e.Id == 1);

entityB.EntityA = new EntityA { Name = "Test" };

_dbContext.SaveChanges();
相关问题