You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

.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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.20 12:09:20