首页 > 解决方案 > 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 跟踪的工作原理时遇到问题,并且感到非常迷茫。

我已经坚持了很长一段时间,所以任何帮助将不胜感激。

谢谢,泰勒

标签: c#mysqlentity-frameworkentity-framework-coreblazor-server-side

解决方案


推荐阅读