开发外卖应用创建订单时Food的Guid主键重复报错如何解决
问题根源
你遇到的重复键异常和主键使用Guid类型完全无关,不需要修改主键类型。异常的本质是EF Core的变更跟踪机制将你前端传入的Food实例识别为需要新增到Food表的新实体,而这些实例已经带上了数据库中已存在的FoodId,插入时触发主键冲突。
你直接将前端传入的、未被EF上下文跟踪的Food集合赋值给了Order的导航属性,EF在保存Order时,会尝试将关联的Food也作为新实体插入Food表,而非仅关联已有Food生成多对多中间表记录。
最优解决方案
- 优化
CreateOrderCommand结构,前端仅需要提交食品ID集合即可,无需提交完整Food实体,既减少传输体积也避免数据被篡改:
public class CreateOrderCommand : IRequest<Guid> { public string UserName { get; set; } // 替换Food集合为食品ID集合 public ICollection<Guid> FoodIds { get; set; } }
- 调整Handler逻辑,从数据库查询已存在的Food实体(会被EF上下文正常跟踪,识别为已有实体),再关联到Order:
public async Task<Guid> Handle(CreateOrderCommand request, CancellationToken cancellationToken) { var order = _mapper.Map<Order>(request); order.OrderTime = DateTime.Now; var user = await _userRepository.GetByUserNameAsync(request.UserName); GeoCoordinate userLocation = new GeoCoordinate(user.Latitude, user.Longitude); List<Restaurant> restaurants = (List<Restaurant>)await _restaurantRepository.ListAllAsync(); List<double> distances = new List<double>(); double minValue = 0; int minValuePosition = -1; //pronadji udaljenosti for (int i = 0; i < restaurants.Count; i++) { TimeSpan ts = DateTime.Now - restaurants[i].LastOrdered; if(ts.TotalMinutes >= 15) { GeoCoordinate restaurantCoords = new GeoCoordinate(restaurants[i].Latitude, restaurants[i].Longitude); distances.Add(userLocation.GetDistanceTo(restaurantCoords)); minValue = distances[i]; minValuePosition = i; } else { distances[i] = -1; } } //pronadji najmanju udaljenost koja je na listi for (int i = 0; i < distances.Count; i++) { if (distances[i] < 0) continue; if (distances[i] < minValue) { minValue = distances[i]; minValuePosition = i; } } if(minValuePosition == -1) { //nema slobodnih restorana throw new Exception("当前无可用餐厅"); } order.UserName = user.UserName; // 新增逻辑:批量查询已存在的食品 var existingFoods = await _foodRepository.ListAsync(f => request.FoodIds.Contains(f.FoodId), cancellationToken); if (existingFoods.Count != request.FoodIds.Count) { throw new ArgumentException("包含无效的食品ID"); } order.Foods = existingFoods; // 用数据库查询的真实价格计算总价,避免前端篡改价格 order.OrderTotal = existingFoods.Sum(f => f.Price); order = await _orderRepository.AddAsync(order); return order.OrderId; }
临时兼容方案(不推荐)
如果你坚持要在Command中传递完整Food实体,可以在关联到Order前手动将这些Food附加到EF上下文,标记为已有实体:
foreach (var food in request.Foods) { // 附加到上下文,标记为未修改,EF就不会尝试插入Food表 _context.Attach(food).State = EntityState.Unchanged; } order.Foods = request.Foods;
该方案的缺点是前端可以篡改Food的价格、名称等字段,附加后EF如果被配置为保存关联实体的修改,会更新Food表的原有数据,存在安全风险。
内容的提问来源于stack exchange,提问作者Nikola Nikolic
相关产品推荐
相关产品推荐

