如何用Entity Framework在.NET Core API中单次请求插入多对多数据
我看了你提供的代码,核心问题出在多对多中间实体的EF Core配置完全错误——你把关联的实体、外键字段全部弄混了,导致EF无法识别你要关联已有子实体,反而默认创建新的记录。另外,你的Category模型里的集合命名也不太规范(比如ProductType应该是ProductTypeCategories,和其他集合保持一致,避免混淆)。
1. 配置错误点拆解
拿ColorsCategory的配置举例,你原来的代码:
modelBuilder.Entity<ColorsCategory>() .HasOne(p => p.Category) .WithMany(p => p.colorsCategories) .HasForeignKey(p => p.ColorId) // 错误:Category的外键应该是CategoryId,不是ColorId .OnDelete(DeleteBehavior.Cascade); modelBuilder.Entity<ColorsCategory>() .HasOne(p => p.Colors) .WithMany(p => p.ColorsCategories) .HasForeignKey(p => p.CategoryId) // 错误:Colors的外键应该是ColorId,不是CategoryId .OnDelete(DeleteBehavior.Cascade);
这里完全搞反了外键关联:Category对应的外键是CategoryId,Colors对应的外键才是ColorId。更离谱的是后面的ProductTypeCategory、UsageCategory等配置,你甚至重复写了HasOne(p => p.Category),根本没关联到对应的子实体(比如ProductType、Usage),这就导致EF完全无法理解你的关联逻辑。
2. 修正后的DbContext配置
首先,先确保你的中间实体(比如ColorsCategory)定义正确,必须包含两个实体的导航属性:
// 示例中间实体(你需要确保所有中间实体都类似) public class ColorsCategory { public int ColorId { get; set; } public Colors Colors { get; set; } public int CategoryId { get; set; } public Category Category { get; set; } }
然后修正OnModelCreating的配置,每个中间实体都要正确关联两边的实体和对应的外键:
protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); // 1. Colors <-> Category 多对多配置 modelBuilder.Entity<ColorsCategory>() .HasKey(p => new { p.ColorId, p.CategoryId }); // 复合主键 modelBuilder.Entity<ColorsCategory>() .HasOne(p => p.Category) .WithMany(p => p.colorsCategories) .HasForeignKey(p => p.CategoryId) // Category对应的外键是CategoryId .OnDelete(DeleteBehavior.Cascade); modelBuilder.Entity<ColorsCategory>() .HasOne(p => p.Colors) .WithMany(p => p.ColorsCategories) .HasForeignKey(p => p.ColorId) // Colors对应的外键是ColorId .OnDelete(DeleteBehavior.Cascade); // 2. ProductType <-> Category 多对多配置 modelBuilder.Entity<ProductTypeCategory>() .HasKey(p => new { p.ProductTypesId, p.CategoryId }); modelBuilder.Entity<ProductTypeCategory>() .HasOne(p => p.Category) .WithMany(p => p.ProductType) .HasForeignKey(p => p.CategoryId) .OnDelete(DeleteBehavior.Cascade); modelBuilder.Entity<ProductTypeCategory>() .HasOne(p => p.ProductType) // 这里要关联ProductType实体,不是Category! .WithMany(p => p.ProductTypeCategories) .HasForeignKey(p => p.ProductTypesId) .OnDelete(DeleteBehavior.Cascade); // 3. Usage <-> Category 多对多配置 modelBuilder.Entity<UsageCategory>() .HasKey(p => new { p.UsageId, p.CategoryId }); modelBuilder.Entity<UsageCategory>() .HasOne(p => p.Category) .WithMany(p => p.Usage) .HasForeignKey(p => p.CategoryId) .OnDelete(DeleteBehavior.Cascade); modelBuilder.Entity<UsageCategory>() .HasOne(p => p.Usage) // 关联Usage实体 .WithMany(p => p.UsageCategories) .HasForeignKey(p => p.UsageId) .OnDelete(DeleteBehavior.Cascade); // 4. SizeType <-> Category 多对多配置 modelBuilder.Entity<SizeTypeCategory>() .HasKey(p => new { p.SizeTypeId, p.CategoryId }); modelBuilder.Entity<SizeTypeCategory>() .HasOne(p => p.Category) .WithMany(p => p.SizeType) .HasForeignKey(p => p.CategoryId) .OnDelete(DeleteBehavior.Cascade); modelBuilder.Entity<SizeTypeCategory>() .HasOne(p => p.SizeType) // 关联SizeType实体 .WithMany(p => p.SizeTypeCategories) .HasForeignKey(p => p.SizeTypeId) .OnDelete(DeleteBehavior.Cascade); }
同时建议修正Category模型里的集合命名,让它更清晰:
public class Category { public int Id { get; set; } public string Name { get; set; } public List<ColorsCategory> ColorsCategories { get; set; } // 修正命名 public List<ProductTypeCategory> ProductTypeCategories { get; set; } // 修正命名 public List<UsageCategory> UsageCategories { get; set; } // 修正命名 public List<SizeTypeCategory> SizeTypeCategories { get; set; } // 修正命名 }
3. 正确的数据插入方式
配置修正后,插入数据时不能直接创建子实体的新对象(比如new Usage { Id = 1 }),因为EF会认为这是新实体并插入。正确的做法是:
- 从数据库获取已有子实体,或者用
DbContext.Attach标记为已存在(Unchanged状态) - 将这些已存在的子实体关联到Category的集合中
- 最后添加Category并保存
示例控制器代码:
[HttpPost] public async Task<ActionResult<Category>> CreateCategory(CategoryCreateDto dto) { var category = new Category { Name = dto.Name }; // 关联已有Colors foreach (var colorId in dto.ColorIds) { var color = new Colors { Id = colorId }; _context.Attach(color); // 标记为已存在,EF不会创建新记录 category.ColorsCategories.Add(new ColorsCategory { Colors = color }); } // 关联已有ProductType foreach (var productTypeId in dto.ProductTypeIds) { var productType = new ProductType { Id = productTypeId }; _context.Attach(productType); category.ProductTypeCategories.Add(new ProductTypeCategory { ProductType = productType }); } // 同理处理Usage和SizeType... _context.Categories.Add(category); await _context.SaveChangesAsync(); return CreatedAtAction(nameof(GetCategory), new { id = category.Id }, category); } // 对应的DTO,只接收需要关联的子实体ID列表 public class CategoryCreateDto { public string Name { get; set; } public List<int> ColorIds { get; set; } public List<int> ProductTypeIds { get; set; } public List<int> UsageIds { get; set; } public List<int> SizeTypeIds { get; set; } }
这样EF就会只在中间表(比如ColorsCategory)中插入关联记录,而不会创建新的Colors、Usage等子实体。
内容的提问来源于stack exchange,提问作者Antoine Gimenez

