c# - EF Core - 添加和更新嵌套的多对多关系
问题描述
我环顾了(我认为是)与 EF Core 的多对多关系所涉及的每个问题,但找不到与我的问题相似的问题。
我的方法可能完全错误,因此请随时纠正我,因为我可能遗漏或不了解 EF Core 以及跟踪的工作原理。
另外,我使用 A DBContextFactory 是因为我使用 Blazor Server 作为前端,这就是我将 updatedInquiry 传递给 AddAndUpdateInquiryAsync 方法的原因。
这是设置:
类 - 我已经排除了不相关的属性
public class Inquiry
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
public ICollection<PartWithPrice> PartsWithPrices { get; set; }
}
public class PartWithPrice
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
/// <summary>
/// The part involved
/// </summary>
public Part Part { get; set; }
public int PartId { get; set; }
public ICollection<Inquiry> Inquiries { get; set; }
public ICollection<ValveInternal> ValveInternals { get; set; }
}
public class ValveInternal
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
public ICollection<PartWithPrice> PartWithPrices { get; set; }
}
如您所见,Inquiry 和 PartWithPrice 以及 PartWithPrice 和 ValveInternal 之间存在多对多的关系。
这是我目前从前端调用以添加或更新查询的服务中的方法(不漂亮,我知道),我遗漏了一些不处理多对多关系的代码,因为我感觉它与问题无关,但如果认为有必要会添加它。
public async Task<Inquiry> AddAndUpdateInquiryAsync(Inquiry updatedInquiry)
{
await using var ctx = Context.CreateDbContext();
var existingInquiry = await ctx.Inquiries
.Include(e => e.FileUploads)
.Include(e => e.Coating)
.Include(e => e.PartsWithPrices)
.ThenInclude(e => e.ValveInternals)
.Include(e => e.PartsWithPrices)
.ThenInclude(e => e.Part)
.ThenInclude(e => e.PartsCategory)
.Include(i => i.CompanyContact)
.ThenInclude(cc => cc.Emails)
.Include(i => i.CompanyContact)
.ThenInclude(cc => cc.PhoneNumbers)
.FirstOrDefaultAsync(i => i.Id == updatedInquiry.Id);
// Add it if it doesnt exist and return
if (existingInquiry == null)
{
// Becasue its a new context, Coatings, Parts, PartsCategories etc aren't being
// tracked so we have to track them manually like so. There is probably a better way
// to do this but I havent found one
updatedInquiry.CoatingId = ctx.Coatings.FirstOrDefault(x => x.Id == updatedInquiry.CoatingId).Id;
var partsWithPrices = updatedInquiry.PartsWithPrices;
foreach (var part in partsWithPrices)
{
part.Part = await ctx.Parts.FirstOrDefaultAsync(p => p.Id == part.Part.Id);
part.Part.PartsCategoryId = ctx.PartsCategories.FirstOrDefault(p => p.Id == part.Part.PartsCategoryId).Id;
}
ctx.Inquiries.Add(updatedInquiry);
await ctx.SaveChangesAsync();
return updatedInquiry;
}
ctx.Entry(existingInquiry).CurrentValues.SetValues(updatedInquiry);
// Handles Deletes
foreach (var part in existingInquiry.PartsWithPrices)
{
if (!updatedInquiry.PartsWithPrices.Any(x => x.Id == part.Id))
{
ctx.Remove(part);
}
}
foreach (var partWithPrice in updatedInquiry.PartsWithPrices)
{
var entity = existingInquiry.PartsWithPrices.FirstOrDefault(part => part.Id == partWithPrice.Id);
if (entity is null)
{
// If it doesn't exist in the table, add it
if (partWithPrice.ValveInternals is not null)
{
DealWithInternals(ctx, partWithPrice);
}
existingInquiry.PartsWithPrices.Add(partWithPrice);
}
else
{
// Update the record if it has changed
if (partWithPrice.ValveInternals is not null)
{
DealWithInternals(ctx, partWithPrice);
}
ctx.Entry(entity).CurrentValues.SetValues(partWithPrice);
}
}
ctx.SaveChanges();
return updatedInquiry;
}
private void DealWithInternals(DbContext ctx, PartWithPrice partWithPrice)
{
foreach (var valveInternal in partWithPrice.ValveInternals)
{
var valveInternalExists = partWithPrice.ValveInternals.FirstOrDefault(vi => vi.Id == valveInternal.Id);
if (valveInternalExists is null)
{
partWithPrice.ValveInternals.Add(valveInternal);
}
else
{
ctx.Entry(valveInternalExists).CurrentValues.SetValues(valveInternalExists);
}
}
}
现在,如果我只是将一个 PartWithPrice 添加到我的查询中,则此方法有效,桥接表已更新并且一切都很好。但是,当我尝试添加第二部分时,没有记录添加到 PartWithPrice 和 ValveInternal 之间的桥接表中,而是用于 Inquiry PartWithPrice 之间的桥接表。因此,仍然添加了 PartWithPrice,但没有添加 ValveInternals。
此外,我无法同时删除一个零件并添加一个新零件,我收到一个异常消息“无法跟踪实体类型 'Coating' 的实例,因为另一个实例的键值为 '{Id: 211}'已被跟踪。附加现有实体时,请确保仅附加一个具有给定键值的实体实例。”,所以显然我的做法是错误的。
这是添加单个 PartWithPrice 时上下文更改跟踪器中的调试视图,以防这有帮助:
Coating {Id: 211} Unchanged
CompanyContact {Id: 1} Unchanged FK {CompanyId: 1}
CompanyContactPhoneNumber {Id: 1} Unchanged FK {CompanyContactId: 1}
Email {Id: 1} Unchanged FK {CompanyContactId: 1}
FileUpload {Id: 12} Unchanged FK {InquiryId: 31}
Inquiry {Id: 31} Unchanged FK {CoatingId: 211} FK {CompanyContactId: 1} FK {CompanyId: 1} FK {DRTVendorId: 15}
PartWithPrice {Id: -2147482647} Added FK {CoatingId: 211} FK {DRTVendorId: 15} FK {PartId: 32}
BallValve {Id: 32} Modified FK {DRTVendorId: <null>} FK {PartsCategoryId: 3} FK {Id: 32} FK {ValveManufacturerId: 115} FK {ValveTypeName: Ball Valve} FK {Id: 32}
CheckValve {Id: 35} Modified FK {DRTVendorId: <null>} FK {PartsCategoryId: 3} FK {Id: 35} FK {ValveManufacturerId: 117} FK {ValveTypeName: Check Valve} FK {Id: 35}
GateValve {Id: 36} Modified FK {DRTVendorId: <null>} FK {PartsCategoryId: 3} FK {Id: 36} FK {ValveManufacturerId: 118} FK {ValveTypeName: Gate Valve} FK {Id: 36}
Valve {Id: 34} Modified FK {DRTVendorId: <null>} FK {PartsCategoryId: 3} FK {Id: 34} FK {ValveManufacturerId: 116} FK {ValveTypeName: Butterfly Valve}
Valve {Id: 37} Modified FK {DRTVendorId: <null>} FK {PartsCategoryId: 3} FK {Id: 37} FK {ValveManufacturerId: 119} FK {ValveTypeName: Globe Or Control Valve}
Valve {Id: 38} Modified FK {DRTVendorId: <null>} FK {PartsCategoryId: 3} FK {Id: 38} FK {ValveManufacturerId: 122} FK {ValveTypeName: Screwed End Gate Valve}
Valve {Id: 39} Modified FK {DRTVendorId: <null>} FK {PartsCategoryId: 3} FK {Id: 39} FK {ValveManufacturerId: 123} FK {ValveTypeName: Threaded Check Valve}
Valve {Id: 40} Modified FK {DRTVendorId: <null>} FK {PartsCategoryId: 3} FK {Id: 40} FK {ValveManufacturerId: 130} FK {ValveTypeName: Wafer Check Valve}
PartsCategory {Id: 3} Modified
ValveInternal {Id: 9} Modified
ValveInternal {Id: 10} Modified
ValveInternal {Id: 11} Modified
InquiryPartWithPrice (Dictionary<string, object>) {InquiriesId: 31, PartsWithPricesId: -2147482647} Added FK {InquiriesId: 31} FK {PartsWithPricesId: -2147482647}
PartWithPriceValveInternal (Dictionary<string, object>) {PartWithPricesId: -2147482647, ValveInternalsId: 9} Added FK {PartWithPricesId: -2147482647} FK {ValveInternalsId: 9}
PartWithPriceValveInternal (Dictionary<string, object>) {PartWithPricesId: -2147482647, ValveInternalsId: 10} Added FK {PartWithPricesId: -2147482647} FK {ValveInternalsId: 10}
PartWithPriceValveInternal (Dictionary<string, object>) {PartWithPricesId: -2147482647, ValveInternalsId: 11} Added FK {PartWithPricesId: -2147482647} FK {ValveInternalsId: 11}
我显然在理解 EF 和 EF 跟踪的工作原理时遇到问题,并且感到非常迷茫。
我已经坚持了很长一段时间,所以任何帮助将不胜感激。
谢谢,泰勒
解决方案
推荐阅读
- java - 从 hyperledger ca 生成的私钥文件中获取 java.security.PrivateKey
- javascript - 如何从gridview中的checkboxcolumn获取多个数据
- go - 我可以在没有两个索引调用的情况下更新和检索 Map 中的元素吗?
- llvm - 静态编译 LLVM 工具
- cursor - GTK3 中嘈杂的 cairo 创建的自定义光标
- android - 基本活动的 ViewDataBinding 类
- agda - 来自标准库“less”的内置“less”
- composer-php - 在 composer.json 中排除文件更新
- angular - 使用 Cypress 测试 Angular 时如何防止整页重新加载?
- java - 获取 Java 和 C# 之间的匹配校验和