.NET Core API关联表嵌套插入实现及最佳实践咨询
.NET Core API 关联表插入的最佳实践
我正在使用.NET Core构建API,需要向存在关联关系的Client、Shipment、ShipmentDetail表插入记录:先向Client表插入记录并获取其ClientId,再将该ClientId插入Shipment表,最后把新增Shipment的ShipmentId插入ShipmentDetail表。目前代码可运行,但不清楚这种场景下的最佳实践,以及是否需要使用多个DTO或端点来处理嵌套插入。
实体类定义
public class Client { public int ClientId { get; set; } public string Name { get; set; } = String.Empty; public string Adress { get; set; } public List<Shipment> Shipments { get; set; } } public class Shipment { public int ShipmentId { get; set; } public DateTime TimeCreated { get; set; } public string ShippingAddress { get; set; } public int DeliveryCost { get; set; } public string OrderNumber { get; set; } public DateTime EstimatedDate { get; set; } public int ClientId { get; set; } public Client Client { get; set; } public List<ShipmentDetail>? ShipmentDetails { get; set; } public List<ShipmentStatu>? ShipmentStatus { get; set; } } public class ShipmentDetail { public int Id { get; set; } public string DeliveryNumber { get; set; } public string TrackingNumber { get; set; } public Shipment Shipment { get; set; } public int ShipmentId { get; set; } public Product Product { get; set; } public int ProductId { get; set; } }
已实现的部分代码
[HttpPost] public async Task<ActionResult<List<Shipment>>> AddShipments(ShipmentDto request) { var client = _context.Clients.Where(p => p.Name == request.ClientName).Select(p => p.ClientId) if (client.ToList().Count == 0) { var newClient = new Client { Name = request.ClientName, Adress = request.ClientAdress }; _context.Clients.Add(newClient); _context.SaveChanges(); var newShipment = new Shipment { ClientId = newClient.ClientId, TimeCreated = Convert.ToDateTime(DateTime.Now.GetDateTimeFormats()[0]), ShippingAddress = request.ShippingAddress, DeliveryCost = request.DeliveryCost, OrderNumber = "x" + DateTime.Now.ToString("MMddyy"), EstimatedDate = Convert.ToDateTime(DateTime.Now.AddDays(2).GetDateTimeFormats()[0]), }; _context.Shipments.Add(newShipment); _context.SaveChanges(); } else { var newShipment = new Shipment { ClientId = client.FirstOrDefault(), TimeCreated = Convert.ToDateTime(DateTime.Now.GetDateTimeFormats()[0]), ShippingAddress = request.ShippingAddress, DeliveryCost = request.DeliveryCost, OrderNumber = "x" + DateTime.Now.ToString("MMddyy"), EstimatedDate = Convert.ToDateTime(DateTime.Now.AddDays(2).GetDateTimeFormats()[0]), }; _context.Shipments.Add(newShipment); _context.SaveChanges(); } var shipment = _context.Shipments.Max(p => p.ShipmentId); return Ok(shipment); }
最佳实践建议
1. 使用事务保证数据一致性
当前代码多次调用SaveChanges(),如果中间某一步失败,会导致数据部分插入。应该用EF Core的事务包裹整个操作,确保要么全成功,要么全回滚:
using var transaction = await _context.Database.BeginTransactionAsync(); try { // 所有插入操作 await _context.SaveChangesAsync(); await transaction.CommitAsync(); } catch (Exception) { await transaction.RollbackAsync(); throw; }
2. 复用代码,避免重复
新增Shipment的逻辑在if和else分支中完全重复,应该提取为单独的方法:
private Shipment CreateShipment(int clientId, ShipmentDto request) { return new Shipment { ClientId = clientId, TimeCreated = DateTime.Now, // 直接用DateTime.Now,无需多余转换 ShippingAddress = request.ShippingAddress, DeliveryCost = request.DeliveryCost, OrderNumber = $"x{DateTime.Now:MMddyy}", EstimatedDate = DateTime.Now.AddDays(2) }; }
3. 设计嵌套DTO接收完整请求
如果需要同时创建ShipmentDetail,应该定义嵌套的DTO来接收所有关联数据,而不是拆分零散字段。例如:
public class CreateShipmentRequestDto { // Client相关字段(不存在则创建) public string ClientName { get; set; } = string.Empty; public string ClientAddress { get; set; } = string.Empty; // Shipment相关字段 public string ShippingAddress { get; set; } = string.Empty; public int DeliveryCost { get; set; } public DateTime? EstimatedDate { get; set; } // ShipmentDetail列表 public List<ShipmentDetailDto> ShipmentDetails { get; set; } = new List<ShipmentDetailDto>(); } public class ShipmentDetailDto { public string DeliveryNumber { get; set; } = string.Empty; public string TrackingNumber { get; set; } = string.Empty; public int ProductId { get; set; } }
4. 端点设计:单端点还是多端点?
- 如果这是一个完整的业务操作(比如"创建包含客户、发货单及明细的发货请求"),用单个端点更合理,符合业务上下文,减少客户端的请求次数。
- 如果业务允许拆分(比如客户可以单独创建,发货单单独创建),可以拆分出
/clients和/shipments等独立端点,但需要客户端自行处理多步请求的一致性。
对于你的场景,推荐用单个端点处理完整流程,因为这是一个连贯的业务动作。
5. 利用EF Core导航属性简化关联操作
不需要手动获取主键ID,EF Core会自动处理关联实体的外键赋值。例如:
// 查找或创建客户 var client = await _context.Clients.FirstOrDefaultAsync(c => c.Name == request.ClientName); if (client == null) { client = new Client { Name = request.ClientName, Adress = request.ClientAddress }; _context.Clients.Add(client); } // 创建发货单并关联客户及明细 var shipment = new Shipment { Client = client, // 直接关联导航属性,EF自动处理ClientId TimeCreated = DateTime.Now, ShippingAddress = request.ShippingAddress, DeliveryCost = request.DeliveryCost, OrderNumber = $"x{DateTime.Now:MMddyy}", EstimatedDate = request.EstimatedDate ?? DateTime.Now.AddDays(2), ShipmentDetails = request.ShipmentDetails.Select(dto => new ShipmentDetail { DeliveryNumber = dto.DeliveryNumber, TrackingNumber = dto.TrackingNumber, ProductId = dto.ProductId }).ToList() }; _context.Shipments.Add(shipment); await _context.SaveChangesAsync(); // 新增后直接获取shipment.ShipmentId,无需查询Max return Ok(shipment.ShipmentId);
6. 异步操作优化
全部使用EF Core的异步方法(如FirstOrDefaultAsync、SaveChangesAsync),避免同步方法阻塞线程,提升API并发性能。
7. 避免用Max获取主键ID
调用SaveChangesAsync()后,EF Core会自动将数据库生成的主键值赋值给实体的主键属性(比如shipment.ShipmentId),无需通过Max()查询,既低效又容易出现并发问题。
优化后的完整控制器代码示例
[HttpPost] public async Task<ActionResult<int>> CreateFullShipment(CreateShipmentRequestDto request) { using var transaction = await _context.Database.BeginTransactionAsync(); try { // 查找或创建客户 var client = await _context.Clients.FirstOrDefaultAsync(c => c.Name == request.ClientName); if (client == null) { client = new Client { Name = request.ClientName, Adress = request.ClientAddress }; _context.Clients.Add(client); } // 创建发货单及明细 var shipment = new Shipment { Client = client, TimeCreated = DateTime.Now, ShippingAddress = request.ShippingAddress, DeliveryCost = request.DeliveryCost, OrderNumber = $"x{DateTime.Now:MMddyy}", EstimatedDate = request.EstimatedDate ?? DateTime.Now.AddDays(2), ShipmentDetails = request.ShipmentDetails.Select(dto => new ShipmentDetail { DeliveryNumber = dto.DeliveryNumber, TrackingNumber = dto.TrackingNumber, ProductId = dto.ProductId }).ToList() }; _context.Shipments.Add(shipment); await _context.SaveChangesAsync(); await transaction.CommitAsync(); return Ok(shipment.ShipmentId); } catch (Exception ex) { await transaction.RollbackAsync(); return BadRequest($"操作失败:{ex.Message}"); } }
内容的提问来源于stack exchange,提问作者Commodore
相关产品推荐
相关产品推荐

