Entity Framework 核心在一对多关系中添加具有所需导航 属性 的新实体?

Entity Framework Core adding new Entity with required navigation property in one-to-many relationship?

上下文

我正在从事一个有 BrandOemModel 的项目。它们之间是一对多的关系。一个 Brand 可以有任意数量的 OemModels,但是一个 OemModel 只能有 1 个 b运行d。出于商业原因,OemModel 需要知道他们 Brand 属于什么。

该项目是使用 Entity Framework Core 5.0 和 Npgsql 5.0.1 包构建的,用于连接到 PostgreSQL 数据库。我正在使用代码优先的方法来生成数据库。

该项目还使用了可选的 可空引用类型 功能。这意味着根据 MSDN documentation:

,任何指向引用类型的导航 属性 本质上都是 [Required]

If nullable reference types are enabled, properties will be configured based on the C# nullability of their .NET type: string? will be configured as optional, but string will be configured as required.

这是我的数据库上下文 class:

public class AppDbContext : DbContext
{
    public AppDbContext(DbContextOptions<AppDbContext> options)
        : base(options) 
    {
    }

    public DbSet<Brand> Brands => Set<Brand>();
    public DbSet<OemModel> OemModels => Set<OemModel>();
}

这是我的模型 classes:

public class Brand
{
    public int Id { get; set; }
    public string Name { get; set; } = string.Empty;
    public List<OemModel> OemModels { get; set; } = new();
}
public class OemModel
{
    public int Id { get; set; }
    public string Name { get; set; } = string.Empty;
    public string OemNumber { get; set; } = string.Empty;

    public int BrandId { get; set; }
    public Brand Brand { get; set; } = null!;
}

我正在使用“空!” null 宽容运算符 根据 MSDN documentation:

As a terser alternative, it is possible to simply initialize the property to null with the help of the null-forgiving operator (!):

public Product Product { get; set; } = null!;

An actual null value will never be observed except as a result of a programming bug, e.g. accessing the navigation property without properly loading the related entity beforehand.

我的问题

我目前在集成测试中向现有 b运行ds 添加模型时遇到问题。

假设以下代码已经运行成功:

var firstBrand = new Brand{ Name = "FirstBrand" };
var secondBrand = new Brand{ Name = "SecondBrand" };

_context.Brands.Add(firstBrand);
_context.Brands.Add(secondBrand);

await _context.SaveChangesAsync();

我无法将模型添加到这些新创建的 b运行ds:

var firstModel = new Model
{
    Name = "FirstModel",
    OemNumber = "A-1"
};
var secondModel = new Model
{
    Name = "SecondModel",
    OemNumber = "A-2"
};
var thirdModel = new Model
{
    Name = "ThirdModel",
    OemNumber = "B-1"
};

firstBrand.OemModels.Add(firstModel);
firstBrand.OemModels.Add(secondModel);
secondBrand.OemModels.Add(thirdModel);

await _context.SaveChangesAsync();

注意:以上两个代码块在同一个方法中,中间没有任何代码。

上面的代码块失败并显示以下错误消息:

Microsoft.EntityFrameworkCore.DbUpdateException: An error occurred while updating the entries. See the inner exception for details.

内部异常:

Npgsql.PostgresException
23505: duplicate key value violates unique constraint "PK_Brands"
   at Npgsql.NpgsqlConnector.<ReadMessage>g__ReadMessageLong|194_0(NpgsqlConnector connector, Boolean async, DataRowLoadingMode dataRowLoadingMode, Boolean readingNotifications, Boolean isReadingPrependedMessage)

我还尝试在模型中明确定义 BrandBrandId 属性(我认为这是有道理的,因为它们本质上是 [Required] 在 NRT 设置之前) .例如:

var firstModel = new OemModel
{
    Name = "FirstModel",
    OemNumber = "A-1",
    Brand = firstBrand,
    BrandId = firstBrand.Id
};

但它仍然失败并出现同样的错误。

我觉得这很混乱,因为我的数据库迁移为我的模型生成了以下内容 tables:

migrationBuilder.CreateTable(
    name: "OemModels",
    columns: table => new
    {
        Id = table.Column<int>(type: "integer", nullable: false)
          .Annotation("Npgsql:ValueGenerationStrategy",NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
        Name = table.Column<string>(type: "text", nullable: false),
        OemNumber = table.Column<string>(type: "text", nullable: false),
        BrandId = table.Column<int>(type: "integer", nullable: false)
    },
    constraints: table =>
    {
        table.PrimaryKey("PK_OemModels", x => x.Id);
        table.ForeignKey(
            name: "FK_OemModels_Brands_BrandId",
            column: x => x.BrandId,
            principalTable: "Brands",
            principalColumn: "Id",
            onDelete: ReferentialAction.Cascade);
     });

并且:

migrationBuilder.CreateTable(
    name: "Brands",
    columns: table => new
    {
         Id = table.Column<int>(type: "integer", nullable: false)
                .Annotation("Npgsql:ValueGenerationStrategy",NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
         Name = table.Column<string>(type: "text", nullable: false)
     },
     constraints: table =>
     {
         table.PrimaryKey("PK_Brands", x => x.Id);
     });

OemModel table 没有“PK_Brands”,但是当我尝试保存添加 OemModel 时所做的更改时出现异常现有 b运行ds.

在调试器中,我的两个 Brand 在添加到上下文后都具有唯一的 Id(如我所料),因此它们在将 OemModel 添加到其内部列表的时间。我的理解是,如果我将模型添加到现有 b运行ds 并保存更改,模型应该自动生成 Ids,不是吗?

我对将 OemModel 添加到现有 Brand 的正确方法感到困惑,如果主体 Brand[Required] 属性。按照我的理解,null!; 赋值应该允许我创建 OemModel,而无需为其父 Brand 指定导航 属性 直接将其添加到 [=22] =] 的 OemModel 列表,但无论它们是否已定义,我都会收到“PK_Brand”错误。

编辑 2

我猜你可以说我有一个“脑放屁”。自从我使用 ASP .NET Core 和 EF Core 以来已经有一段时间了,并且完全忘记了它不会自动跟踪基于分配的更改。您必须显式调用 context.[EntityTypeHere].Update(EntityInstance) 以将其标记为已跟踪(以便下一次调用 SaveChanges/async() 有效。

而且,在后知后觉中“呃,编辑 1 当然会起作用”。它 在创建 模型的同时创建品牌。这不是更新现有的Brand实体!

简单的修复是:


firstBrand.OemModels.Add(firstModel);
firstBrand.OemModels.Add(secondModel);
secondBrand.OemModels.Add(thirdModel);
                
_context.Brands.UpdateRange(firstBrand, secondBrand); // <--- Track updates here.

await _context.SaveChangesAsync();

我使用 UpdateRange() 而不是 Update() 因为我有 2 个实体要更新。

那么,为什么会这样呢?因为 ASP .NET 使用“断开连接的实体”。

MSDN covers "Disconnected Entities"。具体来说:

However, sometimes entities are queried using one context instance and then saved using a different instance. This often happens in "disconnected" scenarios such as a web application where the entities are queried, sent to the client, modified, sent back to the server in a request, and then saved. In this case, the second context instance needs to know whether the entities are new (should be inserted) or existing (should be updated).

[-Emphasis mine]

编辑 1

这也有效,但我有点期待它,因为原始答案的代码块工作正常:

var firstModel = new OemModel {Name = "FirstModel", OemNumber = "1"};
var secondModel = new OemModel {Name = "SecondModel", OemNumber = "2"};
var thirdModel = new OemModel {Name = "ThirdModel", OemNumber = "3"};

var firstBrand = new Brand {
    Name = "FirstBrand",
    OemModels = new List<OemModel>{firstModel, secondModel}
};
var secondBrand = new Brand {
    Name = "SecondBrand",
    OemModels = new List<OemModel>{thirdModel}
};

_context.Brands.Add(firstBrand);
_context.Brands.Add(secondBrand);

await _context.SaveChangesAsync();

它比第一个简单一点(特别是如果我添加更多模型用于测试目的)。

虽然,现在我更迷茫了,我原来做错了什么……

原回答

出于某种原因,这似乎工作得很好:

var firstModel = new OemModel {Name = "FirstModel", OemNumber = "1"};
var secondModel = new OemModel {Name = "SecondModel", OemNumber = "2"};
var thirdModel = new OemModel {Name = "ThirdModel", OemNumber = "3"};

var firstBrand = new Brand {Name = "FirstBrand"};
var secondBrand = new Brand {Name = "SecondBrand"};

_context.Brands.Add(firstBrand);
_context.Brands.Add(secondBrand);

firstBrand.OemModels.Add(firstModel);
firstBrand.OemModels.Add(secondModel);
secondBrand.OemModels.Add(thirdModel);

await _context.SaveChangesAsync();

我不想让这个成为公认的答案,但是,因为我不太确定为什么它与问题中的代码相比有效,但它没有'真正回答将实体添加到具有 [Required] 导航属性的一对多关系的正确方法是什么(或者为什么会这样)。

但我会把它留作答案,以防有人遇到同样的问题。