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

如何在Hibernate/JPA中合理处理实体最后修改时间戳及审计

问题描述

在Quarkus后端中,通过REST接口接收模型并转换为实体持久化,需要记录修改用户和时间戳。当前使用entityManager.merge(parent)执行更新时,即使实体无实际变更,@PreUpdate回调仍会触发,导致父、子实体的时间戳被无意义更新。

尝试过注册Hibernate监听器检测脏属性,但仅能检测基本类型字段变更,无法识别子实体的增删操作;添加PreCollectionUpdateEventListener后遇到两个问题:

  1. 集合未变更时监听器仍触发;
  2. 监听器中调用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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.12 10:45:54