ENVERS

WHAT IS IT?

Hibernate module for Entity Auditing

ENTITY

@Entity
public class Device {
    
    @Id
    private long id;
    @Type(type = "text")
    private String configuration;

    public static Device of(long id, String configuration) { ... }

    public long getId(){ ... }
    public String getConfiguration(){ ... }

    public void setId(long id){ ... }
    public void setId(String configuration){ ... }

}

SAVE IT


final long id = 1;
transactionTemplate.execute(status -> {
    final Device device = Device.of(id, "configuration 1");
    hibernate.getCurrentSession().merge(device);
});
// save it again, it changed configuration
transactionTemplate.execute(status -> {
    final Device device = Device.of(id, "configuration 2");
    hibernate.getCurrentSession().merge(device);
});

HEY, TELL ME...

What was the previous configuration?

What was the configuration yesterday?

All the times whan a configuration change occurred?

"Dunno, LOL!"

"Dunno, LOL!"

"Dunno, LOL!"

AUDITED ENTITY

@Entity
@Audited // <--------
public class Device {
    
    @Id
    private long id;
    @Type(type = "text")
    private String configuration;

    public static Device of(long id, String configuration) { ... }

    public long getId(){ ... }
    public String getConfiguration(){ ... }

    public void setId(long id){ ... }
    public void setId(String configuration){ ... }

}

SAVE IT


final long id = 1;
transactionTemplate.execute(status -> {
    final Device device = Device.of(id, "configuration 1");
    hibernate.getCurrentSession().merge(device);
});
// save it again, it changed configuration
transactionTemplate.execute(status -> {
    final Device device = Device.of(id, "configuration 2");
    hibernate.getCurrentSession().merge(device);
});

HEY, TELL ME...

What was the previous configuration?

What was the configuration yesterday?

All the times whan a configuration change occurred?

long revision = auditReader.getCurrentRevision(Rev.class, true).getId() -2;
String configuration = auditReader.find(Device.class, id, revision).getConfiguration();
long revision = auditReader.getRevisionNumberForDate(yesterday);
String configuration = auditReader.find(Device.class, id, revision).getConfiguration();
auditReader.createQuery().forRevisionsOfEntity(Device.class, false, false)
    .add(AuditEntity.id().eq(id))
    .getResultList()
    .stream()
    .map(entityRevisionAndType -> ((Rev)entityRevisionAndType[1]).getTimestamp())
    .collect(Collectors.toList());

SOME CONCEPTS 

  • 1 revision per transaction
  • only transactions that modify audited entities create a revision
  • revision number is global, not for entity (CVS-like)
  • revisions are kept in an "audit table" for the entity (entity + change info)

WHEN USE IT?

  • need to query back in time
  • already using hibernate
  • simple queries* (at revision or for revisions)

* Can use for more complex things too, but either query time (hibernate) or complexity (sqlQueries) skyrockets.

SETUP MAVEN 

<dependency>  
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-envers</artifactId>
    <version>4.3.7.Final</version><!-- current hibernate version -->
</dependency>        

SETUP SESSIONFACTORY 

@Bean
public LocalSessionFactoryBean localSessionFactoryBean(...) {

    final Properties p = new Properties();
    // default strategy doesn't mark validity end time of a revision
    p.put("org.hibernate.envers.audit_strategy",
         "org.hibernate.envers.strategy.ValidityAuditStrategy");
    // some weird stuff happens if you delete and then recreate an entity 
    // with the same id without this set to true
    p.put("org.hibernate.envers.allow_identifier_reuse", true);
    // suffixes overrides ...
    p.put("org.hibernate.envers.audit_table_suffix", "_auditing");
    p.put("org.hibernate.envers.revision_field_name", "_revision");
    p.put("org.hibernate.envers.revision_type_field_name", "_revision_type");
    p.put("org.hibernate.envers.audit_strategy_validity_end_rev_field_name", "_revision_end");
    // we needed this, but it's usually useless to audit it.
    p.put("org.hibernate.envers.do_not_audit_optimistic_locking_field", false);
    final LocalSessionFactoryBean factoryBean = new LocalSessionFactoryBean();
    // snip...
    factoryBean.setHibernateProperties(p);
    return factoryBean;
}

More here

SETUP CUSTOM REV 

@Entity
@RevisionEntity
public class Rev {

    @Id
    @GeneratedValue
    @RevisionNumber
    private long id;

    @RevisionTimestamp
    private long timestamp;

    // snip getters, setters, hashcode, equals, toString
}

More here

CAVEATS

  • MUST use Criteria API, Envers doesn't notice entity changes when using HQL update/delete queries
  • Some defaults create weird behaviour for fairly reasonable cases (validity strategy, id reuse)
  • Default revision entity holds only number and timestamp, will be often replaced with custom one
  • Some AuditCriterions that join entities across revisions may generate an unexpected quantity of db hits with sequential subqueries
  • data is duplicated in the audit table on change

KNOWN UNKNOWNS

  • migrations
  • entity relationships

(What we know we didn't try)

THANKS

envers

By Tsukihara Caligin