复合主键字段混用@Id与@Generated遇插入异常,求解决方案
复合主键字段上同时使用@Id与@Generated的问题及解决方案
可以在复合主键字段上同时使用JPA的@Id和Hibernate的@Generated注解,但你遇到的问题根源在于:Hibernate处理@IdClass类型的复合主键时,会忽略@Column(insertable=false)和@Generated的配置,强制将所有主键字段包含在INSERT语句中,导致数据库默认值无法生效,最终出现字段值错位报错。
以下是几个符合你需求的可行解决方案:
解决方案1:自定义INSERT语句(推荐)
直接在实体类上使用Hibernate的@SQLInsert注解,手动编写不包含CREATED_AT的INSERT语句,让数据库自动用默认值生成该字段:
@NoArgsConstructor @Getter @Setter @IdClass(BasePK.class) @Table(name = "MY_TABLE") @Entity // 替换OTHER_FIELDS为你实际的其他字段,保持字段顺序与实体类一致 @SQLInsert(sql = "INSERT INTO MY_TABLE (ID, OTHER_FIELDS) VALUES (?, ?)") public class MyEntity implements Identifiable<String> { @Id @NotNull @GenericGenerator(name = "ulid-generator", strategy = "com.example.common.jpa.ULIDIdentifier") @GeneratedValue(generator = "ulid-generator") @Column(name = "ID", updatable = false, nullable = false, length = 26) private String id; @Id @Generated(GenerationTime.INSERT) @Column(name = "CREATED_AT", insertable = false, updatable = false, nullable = false) private Instant createdAt; // 其他字段... }
插入完成后,调用entityManager.refresh(myEntity)从数据库拉取生成的CREATED_AT值,确保实体对象中的该字段是最新的数据库值。
解决方案2:改用@EmbeddedId替代@IdClass
Hibernate对@EmbeddedId(嵌入式主键)的字段属性支持更友好,可以正常识别insertable=false和@Generated配置:
- 修改主键类为嵌入式:
@Embeddable @NoArgsConstructor @AllArgsConstructor @Getter @Setter public class BasePK implements Serializable { private String id; @Generated(GenerationTime.INSERT) @Column(name = "CREATED_AT", insertable = false, updatable = false, nullable = false) private Instant createdAt; @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof BasePK)) return false; BasePK basePK = (BasePK) o; return Objects.equals(id, basePK.id) && Objects.equals(createdAt, basePK.createdAt); } @Override public int hashCode() { return Objects.hash(id, createdAt); } }
- 修改实体类使用
@EmbeddedId:
@NoArgsConstructor @Getter @Setter @Table(name = "MY_TABLE") @Entity public class MyEntity implements Identifiable<String> { @EmbeddedId private BasePK basePK; // 为ID单独提供getter/setter,兼容原有的Identifiable接口 @GenericGenerator(name = "ulid-generator", strategy = "com.example.common.jpa.ULIDIdentifier") @GeneratedValue(generator = "ulid-generator") public String getId() { return basePK != null ? basePK.getId() : null; } public void setId(String id) { if (basePK == null) { basePK = new BasePK(); } basePK.setId(id); } // 其他字段... }
插入后同样需要调用entityManager.refresh(myEntity)获取数据库生成的CREATED_AT值。
解决方案3:自定义Hibernate事件监听器
通过监听Hibernate的插入事件,在插入前跳过CREATED_AT字段的插入,插入后从数据库同步值:
- 编写监听器类:
public class GeneratedIdListener implements PreInsertEventListener, PostInsertEventListener { @Override public boolean onPreInsert(PreInsertEvent event) { if (event.getEntity() instanceof MyEntity) { // 找到CREATED_AT字段的索引,标记为不插入 String[] propertyNames = event.getPersister().getPropertyNames(); int createdAtIdx = -1; for (int i = 0; i < propertyNames.length; i++) { if ("createdAt".equals(propertyNames[i])) { createdAtIdx = i; break; } } if (createdAtIdx != -1) { event.getState()[createdAtIdx] = null; event.getPersister().setPropertyInsertable(createdAtIdx, false); } } return false; } @Override public void onPostInsert(PostInsertEvent event) { if (event.getEntity() instanceof MyEntity) { MyEntity entity = (MyEntity) event.getEntity(); // 查询数据库获取最新的CREATED_AT值并设置回实体 EntityManager em = event.getSession().getEntityManagerFactory().createEntityManager(); MyEntity refreshed = em.find(MyEntity.class, new BasePK(entity.getId(), null)); entity.setCreatedAt(refreshed.getCreatedAt()); em.close(); } } @Override public boolean requiresPostCommitHanding(EntityPersister persister) { return false; } }
- 在Spring Boot配置中注册监听器(application.properties):
spring.jpa.properties.hibernate.ejb.event.pre-insert=com.example.GeneratedIdListener spring.jpa.properties.hibernate.ejb.event.post-insert=com.example.GeneratedIdListener
内容的提问来源于stack exchange,提问作者uzovky
相关产品推荐
相关产品推荐

