EF Core 如何基于两列组合索引实现同一客户下的编号自动递增
按客户维度的编号自动递增实现方案
针对你现有的Ticket实体和已配置的(CustomerId, CustomerTicketNumber)唯一索引,只需要保证同个客户维度下的CustomerTicketNumber从1开始连续不重复即可,不需要修改现有实体模型和索引规则,可根据业务并发量选择以下实现方式:
方案1:DbContext 变更拦截(中小并发场景适用)
不依赖数据库特殊配置,将编号生成逻辑统一收敛到EF Core层,避免业务代码分散编写重复规则:
- 重写
DbContext的SaveChanges方法,统一拦截所有处于Added状态、且未手动指定CustomerTicketNumber的Ticket实体 - 对每个待新增的Ticket,在同一个事务内查询对应客户下已存在的最大
CustomerTicketNumber,取最大值+1作为新编号 - 整个计算、写入流程必须设置序列化隔离级别的事务,否则两个并发请求会同时查询到相同的最大值,生成重复编号触发唯一索引报错
- 核心实现代码:
public override int SaveChanges() { // 筛选待新增、未指定编号的Ticket var pendingNewTickets = ChangeTracker.Entries<Ticket>() .Where(entry => entry.State == EntityState.Added && entry.Entity.CustomerTicketNumber == 0 ) .ToList(); if (!pendingNewTickets.Any()) return base.SaveChanges(); // 开启序列化事务,阻塞并发写避免编号重复 using var transaction = Database.BeginTransaction(IsolationLevel.Serializable); foreach (var ticketEntry in pendingNewTickets) { var customerId = ticketEntry.Entity.CustomerId; // 查询当前客户历史最大编号,无历史记录时默认取0 var maxExistNumber = Tickets .Where(t => t.CustomerId == customerId) .Max(t => (int?)t.CustomerTicketNumber) ?? 0; ticketEntry.Entity.CustomerTicketNumber = maxExistNumber + 1; } var result = base.SaveChanges(); transaction.Commit(); return result; }
方案2:数据库插入触发器(高并发场景适用)
EF层拦截在高并发场景下容易因为事务锁竞争导致吞吐量下降,直接在数据库侧绑定插入触发器,在数据物理写入前自动计算编号:
- 触发器和表逻辑强绑定,不会因为上层应用漏写逻辑导致编号缺失
- 以SQL Server为例的触发器示例:
CREATE TRIGGER trg_Ticket_AutoCustomerNumber ON Tickets INSTEAD OF INSERT AS BEGIN SET NOCOUNT ON; INSERT INTO Tickets (Id, CustomerId, CustomerTicketNumber) SELECT i.Id, i.CustomerId, -- 同客户下历史最大编号+1,无记录则从1开始 ISNULL(MAX(ExistTicket.CustomerTicketNumber), 0) + 1 FROM inserted i LEFT JOIN Tickets ExistTicket ON ExistTicket.CustomerId = i.CustomerId GROUP BY i.Id, i.CustomerId END
落地注意事项
- 必须保留已配置的
(CustomerId, CustomerTicketNumber)唯一索引,这是拦截重复数据的最后一道防线,就算计算逻辑出现异常,数据库也会阻止重复值写入 - 不要在各个业务接口中零散编写编号计算逻辑,统一收敛到DbContext拦截或数据库触发器中,避免不同入口的生成规则不统一
- 不要在无事务隔离的情况下直接查询最大值+1写入,这种写法在并发场景下必然会出现编号重复
内容的提问来源于stack exchange,提问作者Vesuvian
相关产品推荐
相关产品推荐

