Entity Framework中频繁使用EntityState.Unchanged是否真的必要?
这个问题我太熟悉了——本质上是EF的实体跟踪机制在搞鬼!咱们先拆解一下为什么拆分后会出问题,再给你几个更优雅的解决方案,再也不用手动挨个设置EntityState.Unchanged了。
问题根源:实体是否被EF上下文跟踪
你看,在ProblemFreeCheckOut里,你是直接从当前的db上下文加载Item(用db.Items.Include(...)),这时候EF已经在内存中跟踪了这些Item以及它们关联的Memory、ScreenSize等实体。当你把这些跟踪中的实体关联到新的CartItem时,EF知道它们是数据库中已存在的记录,不会重复添加。
但拆分后,你的购物车中的Item实体是脱离当前上下文的(比如存在Session、Cookie或者其他缓存里,不是结账时的db上下文加载的)。EF看到这些“陌生”的实体(没有被当前上下文跟踪),就会默认把它们当成新实体,试图插入到数据库里——这就是重复记录的来源。
优雅解决方案
方案1:购物车只存必要信息(最推荐)
购物车根本不需要保存完整的Item实体,只需要ItemId和Quantity就够了。结账时再从当前上下文加载对应的Item,EF会自动跟踪它和关联实体,完全不用手动设置状态。
首先,定义一个极简的购物车项DTO:
public class CartItemDto { public int ItemId { get; set; } public int Quantity { get; set; } }
然后修改添加到购物车的逻辑:只把ItemId和数量存入购物车,而不是整个Item对象。
最后是结账方法:
public ActionResult CheckOut() { // 假设cartItems是List<CartItemDto>类型 Order order = new Order() { Payed = false, Canceled = false }; db.Orders.Add(order); foreach (var cartItemDto in cartItems) { // 从当前上下文加载Item,EF自动跟踪它和所有关联实体 var item = db.Items .Include(i => i.Memory) .Include(i => i.OperatingSystem) .Include(i => i.ScreenSize) .FirstOrDefault(i => i.ItemId == cartItemDto.ItemId); if (item == null) continue; // 处理商品不存在的情况 var cartItem = new CartItem { Order = order, OrderId = order.OrderId, ItemId = cartItemDto.ItemId, Item = item, Quantity = cartItemDto.Quantity }; db.CartItems.Add(cartItem); } db.SaveChanges(); cartItems.Clear(); return View(); }
这种方式不仅解决了重复添加的问题,还减少了购物车存储的数据量(不用存整个Item对象),更高效也更安全。
方案2:如果必须保留完整Item实体,附加到上下文
如果你因为某些原因必须在购物车中保存完整的Item实体,那结账时只需要把Item附加到当前上下文并标记为Unchanged,EF会自动处理它的所有关联实体,不用挨个设置:
public ActionResult CheckOut() { Order order = new Order() { Payed = false, Canceled = false }; db.Orders.Add(order); foreach (var cartItem in cartItems) { cartItem.Order = order; cartItem.OrderId = order.OrderId; // 关键:将Item附加到当前上下文,标记为已存在(Unchanged) db.Entry(cartItem.Item).State = EntityState.Unchanged; // EF会自动识别Item的关联实体(Memory等)的主键,不会把它们当成新记录 db.CartItems.Add(cartItem); } db.SaveChanges(); cartItems.Clear(); return View(); }
这里的原理是:当你附加一个有主键值的实体时,EF会默认认为它已经存在于数据库中,标记为Unchanged,同时它的导航属性只要有有效的主键值,EF也会将那些实体视为已存在,不会尝试插入新记录。
总结
手动挨个设置每个关联实体的EntityState.Unchanged确实是繁琐且容易出错的做法,问题的核心就是实体是否被当前EF上下文跟踪。最推荐的是方案1,只存ID和数量;如果必须保留实体,方案2也能一次性解决问题,不用再写一堆重复的状态设置代码。
内容的提问来源于stack exchange,提问作者ProfessorChaos

