Spring Boot JPA+PostgreSQL如何实现按年重置的自定义自增标识列
JPA 标准规范里没有内置能直接实现这种「按年重置自增序列+日期前缀拼接」规则的主键生成策略,不过你用的PostgreSQL对自定义序列逻辑支持很好,完全可以不用走「查最后一条记录手动累加」的方案,把ID生成逻辑下沉到数据库侧,配合JPA自定义生成器就能实现,全程不查询业务表数据,并发安全也有保障,分布式部署场景也适用。
具体实现步骤
1. 数据库侧创建ID生成依赖的对象
所有序列判断、重置、拼接逻辑全部在数据库侧完成,从根源避免应用层并发问题。
首先创建专用自增序列:
-- 创建业务ID专用自增序列,起始值、最大值可根据业务量级调整 CREATE SEQUENCE biz_id_seq INCREMENT BY 1 MINVALUE 1 MAXVALUE 99999 START WITH 1;
然后创建单例配置表存储序列当前所属年份,避免服务重启、跨年停服导致的序列错乱:
CREATE TABLE biz_id_seq_config ( current_year integer NOT NULL PRIMARY KEY ); -- 初始化配置为当前年份 INSERT INTO biz_id_seq_config (current_year) VALUES (EXTRACT(YEAR FROM CURRENT_DATE)::integer);
最后编写自动生成ID的函数,内置跨年重置逻辑:
CREATE OR REPLACE FUNCTION generate_biz_id() RETURNS text AS $$ DECLARE curr_year integer; seq_val integer; today date := CURRENT_DATE; BEGIN -- 加排他锁避免并发插入时的序列冲突 LOCK TABLE biz_id_seq_config IN EXCLUSIVE MODE; SELECT current_year INTO curr_year FROM biz_id_seq_config; -- 检测到跨年时自动重置序列,更新配置年份 IF curr_year != EXTRACT(YEAR FROM today)::integer THEN ALTER SEQUENCE biz_id_seq RESTART WITH 1; UPDATE biz_id_seq_config SET current_year = EXTRACT(YEAR FROM today)::integer; END IF; -- 获取下一个序列值,按DDMMYYYYN格式拼接,DD、MM自动补前导零 seq_val := nextval('biz_id_seq'); RETURN to_char(today, 'DDMMYYYY') || seq_val::text; END; $$ LANGUAGE plpgsql VOLATILE;
2. JPA侧配置自定义ID生成器
JPA支持扩展自定义主键生成器,直接调用上面写好的数据库函数取ID即可,不需要额外业务逻辑。
首先编写ID生成器实现类:
import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.id.IdentifierGenerator; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; public class DatePrefixedYearlyResetIdGenerator implements IdentifierGenerator { @Override public Object generate(SharedSessionContractImplementor session, Object entity) { Connection connection = session.getJdbcConnectionAccess().obtainConnection(); try (PreparedStatement statement = connection.prepareStatement("SELECT generate_biz_id()"); ResultSet resultSet = statement.executeQuery()) { if (resultSet.next()) { return resultSet.getString(1); } throw new RuntimeException("业务ID生成失败,数据库未返回结果"); } catch (SQLException e) { throw new RuntimeException("业务ID生成异常", e); } finally { try { session.getJdbcConnectionAccess().releaseConnection(connection); } catch (SQLException ignored) {} } } }
在实体类上引用这个生成器即可:
import jakarta.persistence.*; import org.hibernate.annotations.GenericGenerator; @Entity @Table(name = "your_business_table") public class YourBusinessEntity { @Id @GeneratedValue(generator = "datePrefixedIdGen") @GenericGenerator(name = "datePrefixedIdGen", type = DatePrefixedYearlyResetIdGenerator.class) @Column(name = "biz_id", length = 20, nullable = false, unique = true) private String bizId; // 其余实体字段、构造方法、getter/setter省略 }
注意事项
- 不要在应用层用
@PrePersist等生命周期钩子做ID拼接、序列累加逻辑,分布式部署下没有全局锁会出现ID重复问题 - 如果需要自增序列N固定长度(比如固定4位,不足补前导零),只需要修改ID生成函数的拼接逻辑,把
seq_val::text替换为lpad(seq_val::text, 4, '0')即可,长度根据业务单日最大插入量调整 - 如果业务插入并发达到每秒千次以上,可以把单序列逻辑调整为按日期分桶的优化实现,绝大多数常规业务场景下当前实现性能足够
- 字段长度预留足够空间,避免序列位数增长后ID长度超出字段限制
内容的提问来源于stack exchange,提问作者fbm fatma
相关产品推荐
相关产品推荐

