EF6 API多对多关系数据更新问题:重复创建关联项及更新失效
解决EF多对多关联中PUT/POST时重复创建关联实体的问题
第一个问题:为什么添加已有Serie的新Card会创建新Serie?
这是因为Entity Framework默认会将未被上下文跟踪的关联实体判定为新实体,进而执行插入操作。当你从Postman传递Card对象时,附带的Serie/Effect对象只是包含Id的简单DTO,EF并没有在当前上下文中跟踪这些对象,所以会默认把它们当成新的记录插入数据库。
你修改后的代码为什么数据库无更新?
你尝试将关联实体的状态设为EntityState.Modified,但这个思路存在两个核心问题:
- 多对多关系的更新不能通过直接修改实体状态实现,EF需要通过导航属性的
Add/Remove操作来维护关联表数据; - 客户端传来的Serie/Effect对象没有被上下文跟踪,直接设置
Modified状态可能会因为上下文未识别该实体而失效,甚至引发异常。
正确的PUT请求处理方案
我们需要先从数据库加载现有Card及其关联数据,再对比更新基本属性和关联关系,确保EF能正确识别哪些是已存在的关联实体:
// PUT: api/Cards/5 [ResponseType(typeof(void))] public async Task<IHttpActionResult> PutCard(int id, Card card) { if (!ModelState.IsValid) { return BadRequest(ModelState); } if (id != card.Id) { return BadRequest(); } // 从数据库加载现有Card,同时包含关联的Series和Effects var existingCard = await db.Cards .Include(c => c.Series) .Include(c => c.Effects) .FirstOrDefaultAsync(c => c.Id == id); if (existingCard == null) { return NotFound(); } // 更新Card的基本属性(无需手动赋值,EF自动映射) db.Entry(existingCard).CurrentValues.SetValues(card); // 处理Serie关联:移除不在新列表中的项,添加新的已存在项 // 先移除旧关联 foreach (var existingSerie in existingCard.Series.ToList()) { if (!card.Series.Any(s => s.Id == existingSerie.Id)) { existingCard.Series.Remove(existingSerie); } } // 再添加新关联(确保是数据库中已有的Serie) foreach (var newSerie in card.Series) { if (!existingCard.Series.Any(s => s.Id == newSerie.Id)) { var targetSerie = await db.Series.FindAsync(newSerie.Id); if (targetSerie != null) { existingCard.Series.Add(targetSerie); } } } // 用同样的逻辑处理Effect关联 foreach (var existingEffect in existingCard.Effects.ToList()) { if (!card.Effects.Any(e => e.Id == existingEffect.Id)) { existingCard.Effects.Remove(existingEffect); } } foreach (var newEffect in card.Effects) { if (!existingCard.Effects.Any(e => e.Id == newEffect.Id)) { var targetEffect = await db.Effects.FindAsync(newEffect.Id); if (targetEffect != null) { existingCard.Effects.Add(targetEffect); } } } try { await db.SaveChangesAsync(); } catch (DbUpdateConcurrencyException) { if (!CardExists(id)) { return NotFound(); } else { throw; } } return StatusCode(HttpStatusCode.NoContent); }
顺带解决POST请求的相同问题
如果POST新Card时也遇到重复创建Serie/Effect的问题,需要先将客户端传来的关联实体替换为数据库中已有的实例:
// POST: api/Cards [ResponseType(typeof(Card))] public async Task<IHttpActionResult> PostCard(Card card) { if (!ModelState.IsValid) { return BadRequest(ModelState); } // 替换Serie为数据库中已有的实体 var validSeries = new HashSet<Serie>(); foreach (var serie in card.Series) { var existingSerie = await db.Series.FindAsync(serie.Id); if (existingSerie != null) { validSeries.Add(existingSerie); } } card.Series = validSeries; // 替换Effect为数据库中已有的实体 var validEffects = new HashSet<Effect>(); foreach (var effect in card.Effects) { var existingEffect = await db.Effects.FindAsync(effect.Id); if (existingEffect != null) { validEffects.Add(existingEffect); } } card.Effects = validEffects; db.Cards.Add(card); await db.SaveChangesAsync(); return CreatedAtRoute("DefaultApi", new { id = card.Id }, card); }
总结
你不需要自定义PUT请求的路由,只需要修改PUT方法的业务逻辑:
- 加载现有实体及其关联数据;
- 通过
CurrentValues.SetValues更新基本属性; - 对比并更新多对多关联(移除旧项、添加已存在的新项);
- 确保关联实体是从数据库加载的,而非客户端传来的未跟踪对象。
内容的提问来源于stack exchange,提问作者converset jordan
相关产品推荐
相关产品推荐

