EF Core与.NET 8中多对多关联表插入数据失败排查
场景说明
在.NET 8环境下使用EF Core配置了Order、Product及多对多关联表OrderDetail模型,尝试通过API端点创建包含2个商品及对应数量的新订单时,Orders表生成了订单记录,但OrderDetails表无数据,同时出现PostgreSQL非空约束错误。
错误信息
Npgsql.PostgresException (0x80004005): 23502: null value in column "UserId" of relation "Orders" violates not-null constraint
DETAIL: Failing row contains (15, null, 1, 2024-01-01 14:07:25.091254+00, 2024-01-01 14:07:25.091254+00, null, null).
Exception data:
Severity: ERROR
SqlState: 23502
MessageText: null value in column "UserId" of relation "Orders" violates not-null constraintDetail: Failing row contains (15, null, 1, 2024-01-01 14:07:25.091254+00, 2024-01-01 14:07:25.091254+00, null, null).
SchemaName: public
TableName: Orders
ColumnName: UserId
File: execMain.c
Line: 2003
Routine: ExecConstraints
解决方案
1. 修复OrderDetail实体配置
当前OrderDetail中导航属性Order和Product使用new()初始化,EF会自动跟踪这个新创建的空Order实例,导致保存时插入一条UserId为空的无效订单。同时需要为OrderDetail配置复合主键(因为它是多对多关联表,主键由OrderId和ProductId组成)。
修改OrderDetail类:
public class OrderDetail { public int ProductId { get; set; } public Product Product { get; set; } = null!; public int OrderId { get; set; } public Order Order { get; set; } = null!; public int Amount { get; set; } }
在DkdbContext的OnModelCreating方法中添加复合主键配置:
builder.Entity<OrderDetail>() .HasKey(od => new { od.OrderId, od.ProductId });
2. 优化订单创建逻辑,合并保存操作
原代码分两次调用SaveChangesAsync,且手动设置OrderId,容易引发跟踪冲突。直接通过Order的OrderDetails集合添加关联数据,一次完成保存,让EF自动处理外键关联。
修改端点代码:
public class OrderEnpoint { public static void Map(WebApplication app) { app.MapPost("/order", async (DkdbContext db, OrderDto orderRequest, UserManager<DkUser> userManager) => { if (orderRequest == null || string.IsNullOrEmpty(orderRequest.UserId)) return Results.BadRequest(); var user = await userManager.FindByIdAsync(orderRequest.UserId); if (user == null) return Results.NotFound("指定用户不存在"); // 验证商品ID是否有效(可选但推荐) var productIds = orderRequest.ProductOrderList.Select(e => e.ProductId).ToList(); var existingProductCount = await db.Products.CountAsync(p => productIds.Contains(p.Id)); if (existingProductCount != productIds.Count) return Results.BadRequest("存在无效的商品ID"); var newOrder = new Order { UserId = orderRequest.UserId, CreatedBy = user.UserName, UpdatedBy = user.UserName, // 直接将OrderDetail关联到Order的集合中 OrderDetails = orderRequest.ProductOrderList .Select(e => new OrderDetail { ProductId = e.ProductId, Amount = e.Amount }).ToList() }; await db.Orders.AddAsync(newOrder); await db.SaveChangesAsync(); return Results.Created($"/order/{newOrder.Id}", new { OrderId = newOrder.Id }); }); } }
3. 补充数据验证(可选)
为避免无效数据插入,可在创建订单前验证:
- 用户ID对应的用户是否存在
- 商品ID是否存在于
Products表中 - 商品数量
Amount是否大于0
原因分析
- 无效Order实例跟踪:
OrderDetail中Order = new()会创建一个空的Order对象,EF上下文会自动跟踪该实例,保存时尝试插入这条UserId为空的记录,触发非空约束错误。 - 多次保存的冲突:原代码分两次保存,第一次保存有效订单后,第二次添加
OrderDetail时,EF对导航属性的跟踪可能出现冲突,导致OrderDetails数据未正确插入。
内容的提问来源于stack exchange,提问作者Vitou Hun

