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

如何用@TableGenerator实现按年按地点重置的自定义前缀ID(JPA+Hibernate)

可行的自定义ID生成方案推荐

针对你需要的「年份-产地编码-序号」格式且每年按产地重置计数器的需求,除了你尝试过的JPA注解方案,还有以下几种实用方案:

方案1:数据库触发器+自增表(单库场景最可靠)

用数据库层面的逻辑维护计数器,从根源避免应用层并发冲突,步骤如下:

1. 创建计数器表

建一张以「年份+产地编码」为联合主键的计数器表,确保每个维度的计数器独立:

CREATE TABLE PRODUCT_ID_COUNTER (
    year INT NOT NULL,
    source_code VARCHAR(10) NOT NULL,
    current_count INT DEFAULT 1,
    PRIMARY KEY (year, source_code)
);

2. 编写触发器自动生成ID

以MySQL为例,写一个插入前触发器,自动完成计数器更新和ID拼接:

DELIMITER //
CREATE TRIGGER generate_product_id BEFORE INSERT ON product
FOR EACH ROW
BEGIN
    DECLARE current_year INT;
    DECLARE next_count INT;
    SET current_year = YEAR(NOW());
    
    -- 不存在则插入初始计数器,存在则自增
    INSERT INTO PRODUCT_ID_COUNTER (year, source_code, current_count)
    VALUES (current_year, NEW.source, 1)
    ON DUPLICATE KEY UPDATE current_count = current_count + 1;
    
    -- 获取更新后的计数器值
    SELECT current_count INTO next_count FROM PRODUCT_ID_COUNTER
    WHERE year = current_year AND source_code = NEW.source;
    
    -- 拼接成指定格式的ID
    SET NEW.id = CONCAT(current_year, '-', NEW.source, '-', LPAD(next_count, 7, '0'));
END //
DELIMITER ;

之后插入产品时,只需传入source等业务字段,ID会由数据库自动生成,无需应用层额外处理。

方案2:优化JPA自定义ID生成器

如果更倾向于在JPA层处理,可以结合EntityManager操作计数器表,实现动态维度的ID生成:

1. 定义计数器实体

@Entity
@Table(name = "PRODUCT_ID_COUNTER")
public class ProductIdCounter {
    @EmbeddedId
    private ProductIdCounterId id;
    
    @Column(name = "current_count")
    private Integer currentCount;
    
    // 构造器、getter、setter省略
}

@Embeddable
public class ProductIdCounterId implements Serializable {
    private Integer year;
    private String sourceCode;
    
    // 构造器、equals、hashCode、getter、setter省略
}

2. 自定义ID生成器实现

public class ProductIdGenerator implements IdentifierGenerator {
    @Override
    public Serializable generate(SharedSessionContractImplementor session, Object obj) throws HibernateException {
        Product product = (Product) obj;
        String source = product.getSource();
        int currentYear = LocalDate.now().getYear();
        
        EntityManager em = session.getEntityManagerFactory().createEntityManager();
        try {
            em.getTransaction().begin();
            ProductIdCounterId counterId = new ProductIdCounterId(currentYear, source);
            ProductIdCounter counter = em.find(ProductIdCounter.class, counterId);
            
            int nextCount;
            if (counter == null) {
                counter = new ProductIdCounter();
                counter.setId(counterId);
                counter.setCurrentCount(1);
                nextCount = 1;
                em.persist(counter);
            } else {
                nextCount = counter.getCurrentCount() + 1;
                counter.setCurrentCount(nextCount);
                em.merge(counter);
            }
            em.getTransaction().commit();
            
            return String.format("%d-%s-%07d", currentYear, source, nextCount);
        } finally {
            em.close();
        }
    }
}

3. 产品实体上配置生成器

@Entity
public class Product {
    @Id
    @GenericGenerator(name = "productIdGen", strategy = "com.yourpackage.ProductIdGenerator")
    @GeneratedValue(generator = "productIdGen")
    private String id;
    
    private String name;
    private String description;
    private String source;
    
    // getter、setter省略
}

注意给ProductIdCounter加@Version字段启用乐观锁,避免并发场景下的计数器重复问题。

方案3:Redis自增(分布式场景适用)

如果系统是分布式部署,用Redis的原子自增操作能高效解决并发和跨节点计数问题:

1. 自定义ID生成组件

@Component
public class RedisProductIdGenerator {
    @Autowired
    private StringRedisTemplate redisTemplate;
    
    public String generateId(String source) {
        int currentYear = LocalDate.now().getYear();
        String key = String.format("product:id:counter:%d:%s", currentYear, source);
        
        // 原子自增,不存在则初始化为1
        Long nextCount = redisTemplate.opsForValue().increment(key);
        if (nextCount == null) {
            nextCount = 1L;
            redisTemplate.opsForValue().set(key, "1");
        }
        
        // 设置键过期时间为当年年底,自动实现次年重置
        LocalDate endOfYear = LocalDate.of(currentYear, 12, 31);
        long expireDays = ChronoUnit.DAYS.between(LocalDate.now(), endOfYear) + 1;
        redisTemplate.expire(key, expireDays, TimeUnit.DAYS);
        
        return String.format("%d-%s-%07d", currentYear, source, nextCount);
    }
}

2. 业务层调用生成ID

@Service
public class ProductService {
    @Autowired
    private ProductRepository productRepository;
    @Autowired
    private RedisProductIdGenerator idGenerator;
    
    public Product saveProduct(Product product) {
        String id = idGenerator.generateId(product.getSource());
        product.setId(id);
        return productRepository.save(product);
    }
}

该方案性能远高于数据库操作,天然支持分布式并发,且无需手动维护计数器重置逻辑。


内容的提问来源于stack exchange,提问作者RookieMaker

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.14 03:45:55