创建含唯一字符串与对应序列自增编号的Hibernate复合主键
实现Hibernate复合主键(唯一字符串+对应分组自增数值)
刚接触Hibernate就碰到这种带分组自增的复合主键问题,确实容易懵——毕竟MySQL自带的自增是全局的,没法直接针对每个字符串单独计数。结合你用的Java 8、Hibernate 5、Spring Boot 1.5这套技术栈,我给你两种可行的实现方案,一步步来:
方案一:Hibernate层面实现自增逻辑
这种方式靠代码控制插入前的序列生成,适合不想动数据库触发器的场景。
1. 定义复合主键类
Hibernate要求复合主键必须是可序列化的类,还得重写equals()和hashCode(),用来判断主键是否唯一。你可以这么写:
import java.io.Serializable; import java.util.Objects; public class TicketPK implements Serializable { // 你的唯一字符串字段,比如票种编码 private String ticketCode; // 对应ticketCode的自增数值 private Long sequenceNum; // 必须要有无参构造器,Hibernate反射实例化要用 public TicketPK() {} // 带参构造器,方便创建实例 public TicketPK(String ticketCode, Long sequenceNum) { this.ticketCode = ticketCode; this.sequenceNum = sequenceNum; } // 别忘了加getter和setter @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; TicketPK ticketPK = (TicketPK) o; return Objects.equals(ticketCode, ticketPK.ticketCode) && Objects.equals(sequenceNum, ticketPK.sequenceNum); } @Override public int hashCode() { return Objects.hash(ticketCode, sequenceNum); } }
2. 映射票务实体类
用@EmbeddedId把刚才的主键类嵌入到实体里,然后通过@PrePersist注解在插入前自动生成对应ticketCode的sequenceNum:
import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import javax.persistence.*; import java.util.Date; @Entity @Table(name = "tickets") public class Ticket implements ApplicationContextAware { // 用来获取Spring容器里的Repository private static ApplicationContext context; @EmbeddedId private TicketPK id; // 其他业务字段,比如票名、创建时间 private String ticketName; private Date createTime; // 无参构造器必须有 public Ticket() {} // 业务构造器:只传ticketCode和票名,sequenceNum后续自动生成 public Ticket(String ticketCode, String ticketName) { this.id = new TicketPK(ticketCode, null); this.ticketName = ticketName; this.createTime = new Date(); } // 实现ApplicationContextAware,拿到Spring容器 @Override public void setApplicationContext(ApplicationContext applicationContext) { context = applicationContext; } // 插入前自动生成sequenceNum @PrePersist public void generateSequenceNum() { if (id.getSequenceNum() == null) { // 从Spring容器里拿Repository,查询当前ticketCode的最大sequenceNum TicketRepository repository = context.getBean(TicketRepository.class); Long maxNum = repository.findMaxSequenceNumByTicketCode(id.getTicketCode()); // 如果是第一个,从1开始,否则加1 id.setSequenceNum(maxNum == null ? 1 : maxNum + 1); } } // 别忘了加所有字段的getter和setter }
3. 配套Spring Data JPA Repository
创建一个Repository来查询每个ticketCode的最大序列值:
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; public interface TicketRepository extends JpaRepository<Ticket, TicketPK> { // 查询指定ticketCode的最大sequenceNum @Query("SELECT MAX(t.id.sequenceNum) FROM Ticket t WHERE t.id.ticketCode = :code") Long findMaxSequenceNumByTicketCode(@Param("code") String code); }
4. 数据库表结构
对应的MySQL表不需要给sequenceNum设自增,直接建复合主键就行:
CREATE TABLE tickets ( ticket_code VARCHAR(50) NOT NULL, sequence_num BIGINT NOT NULL, ticket_name VARCHAR(100) NOT NULL, create_time DATETIME NOT NULL, PRIMARY KEY (ticket_code, sequence_num) );
方案二:数据库触发器实现自增逻辑
如果更倾向于让数据库自己处理序列生成,可以用触发器+辅助表的方式,避免代码里的逻辑:
1. 创建辅助序列表
用来记录每个ticketCode的当前序列值,避免每次插入都查原表的最大值:
CREATE TABLE ticket_sequence ( ticket_code VARCHAR(50) PRIMARY KEY, current_num BIGINT DEFAULT 1 );
2. 创建插入触发器
在插入tickets表前自动计算sequenceNum:
DELIMITER // CREATE TRIGGER trigger_ticket_sequence BEFORE INSERT ON tickets FOR EACH ROW BEGIN DECLARE next_num BIGINT; -- 查询当前ticketCode的序列值 SELECT current_num INTO next_num FROM ticket_sequence WHERE ticket_code = NEW.ticket_code; -- 如果是新的ticketCode,初始化序列 IF next_num IS NULL THEN SET next_num = 1; INSERT INTO ticket_sequence(ticket_code, current_num) VALUES(NEW.ticket_code, next_num + 1); ELSE -- 更新序列值为下一个数 UPDATE ticket_sequence SET current_num = next_num + 1 WHERE ticket_code = NEW.ticket_code; END IF; -- 给新插入的行赋值sequenceNum SET NEW.sequence_num = next_num; END // DELIMITER ;
这种情况下,实体类里的@PrePersist方法和Repository的查询方法都可以去掉,插入时数据库会自动填充sequenceNum。
重要注意事项
- 并发问题:不管用哪种方案,如果有多个线程同时插入同一个ticketCode,都可能出现序列重复的情况。建议开启数据库的
REPEATABLE READ隔离级别,或者给查询/更新逻辑加锁,也可以在实体类里加乐观锁字段(@Version)来避免冲突。 - Spring Boot配置:记得在
application.properties里配置好Hibernate和数据源:
spring.jpa.hibernate.ddl-auto=update spring.jpa.show-sql=true spring.datasource.url=jdbc:mysql://localhost:3306/your_db?useSSL=false&serverTimezone=UTC spring.datasource.username=root spring.datasource.password=your_password spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
内容的提问来源于stack exchange,提问作者mrClean
相关产品推荐
相关产品推荐

