如何创建含年度自增序列的复合主键?Hibernate报错排查
解决复合主键(年份+年度自增序列)生成问题
你的需求是创建一个由当前年份+每年重置的自增序列组成的复合主键,但用@EmbeddedId实现时遇到了null id generated的错误,且seq字段始终为空,这主要是因为Hibernate对嵌入式主键内的自动生成字段支持有限,原代码的序列生成策略也无法实现年度重置的需求。
问题根源分析
@EmbeddedId内的@GeneratedValue不生效:Hibernate不会主动为嵌入式主键中的字段自动生成值,即使你加了@SequenceGenerator和@GeneratedValue,也不会触发序列生成逻辑。- 全局序列无法年度重置:原代码的序列是全局递增的,无法做到每年从1开始计数,不符合你的需求。
- 手动实例化主键时
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é
相关产品推荐
相关产品推荐

