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

如何创建含年度自增序列的复合主键?Hibernate报错排查

解决复合主键(年份+年度自增序列)生成问题

你的需求是创建一个由当前年份+每年重置的自增序列组成的复合主键,但用@EmbeddedId实现时遇到了null id generated的错误,且seq字段始终为空,这主要是因为Hibernate对嵌入式主键内的自动生成字段支持有限,原代码的序列生成策略也无法实现年度重置的需求。

问题根源分析

  1. @EmbeddedId内的@GeneratedValue不生效:Hibernate不会主动为嵌入式主键中的字段自动生成值,即使你加了@SequenceGenerator和@GeneratedValue,也不会触发序列生成逻辑。
  2. 全局序列无法年度重置:原代码的序列是全局递增的,无法做到每年从1开始计数,不符合你的需求。
  3. 手动实例化主键时seq无值:你手动创建EmbedddedCarsKey实例时,Hibernate没有触发序列生成,导致seq为空,进而抛出主键为空的异常。

解决方案:自定义主键生成器

最靠谱的方式是实现一个自定义的Hibernate主键生成器,负责生成「年份+年度自增序列」的复合主键。以下是完整实现步骤:

1. 实现自定义主键生成器

这个生成器会查询当前年份的最大序列值,然后自增,保证每年从1开始计数:

import org.hibernate.HibernateException;
import org.hibernate.engine.spi.SharedSessionContractImplementor;
import org.hibernate.id.IdentifierGenerator;
import java.io.Serializable;
import java.time.LocalDate;
import java.util.Objects;
import javax.persistence.Query;

public class YearlySequenceGenerator implements IdentifierGenerator {

    @Override
    public Serializable generate(SharedSessionContractImplementor session, Object object) throws HibernateException {
        LocalDate now = LocalDate.now();
        int currentYear = now.getYear();

        // 原子查询当前年份的最大序列值,避免并发冲突
        String sql = "SELECT COALESCE(MAX(id.seq), 0) FROM Cars id WHERE id.year = :year";
        Query query = session.createNativeQuery(sql);
        query.setParameter("year", currentYear);
        Integer maxSeq = (Integer) query.getSingleResult();

        int nextSeq = maxSeq + 1;
        // 返回组装好的复合主键实例
        return new EmbedddedCarsKey(currentYear, nextSeq);
    }
}

2. 修改实体类与嵌入式主键

  • 在@EmbeddedId上指定自定义生成器
  • 嵌入式主键必须实现equals()和hashCode()(Hibernate需要用来识别实体)
  • 保留无参构造器(Hibernate实例化需要)
import org.hibernate.annotations.GenericGenerator;
import javax.persistence.*;
import java.io.Serializable;
import java.util.Objects;

@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
public class Cars {
    @EmbeddedId
    @GenericGenerator(
        name = "yearlySeqGenerator",
        strategy = "com.ush.mcl.model.YearlySequenceGenerator" // 替换为你的生成器全类名
    )
    @GeneratedValue(generator = "yearlySeqGenerator")
    private EmbedddedCarsKey id;

    private String name;

    // 无参构造器
    public Cars() {}

    // getters and setters
    public EmbedddedCarsKey getId() { return id; }
    public void setId(EmbedddedCarsKey id) { this.id = id; }
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
}

@Embeddable
public class EmbedddedCarsKey implements Serializable {
    private int year;
    private Integer seq;

    // 无参构造器(必须保留)
    public EmbedddedCarsKey() {}

    // 带参构造器,用于生成器组装主键
    public EmbedddedCarsKey(int year, Integer seq) {
        this.year = year;
        this.seq = seq;
    }

    // getters and setters
    public int getYear() { return year; }
    public void setYear(int year) { this.year = year; }
    public Integer getSeq() { return seq; }
    public void setSeq(Integer seq) { this.seq = seq; }

    // 重写equals和hashCode,复合主键必须实现
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        EmbedddedCarsKey that = (EmbedddedCarsKey) o;
        return year == that.year && Objects.equals(seq, that.seq);
    }

    @Override
    public int hashCode() {
        return Objects.hash(year, seq);
    }
}

3. 测试保存操作

现在你可以正常保存实体,无需手动实例化主键:

Cars cars = new Cars();
cars.setName("Honda");
carsRepository.save(cars);

注意事项

  • 并发安全:如果是高并发场景,建议在查询最大序列值时加锁(比如用SELECT ... FOR UPDATE),避免多个请求同时生成相同的序列值。
  • 数据库兼容性:上述SQL中的COALESCE是标准SQL函数,大部分数据库都支持,如果是特殊数据库可调整语法。
  • JPA标准兼容:@GenericGenerator是Hibernate的扩展注解,如果需要纯JPA实现,可以使用@GeneratedValue配合javax.persistence.GenerationType.SEQUENCE,但需要额外配置数据库序列和年度重置逻辑(比如用数据库触发器)。

内容的提问来源于stack exchange,提问作者Yvan Dupré

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.28 06:23:36