Auditing con JPA, Hibernate e Spring Data JPA

1. Panoramica

Nel contesto di ORM, controllo del database significa tracciamento e registrazione di eventi relativi a entità persistenti o semplicemente controllo delle versioni di entità. Ispirati dai trigger SQL, gli eventi sono operazioni di inserimento, aggiornamento ed eliminazione sulle entità. I vantaggi del controllo del database sono analoghi a quelli forniti dal controllo della versione del codice sorgente.

Mostreremo tre approcci per introdurre l'auditing in un'applicazione. Innanzitutto, lo implementeremo utilizzando lo standard JPA. Successivamente, esamineremo due estensioni JPA che forniscono la propria funzionalità di controllo: una fornita da Hibernate, un'altra da Spring Data.

Ecco le entità correlate di esempio, Bar e Foo, che verranno utilizzate in questo esempio:

2. Auditing con JPA

JPA non contiene esplicitamente un'API di controllo, ma la funzionalità può essere ottenuta utilizzando gli eventi del ciclo di vita dell'entità.

2.1. @PrePersist, @PreUpdate e @PreRemove

Nella classe Entità JPA , è possibile specificare un metodo come callback che verrà richiamato durante un particolare evento del ciclo di vita dell'entità. Poiché siamo interessati ai callback eseguiti prima delle corrispondenti operazioni DML, sono disponibili annotazioni di callback @PrePersist , @PreUpdate e @PreRemove per i nostri scopi:

@Entity public class Bar { @PrePersist public void onPrePersist() { ... } @PreUpdate public void onPreUpdate() { ... } @PreRemove public void onPreRemove() { ... } }

I metodi di callback interni dovrebbero sempre restituire void e non accettare argomenti. Possono avere qualsiasi nome e qualsiasi livello di accesso ma non devono essere statici.

Tieni presente che l' annotazione @Version in JPA non è strettamente correlata al nostro argomento: ha a che fare con il blocco ottimistico più che con i dati di controllo.

2.2. Implementazione dei metodi di callback

Tuttavia, esiste una limitazione significativa con questo approccio. Come indicato nella specifica JPA 2 (JSR 317):

In generale, il metodo del ciclo di vita di un'applicazione portatile non deve richiamare operazioni EntityManager o Query , accedere ad altre istanze di entità o modificare le relazioni all'interno dello stesso contesto di persistenza. Un metodo di callback del ciclo di vita può modificare lo stato di non relazione dell'entità su cui viene richiamato.

In assenza di un framework di controllo, è necessario mantenere manualmente lo schema del database e il modello di dominio. Per il nostro semplice caso d'uso, aggiungiamo due nuove proprietà all'entità, poiché possiamo gestire solo lo "stato di non relazione dell'entità". Una proprietà dell'operazione memorizzerà il nome di un'operazione eseguita e una proprietà timestamp è per il timestamp dell'operazione:

@Entity public class Bar { //... @Column(name = "operation") private String operation; @Column(name = "timestamp") private long timestamp; //... // standard setters and getters for the new properties //... @PrePersist public void onPrePersist() { audit("INSERT"); } @PreUpdate public void onPreUpdate() { audit("UPDATE"); } @PreRemove public void onPreRemove() { audit("DELETE"); } private void audit(String operation) { setOperation(operation); setTimestamp((new Date()).getTime()); } }

Se è necessario aggiungere tale controllo a più classi, è possibile utilizzare @EntityListeners per centralizzare il codice. Per esempio:

@EntityListeners(AuditListener.class) @Entity public class Bar { ... }
public class AuditListener { @PrePersist @PreUpdate @PreRemove private void beforeAnyOperation(Object object) { ... } }

3. Hibernate Envers

Con Hibernate, potremmo fare uso di Interceptor ed EventListeners , nonché di trigger di database per eseguire il controllo. Ma il framework ORM offre Envers, un modulo che implementa il controllo e il controllo delle versioni delle classi persistenti.

3.1. Inizia con Envers

Per configurare Envers, è necessario aggiungere il JAR hibernate-envers nel proprio classpath:

 org.hibernate hibernate-envers ${hibernate.version} 

Quindi aggiungi l' annotazione @Audited su una @Entity (per controllare l'intera entità) o su @Column s specifiche (se devi controllare solo proprietà specifiche):

@Entity @Audited public class Bar { ... }

Nota che Bar ha una relazione uno-a-molti con Foo . In questo caso, dobbiamo controllare anche Foo aggiungendo @Audited su Foo o impostare @NotAudited sulla proprietà della relazione in Bar :

@OneToMany(mappedBy = "bar") @NotAudited private Set fooSet;

3.2. Creazione di tabelle del registro di controllo

Esistono diversi modi per creare tabelle di controllo:

  • impostare hibernate.hbm2ddl.auto per creare , creare-rilasciare o aggiornare , in modo che Envers possa crearli automaticamente
  • utilizzare o rg.hibernate.tool.EnversSchemaGenerator per esportare lo schema del database completo a livello di programmazione
  • utilizzare un'attività Ant per generare istruzioni DDL appropriate
  • usa un plugin Maven per generare uno schema di database dalle tue mappature (come Juplo) per esportare lo schema Envers (funziona con Hibernate 4 e versioni successive)

Seguiremo il primo percorso, poiché è il più semplice, ma tieni presente che l'uso di hibernate.hbm2ddl.auto non è sicuro in produzione.

Nel nostro caso, le tabelle bar_AUD e foo_AUD (se hai impostato anche Foo come @Audited ) dovrebbero essere generate automaticamente. Le tabelle di audit copiano tutti i campi controllati dalla tabella dell'entità con due campi, REVTYPE (i valori sono: "0" per l'aggiunta, "1" per l'aggiornamento, "2" per la rimozione di un'entità) e REV .

Oltre a questi, per impostazione predefinita verrà generata una tabella aggiuntiva denominata REVINFO , che include due campi importanti, REV e REVTSTMP e registra il timestamp di ogni revisione. E come puoi immaginare, bar_AUD.REV e foo_AUD.REV sono in realtà chiavi esterne a REVINFO.REV.

3.3. Configurazione di Envers

È possibile configurare le proprietà di Envers proprio come qualsiasi altra proprietà di Hibernate.

Ad esempio, cambiamo il suffisso della tabella di controllo (che per impostazione predefinita è " _AUD ") in " _AUDIT_LOG ". Ecco come impostare il valore della proprietà corrispondente org.hibernate.envers.audit_table_suffix :

Properties hibernateProperties = new Properties(); hibernateProperties.setProperty( "org.hibernate.envers.audit_table_suffix", "_AUDIT_LOG"); sessionFactory.setHibernateProperties(hibernateProperties);

Un elenco completo delle proprietà disponibili può essere trovato nella documentazione di Envers.

3.4. Accessing Entity History

You can query for historic data in a way similar to querying data via theHibernate criteria API. The audit history of an entity can be accessed using the AuditReader interface, which can be obtained with an open EntityManager or Session via the AuditReaderFactory:

AuditReader reader = AuditReaderFactory.get(session);

Envers provides AuditQueryCreator (returned by AuditReader.createQuery()) in order to create audit-specific queries. The following line will return all Bar instances modified at revision #2 (where bar_AUDIT_LOG.REV = 2):

AuditQuery query = reader.createQuery() .forEntitiesAtRevision(Bar.class, 2)

Here is how to query for Bar‘s revisions, i.e. it will result in getting a list of all Bar instances in all their states that were audited:

AuditQuery query = reader.createQuery() .forRevisionsOfEntity(Bar.class, true, true);

If the second parameter is false the result is joined with the REVINFO table, otherwise, only entity instances are returned. The last parameter specifies whether to return deleted Bar instances.

Then you can specify constraints using the AuditEntity factory class:

query.addOrder(AuditEntity.revisionNumber().desc());

4. Spring Data JPA

Spring Data JPA is a framework that extends JPA by adding an extra layer of abstraction on the top of the JPA provider. This layer allows for support for creating JPA repositories by extending Spring JPA repository interfaces.

For our purposes, you can extend CrudRepository, the interface for generic CRUD operations. As soon as you've created and injected your repository to another component, Spring Data will provide the implementation automatically and you're ready to add auditing functionality.

4.1. Enabling JPA Auditing

To start, we want to enable auditing via annotation configuration. In order to do that, just add @EnableJpaAuditing on your @Configuration class:

@Configuration @EnableTransactionManagement @EnableJpaRepositories @EnableJpaAuditing public class PersistenceConfig { ... }

4.2. Adding Spring's Entity Callback Listener

As we already know, JPA provides the @EntityListeners annotation to specify callback listener classes. Spring Data provides its own JPA entity listener class: AuditingEntityListener. So let's specify the listener for the Bar entity:

@Entity @EntityListeners(AuditingEntityListener.class) public class Bar { ... }

Now auditing information will be captured by the listener on persisting and updating the Bar entity.

4.3. Tracking Created and Last Modified Dates

Next, we will add two new properties for storing the created and last modified dates to our Bar entity. The properties are annotated by the @CreatedDate and @LastModifiedDate annotations accordingly, and their values are set automatically:

@Entity @EntityListeners(AuditingEntityListener.class) public class Bar { //... @Column(name = "created_date", nullable = false, updatable = false) @CreatedDate private long createdDate; @Column(name = "modified_date") @LastModifiedDate private long modifiedDate; //... }

Generally, you would move the properties to a base class (annotated by @MappedSuperClass) which would be extended by all your audited entities. In our example, we add them directly to Bar for the sake of simplicity.

4.4. Auditing the Author of Changes With Spring Security

If your app uses Spring Security, you can not only track when changes were made but also who made them:

@Entity @EntityListeners(AuditingEntityListener.class) public class Bar { //... @Column(name = "created_by") @CreatedBy private String createdBy; @Column(name = "modified_by") @LastModifiedBy private String modifiedBy; //... }

The columns annotated with @CreatedBy and @LastModifiedBy are populated with the name of the principal that created or last modified the entity. The information is pulled from SecurityContext‘s Authentication instance. If you want to customize values that are set to the annotated fields, you can implement AuditorAware interface:

public class AuditorAwareImpl implements AuditorAware { @Override public String getCurrentAuditor() { // your custom logic } }

In order to configure the app to use AuditorAwareImpl to look up the current principal, declare a bean of AuditorAware type initialized with an instance of AuditorAwareImpl and specify the bean's name as the auditorAwareRef parameter's value in @EnableJpaAuditing:

@EnableJpaAuditing(auditorAwareRef="auditorProvider") public class PersistenceConfig { //... @Bean AuditorAware auditorProvider() { return new AuditorAwareImpl(); } //... }

5. Conclusion

We have considered three approaches to implementing auditing functionality:

  • The pure JPA approach is the most basic and consists of using lifecycle callbacks. However, you are only allowed to modify the non-relationship state of an entity. This makes the @PreRemove callback useless for our purposes, as any settings you've made in the method will be deleted then along with the entity.
  • Envers is a mature auditing module provided by Hibernate. It is highly configurable and lacks the flaws of the pure JPA implementation. Thus, it allows us to audit the delete operation, as it logs into tables other than the entity's table.
  • L'approccio Spring Data JPA riassume il lavoro con i callback JPA e fornisce utili annotazioni per le proprietà di controllo. È anche pronto per l'integrazione con Spring Security. Lo svantaggio è che eredita gli stessi difetti dell'approccio JPA, quindi l'operazione di eliminazione non può essere controllata.

Gli esempi per questo articolo sono disponibili in un repository GitHub.