Entity Framework 核心在一对多关系中添加具有所需导航 属性 的新实体?
Entity Framework Core adding new Entity with required navigation property in one-to-many relationship?
上下文
我正在从事一个有 Brand
和 OemModel
的项目。它们之间是一对多的关系。一个 Brand
可以有任意数量的 OemModel
s,但是一个 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)
我还尝试在模型中明确定义 Brand
和 BrandId
属性(我认为这是有道理的,因为它们本质上是 [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 并保存更改,模型应该自动生成 Id
s,不是吗?
我对将 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]
导航属性的一对多关系的正确方法是什么(或者为什么会这样)。
但我会把它留作答案,以防有人遇到同样的问题。
上下文
我正在从事一个有 Brand
和 OemModel
的项目。它们之间是一对多的关系。一个 Brand
可以有任意数量的 OemModel
s,但是一个 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, butstring
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)
我还尝试在模型中明确定义 Brand
和 BrandId
属性(我认为这是有道理的,因为它们本质上是 [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 并保存更改,模型应该自动生成 Id
s,不是吗?
我对将 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]
导航属性的一对多关系的正确方法是什么(或者为什么会这样)。
但我会把它留作答案,以防有人遇到同样的问题。