如何用@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
相关产品推荐
相关产品推荐

