11

There is application spring+jpa+envers(hibernate) envers needs to save history of entities in special table.

After I saved a few times my entity, I expected to see filled version field in USER table and filled version field in USER_AUT. But actual result is correct value in USER table, but added REV_TYPE, REV columns( in field just couter's for all rows) and null in version colums.

I use 4.0.1.Final hibernate

<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-envers</artifactId>
    <version>4.0.1.Final</version>
</dependency>
<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-entitymanager</artifactId>
    <version>4.0.1.Final</version>
</dependency>

But, when I look in table, all values in Version field are null

My entity is

import lombok.*;
import org.hibernate.annotations.GenericGenerator;
import org.hibernate.envers.Audited;

import javax.persistence.*;

@Entity
@Audited
@Table(name = "User", uniqueConstraints = {
        @UniqueConstraint(columnNames = { "prKey"})})
@NoArgsConstructor(access = AccessLevel.PUBLIC)
@AllArgsConstructor
@Getter
@Setter
public class User {
    @Id
    @GeneratedValue(generator = "uuid")
    @GenericGenerator(name = "uuid", strategy = "uuid2")
    @Column(name = "PR_KEY", unique = true)
    private String prKey;

    @Column(name = "name", length = 100, unique = false)
    private String name;

    @Version
    private int version;

    public User(String name){
        this.name = name;
    }
}

And when I get entities using audit:

 public List<User> getHistory(String id) {
        AuditReader auditReader = AuditReaderFactory.get(entityManagerFactory.createEntityManager());

        List<Number> auditVersions = auditReader.getRevisions(User.class, id);
        List<User> users = auditVersions.stream().map(item -> auditReader.find(User.class, id, item.intValue())).collect(Collectors.toList());

        return extractRiskMetrics(riskMetricRecords);
    }

So, my persistence - config is

@Configuration
@EnableTransactionManagement
@EnableJpaAuditing
@EnableJpaRepositories(basePackages = {"persistence"})
@ComponentScan(basePackages = {"persistence", "model"})
public class PersistenceConfig {
    private static final String PACKAGE_WITH_JPA_ENTITIES = "persistence";
    private final Logger log = Logger.getLogger(getClass());

    @Bean
    @Resource(type = DataSource.class, lookup = "jdbc/MyDatasource", name = "jdbc/MyDatasource")
    public DataSource dataSource() {
        final JndiDataSourceLookup dsLookup = new JndiDataSourceLookup();
        dsLookup.setResourceRef(true);
        DataSource dataSource = dsLookup.getDataSource("java:comp/env/jdbc/MyDatasource");
        return dataSource;
    }

    @Bean
    public LocalContainerEntityManagerFactoryBean entityManagerFactory() {
        LocalContainerEntityManagerFactoryBean entityManager = new LocalContainerEntityManagerFactoryBean();
        entityManager.setDataSource(dataSource());
        entityManager.setPackagesToScan(PACKAGE_WITH_JPA_ENTITIES);
        entityManager.setJpaVendorAdapter(new HibernateJpaVendorAdapter());
        entityManager.setJpaProperties(getHibernateProperties());
        log.info("Entity Manager configured.");
        return entityManager;
    }

    @Bean
    public JpaTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
        JpaTransactionManager transactionManager = new JpaTransactionManager();
        transactionManager.setEntityManagerFactory(entityManagerFactory);
        return transactionManager;
    }

    //Set properties hibernate
    private Properties getHibernateProperties() {
        Properties properties = new Properties();
        properties.put("hibernate.dialect", "org.hibernate.dialect.OracleDialect");
        properties.put("hibernate.show_sql", "true");
        properties.put("hibernate.hbm2ddl.auto", "none");
        properties.put("org.hibernate.envers.do_not_audit_optimistic_locking_field", false);

        properties.put("verifyServerCertificate", false);
        properties.put("useSSL", false);
        properties.put("requireSSL", false);
        properties.put("useLegacyDatetimeCode", false);
        properties.put("useUnicode", "yes");
        properties.put("characterEncoding", "UTF-8");
        properties.put("serverTimezone", "UTC");
        properties.put("useJDBCCompliantTimezoneShift", true);
        return properties;
    }
}

Updates:

org.hibernate.envers.do_not_audit_optimistic_locking_field set to false, but version fields still null.

May be it linked with conflict Spring Data Jpa and Hibernate - envers?

In fact, executed querie(changed and f.c.)

[1/22/19 14:04:51:996 MSK] 00000096 SystemOut     O Hibernate: update UserRecord set User=?, version=? where PR_KEY=? and version=?
[1/22/19 14:04:51:998 MSK] 00000096 SystemOut     O Hibernate: select hibernate_sequence.nextval from dual
[1/22/19 14:04:52:000 MSK] 00000096 SystemOut     O Hibernate: insert into REVINFO (REVTSTMP, REV) values (?, ?)
[1/22/19 14:04:52:002 MSK] 00000096 SystemOut     O Hibernate: insert into UserRecord_AUD (REVTYPE, busId, User, UserType, someInfo, PR_KEY, REV) values (?, ?, ?, ?, ?, ?, ?)

So, in AUD table there is no where version=?

Roberto
  • 1,288
  • 5
  • 23
  • 47
  • 1
    At which table do you look? `user` or `user_AUD` ? – Ralph Jan 18 '19 at 17:44
  • @Ralph I look in user_AUD table, because in just user table version is okey(incremented) – Roberto Jan 20 '19 at 13:37
  • Version columns should not be audited by default. Why do you want to audit Version columns? if your entity is annotated with Audited, when any field changes - there will be an audit entry in *_AUD table. You will also have an entry in REVINFO table, where the column REV will basically represent the "versions" of your entity (each "snapshot" of your audited entity is a different revision). – hovanessyan Jan 23 '19 at 09:54
  • 1
    @VladislavOsipenkov also you say in the title all Version columns are null, but then you mention that in USER table Version column is actually populated as expected, but there's problem with the Version column in the AUD table (where you actually don't need Version column). – hovanessyan Jan 23 '19 at 10:01
  • @hovanessyan thanks, updated title, problem is only in AUD table. I need in filled version in AUD, because I provide getHistrory operation and return all changes of entity with filled version column. – Roberto Jan 23 '19 at 10:13
  • 1
    You can use the revision number from REVINFO table (column REV). Version is an implementation detail of optimistic locking. The audit history presented in reports/ui to some user should not include the Version column. In the general case REV and Version should have the same values (e.g. for REV 1 the Version is 1 and so on). My point is that the REV column is the actual column to use when showing what has changed in an Entity when using Envers. – hovanessyan Jan 23 '19 at 10:18
  • @hovanessyan in my case, if I added 3 time first entity(changed value) and 2 times second entity. REV will be 5 for second entity, but must be just 2. They are not equals – Roberto Jan 23 '19 at 10:26
  • @VladislavOsipenkov yes I get your point. I've added a code sample as response to address that. Still I think you should not expose the Version field - probably expose REV as it is or remap to 1,2,3 sequence. – hovanessyan Jan 24 '19 at 08:15

2 Answers2

4

Look at configuration setting org.hibernate.envers.do_not_audit_optimistic_locking_field.

This configuration setting controls whether or not Hibernate Envers will include the @Version annotated field in the audit schema or not. By default the setting is set to true which means that the optimistic locking field won't be audited. By setting this to false, you will audit the column's value.

I do want to caution you about setting this field to false.

If your application performs explicit optimistic-locking increment functionality, this will lead to additional rows being added to the audit history table even if none of the other database columns are changed as part of your business process. This is because once you enable @Version fields to be tracked, Hibernate Envers simply treats them as any other basic attribute on the entity. Therefore, forced optimistic-lock increment will trigger an audit change.

Naros
  • 19,928
  • 3
  • 41
  • 71
  • updated issue with this parameter, but problem is still same – Roberto Jan 22 '19 at 07:18
  • Because your configuration is still using `hibernate.hbm2ddl.auto=none`. You're basically saying that if there are schema changes, ignore them. While you are developing (especially with Envers), you should leave this at least as `update` until you're comfortable with what you have and that its stable; otherwise, you will need to manually make the necessary changes. In Hibernate 6, we're introducing a new feature where you can say Envers should be `update` while ORM itself should perhaps be `none` to have more control over how schema changes get applied. – Naros Jan 24 '19 at 07:49
1

As you mentioned - REVINFO is a centralized table for all audited entities.

The basic idea presented below is to remap the revision numbers to an integer sequence - so RevNumber(2,5,31,125) will be remapped to customVersion(1,2,3,4)

Let's say you have EntityA and you want to fetch all revision s for it (and map each revision data to a custom class RevisionEntityDto).

Using AuditReader from Envers you can do something like:

AuditReader auditReader = AuditReaderFactory.get(entityManager);
//getRevisions() returns revisions sorted in ascending order (older revisions come first)
List<Number> entityARevisions = auditReader.getRevisions(EntityA.class, primaryKeyOfEntityA);

//entityARevisions is already sorted;
for (int customVersion = 0; customVersion < entityARevisions.size(); customVersion++) {
   createRevisionEntityDto(primaryKeyOfEntityA, auditReader, revision, customVersion);
}

private RevisionEntityDto createRevisionEntityDto(Long primaryKeyOfEntityA, AuditReader, Number revision) {
  EntityA revisionOfEntityA = auditReader.find(EntityA.class, primaryKey, revision);
  Date revDate = auditReader.getRevisionDate(revision);
  // at this point you have a single revision of EntityA
  return toRevisionEntityDto(revision, revisionOfEntityA, revDate);
}

private RevisionEntityDto toRevisionEntityDto(Number revision, EntityA revisionOfEntityA, Date revisionDate, int customVersion) {
  //here you do the mapping logic;
  RevisionEntityDto revEntityDto = new RevisionEntityDto();
  revEntityDto.setFieldA(revisionOfEntityA.getFieldA);
  revEntityDto.setDate(revisionDate); // you can use the date to sort if you want at a later stage;
  revEntityDto.setCustomVersion(customVersion);
  return revEntityDto;
}
hovanessyan
  • 30,580
  • 6
  • 55
  • 83