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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.30 04:39:20