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

创建含唯一字符串与对应序列自增编号的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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.19 07:42:24