Hibernate envers 在没有更改时创建记录

Hibernate envers creating a record when no changes

我一直在寻找一种方法,让 envers 不记录自上次记录以来没有修改的任何我合并的实体。 原来这应该是Envers的正常行为(没有修改就没有审核)

实体只有 @Audited 注释,但即使自上次审计以来没有变化,它们也会继续接受审计。 这是我的配置 persitence.xml:

<property name="org.hibernate.envers.revision_field_name" value="revision" />
<property name="org.hibernate.envers.revision_type_field_name" value="revision_type" />
<property name="org.hibernate.envers.revision_on_collection_change" value="false"/>
<property name="org.hibernate.envers.store_data_at_delete" value="true"/>

我找到了 Hibernate Envers: Auditing an object, calling merge on it, gives an audit record EVERY time even with no change? 但没有答案。

我的一些 equals()/hascode() 方法仅测试 ID(主键),但我没有找到任何关于这之间如何关联的话题。

我也看到有一个新参数可以查看哪个字段发生了变化,但我认为这也与我的问题无关。

如果重要的话,我正在使用 Postgresql。

对此行为有什么想法吗?我目前唯一的解决方案是通过 entityManager 获取实体并比较它们(如果涉及到这个,我将使用一些基于反射的 API)。

问题不是来自应用程序,而是来自代码本身。我们的实体有一个字段 "lastUpdateDate",它在每个 merge() 的当前日期设置。合并后由 envers 进行比较,因此此字段自上次修订以来已发生变化。

对于那些好奇的人,版本之间的变化在 org.hibernate.envers.internal.entities.mapper.MultiPropertyMapper.map() 中评估(至少在 evers 4.3.5.Final 中)returns 如果 [=] 之间有任何变化则为真13=] 和 newState。它使用特定的映射器,具体取决于 属性 比较。


编辑:我将在此处说明我是如何解决问题的,但也可以使用 Dagmar 的解决方案。然而,我的可能有点棘手和肮脏。

我按照 The official documentation and various SO answers 中的描述使用了 Envers 的 EnversPostUpdateEventListenerImpl:我创建了我的并强迫 Envers 使用它。

@Override
public void onPostUpdate(PostUpdateEvent event) {
    //Maybe you should try catch that !
    if ( event.getOldState() != null ) {
        final EntityPersister entityPersister = event.getPersister();
        final String[] propertiesNames = entityPersister.getPropertyNames();

        for ( int i = 0; i < propertiesNames.length; ++i ) {
            String propertyName = propertiesNames[i];
            if(checkProperty(propertyName){
                event.getOldState()[i] = event.getState()[i];
        }
    }
    // Normal Envers processing
    super.onPostUpdate(event);
}

我的 checkProperty(String propertyName) 刚刚检查了它是否是更新日期 属性(propertyName.endsWith("lastUpdateDate") 因为它们在我们的应用程序中就是这样)。诀窍是,我将旧状态设置为新状态,所以如果这是我实体中唯一修改的字段,它不会审核它(用 envers 保存它)。但是,如果有其他字段被修改,Envers 将使用这些修改的字段和正确的 lastUpdateDate 审计实体。

我也遇到了一个问题,oldState 是未设置 hh:mm:ss 的时间(只有零),而新状态是同一天设置的时间。所以我使用了类似的技巧:

Date oldDtEffet = (Date) event.getOldState()[i];
Date newDtEffet = (Date) event.getState()[i];
if(oldDtEffet != null && newDtEffet != null &&
        DateUtils.isDateEqualsWithoutTime(oldDtEffet,newDtEffet)){
    event.getOldState()[i] = event.getState()[i];
}

(注意:您必须重新实现所有事件侦听器,即使它们只是继承 Envers 类,没有转机。请确保 org.hibernate.integrator.spi.Integrator 在您的应用程序中)

好消息是 Hibernate Envers 按预期工作 - 除非修改可审核的 属性,否则不会创建版本(AUD 表中的条目)。

但是,在我们的应用程序中,我们实现了一个 MergeEventListener,它在每个实体保存时更新跟踪字段(lastUpdated、lastUpdatedBy)。这导致 Envers 制作了一个新版本,即使实体没有任何变化。

最终解决方案非常简单(对我们而言)- 使用一个示例说明如何使用 Hibernate 中的拦截器和事件:http://docs.jboss.org/hibernate/core/3.6/reference/en-US/html/events.html

我们将 class 实现 PersistEventListenerMergeEventListener 替换为扩展 EmptyInterceptor 并覆盖 onFlushDirty 和 onSave 方法的 class。

public class EntitySaveInterceptor extends EmptyInterceptor {

  @Override
  public boolean onFlushDirty(Object entity, Serializable id, Object[] currentState, Object[] previousState, String[] propertyNames, Type[] types) {
    setModificationTrackerProperties(entity);
    return super.onFlushDirty(entity, id, currentState, previousState, propertyNames, types);
  }

  @Override
  public boolean onSave(Object entity, Serializable id, Object[] state, String[] propertyNames, Type[] types) {
    setModificationTrackerProperties(entity);
    return super.onSave(entity, id, state, propertyNames, types);
  }

  private void setModificationTrackerProperties(Object object) {
    if (SecurityContextHolder.getContext() != null && SecurityContextHolder.getContext().getAuthentication() != null) {
      Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
      if (principal != null && principal instanceof MyApplicationUserDetails) {
        User user = ((MyApplicationUserDetails) principal).getUser();
        if (object instanceof ModificationTracker && user != null) {
          ModificationTracker entity = (ModificationTracker) object;
          Date currentDateTime = new Date();
          if (entity.getCreatedDate() == null) {
            entity.setCreatedDate(currentDateTime);
          }
          if (entity.getCreatedBy() == null) {
            entity.setCreatedBy(user);
          }
          entity.setLastUpdated(currentDateTime);
          entity.setLastUpdatedBy(user);
        }
      }
    }
  }
}

将 EntitySaveInterceptor 连接到 Hibernate JPA 持久性单元

<?xml version="1.0" encoding="UTF-8"?>
<persistence xmlns="http://java.sun.com/xml/ns/persistence"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.0"
             xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd">

    <persistence-unit name="myapplication" transaction-type="RESOURCE_LOCAL">
        <provider>org.hibernate.ejb.HibernatePersistence</provider>
        <properties>
            <property name="hibernate.ejb.interceptor" value="org.myapplication.interceptor.EntitySaveInterceptor" />           
            <property name="hibernate.hbm2ddl.auto" value="none"/>
            <property name="hibernate.show_sql" value="false"/>
        </properties>
    </persistence-unit>

</persistence>

为了完整起见,这里是 ModificationTracker 接口:

public interface ModificationTracker {

  public Date getLastUpdated();

  public Date getCreatedDate();

  public User getCreatedBy();

  public User getLastUpdatedBy();

  public void setLastUpdated(Date lastUpdated);

  public void setCreatedDate(Date createdDate);

  public void setCreatedBy(User createdBy);

  public void setLastUpdatedBy(User lastUpdatedBy);
}

也应该可以通过使用 PreUpdateEventListener 的实现来设置 ModificationTracker 值来解决这个问题,因为该侦听器也仅在对象变脏时才会触发。

我也遇到过类似的情况。

我发现审计表中出现重复行的原因是在审计实体中使用了 LocalDateTime 字段。

LocalDateTime 字段保存到 MySQL 数据库中的 DATETIME 字段。问题在于 DATETIME 字段的精度为 1 秒,而 LocalDateTime 的精度要高得多,因此当 Envers 将数据库中的数据与对象进行比较时,它会看到差异,甚至 LocalDateTime 字段也没有更改。

我通过将 LocalDateTime 字段截断为秒来解决这个问题。