如何在Hibernate/JPA中合理处理实体最后修改时间戳及审计
问题描述
在Quarkus后端中,通过REST接口接收模型并转换为实体持久化,需要记录修改用户和时间戳。当前使用entityManager.merge(parent)执行更新时,即使实体无实际变更,@PreUpdate回调仍会触发,导致父、子实体的时间戳被无意义更新。
尝试过注册Hibernate监听器检测脏属性,但仅能检测基本类型字段变更,无法识别子实体的增删操作;添加PreCollectionUpdateEventListener后遇到两个问题:
- 集合未变更时监听器仍触发;
- 监听器中调用
session.merge(parent)无效果。
现有实体类及代码
ParentEntity
@Entity(name = "ParentEntity ") @Table(name = "Parent") public class ParentEntity extends AbstractAuditableEntity { @Id @Column(name = "id", nullable = false) @GeneratedValue(strategy = AUTO) private Long id; @Column(name = "name", nullable = false) private String name = ""; @ManyToMany(targetEntity = ChildEntity.class, fetch = FetchType.EAGER, cascade = { CascadeType.MERGE, CascadeType.REFRESH }) @JoinTable(name = "Parent_Child", joinColumns = @JoinColumn(name = "parentId"), inverseJoinColumns = @JoinColumn(name = "childId")) private Set<ChildEntity> children= new HashSet<>(); }
ChildEntity
@Entity(name = "ChildEntity") @Table(name = "Child") public class ChildEntity extends AbstractAuditableEntity { @Id @Column(name = "id", nullable = false) @GeneratedValue(strategy = AUTO) private Long id; @Column(name = "name", nullable = false) private String name = ""; }
AbstractAuditableEntity
@MappedSuperclass public abstract class AbstractAuditableEntity { public static final String SYSTEM_MODIFIER = "system"; @Column(columnDefinition = "TIMESTAMP WITH TIME ZONE", name = "modified_at", nullable = true) @Temporal(TemporalType.TIMESTAMP) private Date modifiedAt = Date.from(Instant.now()); @Column(name = "modified_by") private String modifiedBy = ""; @PrePersist public void onCreate() { this.updateAuditFields(); } private void updateAuditFields() { this.modifiedAt = Date.from(Instant.now()); String username = UserContext.getCurrentUsername(); if (username == null) { this.modifiedBy = SYSTEM_MODIFIER; } else { this.modifiedBy = username; } } }
AbstractDAO
public T update(T entity) throws ViolatedConstraintException { T result = (T) this.getEntityManager().merge(entity); return result; }
现有监听器AuditablePreUpdateListener
public class AuditablePreUpdateListener implements PreUpdateEventListener, PreCollectionUpdateEventListener { @Override public boolean onPreUpdate(@Nullable PreUpdateEvent event) { Object entity = event.getEntity(); if (entity instanceof AbstractAuditableEntity) { EntityPersister persister = event.getPersister(); Object[] newState = event.getState(); Object[] oldState = event.getOldState(); String[] propertyNames = persister.getPropertyNames(); int modifiedAtIndex = Arrays.asList(propertyNames).indexOf("modifiedAt"); int modifiedByIndex = Arrays.asList(propertyNames).indexOf("modifiedBy"); int[] dirtyProperties = persister.findDirty(newState, oldState, event.getEntity(), event.getSession()); if (dirtyProperties != null && dirtyProperties.length > 0) { int[] filteredDirtyProperties = IntStream.of(dirtyProperties).filter(i -> i != modifiedAtIndex && i != modifiedByIndex).toArray(); if (filteredDirtyProperties.length > 0) { AbstractAuditableEntity auditableEntity = (AbstractAuditableEntity) entity; auditableEntity.setModifiedAt(Date.from(Instant.now())); String username = UserContext.getCurrentUsername(); auditableEntity.setModifiedBy(Objects.requireNonNullElse(username, AbstractAuditableEntity.SYSTEM_MODIFIER)); newState[modifiedAtIndex] = auditableEntity.getModifiedAt(); newState[modifiedByIndex] = auditableEntity.getModifiedBy(); } else { newState[modifiedAtIndex] = oldState[modifiedAtIndex]; newState[modifiedByIndex] = oldState[modifiedByIndex]; } } else { newState[modifiedAtIndex] = oldState[modifiedAtIndex]; newState[modifiedByIndex] = oldState[modifiedByIndex]; } } return false; } private void updateAuditFields(AbstractAuditableEntity entity) { String username = UserContext.getCurrentUsername(); entity.setModifiedBy(Objects.requireNonNullElse(username, AbstractAuditableEntity.SYSTEM_MODIFIER)); entity.setModifiedAt(Date.from(Instant.now())); } @Override public void onPreUpdateCollection(PreCollectionUpdateEvent event) { Object owner = event.getCollection().getOwner(); if (owner instanceof AbstractAuditableEntity) { updateAuditFields((AbstractAuditableEntity) owner); } } }
解决方案
1. 调整审计基类,去掉自动初始化并补全setter
修改AbstractAuditableEntity,移除modifiedAt的自动初始化(避免未持久化就生成时间戳),同时补全setter方法供监听器调用:
@MappedSuperclass public abstract class AbstractAuditableEntity { public static final String SYSTEM_MODIFIER = "system"; @Column(columnDefinition = "TIMESTAMP WITH TIME ZONE", name = "modified_at", nullable = true) @Temporal(TemporalType.TIMESTAMP) private Date modifiedAt; // 移除自动初始化 @Column(name = "modified_by") private String modifiedBy; @PrePersist public void onCreate() { updateAuditFields(); } public void updateAuditFields() { this.modifiedAt = Date.from(Instant.now()); String username = UserContext.getCurrentUsername(); this.modifiedBy = username == null ? SYSTEM_MODIFIER : username; } // 补全setter方法 public void setModifiedAt(Date modifiedAt) { this.modifiedAt = modifiedAt; } public void setModifiedBy(String modifiedBy) { this.modifiedBy = modifiedBy; } }
2. 修复监听器,精准检测集合变更
优化AuditablePreUpdateListener,在集合更新逻辑中判断集合是否真的脏了,避免无意义触发;同时完善实体脏检查逻辑:
public class AuditablePreUpdateListener implements PreUpdateEventListener, PreCollectionUpdateEventListener { @Override public boolean onPreUpdate(PreUpdateEvent event) { Object entity = event.getEntity(); if (entity instanceof AbstractAuditableEntity) { EntityPersister persister = event.getPersister(); Object[] newState = event.getState(); Object[] oldState = event.getOldState(); String[] propertyNames = persister.getPropertyNames(); int modifiedAtIndex = Arrays.asList(propertyNames).indexOf("modifiedAt"); int modifiedByIndex = Arrays.asList(propertyNames).indexOf("modifiedBy"); // 获取真正的脏属性,排除审计字段自身的变更 int[] dirtyProperties = persister.findDirty(newState, oldState, entity, event.getSession()); if (dirtyProperties == null || dirtyProperties.length == 0) { // 无业务字段变更,重置审计字段为旧值 newState[modifiedAtIndex] = oldState[modifiedAtIndex]; newState[modifiedByIndex] = oldState[modifiedByIndex]; return false; } // 过滤掉审计字段的变更记录 int[] filteredDirty = IntStream.of(dirtyProperties) .filter(i -> i != modifiedAtIndex && i != modifiedByIndex) .toArray(); if (filteredDirty.length > 0) { // 存在业务字段变更,更新审计信息 AbstractAuditableEntity auditable = (AbstractAuditableEntity) entity; auditable.updateAuditFields(); newState[modifiedAtIndex] = auditable.getModifiedAt(); newState[modifiedByIndex] = auditable.getModifiedBy(); } else { // 仅审计字段变更,重置为旧值 newState[modifiedAtIndex] = oldState[modifiedAtIndex]; newState[modifiedByIndex] = oldState[modifiedByIndex]; } } return false; } @Override public void onPreUpdateCollection(PreCollectionUpdateEvent event) { PersistentCollection collection = event.getCollection(); // 仅当集合已初始化且确实脏了,才更新所有者的审计字段 if (collection.wasInitialized() && collection.isDirty()) { Object owner = collection.getOwner(); if (owner instanceof AbstractAuditableEntity) { AbstractAuditableEntity auditable = (AbstractAuditableEntity) owner; auditable.updateAuditFields(); // 手动标记所有者为脏,确保审计字段被持久化 EntityPersister persister = event.getSession().getSessionFactory() .getMetamodel().entityPersister(owner.getClass()); persister.setPropertyValue(owner, "modifiedAt", auditable.getModifiedAt()); persister.setPropertyValue(owner, "modifiedBy", auditable.getModifiedBy()); } } } }
3. 在Quarkus中注册监听器
在application.properties中添加配置,将监听器注册到Hibernate:
quarkus.hibernate-orm.listeners.pre-update=com.yourpackage.AuditablePreUpdateListener quarkus.hibernate-orm.listeners.pre-collection-update=com.yourpackage.AuditablePreUpdateListener
4. 优化DAO的merge逻辑(可选)
修改AbstractDAO的update方法,优先复用持久化上下文的实体,仅复制有变更的字段,减少无意义的merge操作:
public T update(T entity) throws ViolatedConstraintException { EntityManager em = getEntityManager(); if (!em.contains(entity)) { // 查询库中已存在的持久化实体 T managedEntity = em.find((Class<T>) entity.getClass(), getId(entity)); if (managedEntity != null) { // 仅复制非空属性,避免覆盖原有值 BeanUtils.copyProperties(entity, managedEntity, getNullPropertyNames(entity)); return managedEntity; } } return em.merge(entity); } // 辅助方法:获取实体中为null的属性名 private String[] getNullPropertyNames(Object source) { final BeanWrapper src = new BeanWrapperImpl(source); PropertyDescriptor[] pds = src.getPropertyDescriptors(); Set<String> emptyNames = new HashSet<>(); for (PropertyDescriptor pd : pds) { Object srcValue = src.getPropertyValue(pd.getName()); if (srcValue == null) emptyNames.add(pd.getName()); } return emptyNames.toArray(new String[0]); } // 辅助方法:获取实体ID(确保所有实体都实现getId方法) private Object getId(Object entity) { try { Method getIdMethod = entity.getClass().getMethod("getId"); return getIdMethod.invoke(entity); } catch (Exception e) { throw new RuntimeException("无法获取实体ID", e); } }
关键说明
- 实体脏检查:通过Hibernate的
persister.findDirty过滤审计字段的变更,仅当业务字段真的修改时才更新时间戳。 - 集合变更判断:用
collection.isDirty()精准识别集合的增删改操作,避免无意义触发审计更新。 - 减少无效merge:优先复用持久化上下文的实体,手动复制变更字段,降低Hibernate脏检查的触发概率。
内容的提问来源于stack exchange,提问作者unrated
相关产品推荐
相关产品推荐

