Auditing mit JPA, Hibernate und Spring Data JPA

1. Übersicht

Im Kontext von ORM bedeutet Datenbanküberwachung das Verfolgen und Protokollieren von Ereignissen in Bezug auf persistente Entitäten oder einfach die Versionierung von Entitäten. Inspiriert von SQL-Triggern sind die Ereignisse Einfüge-, Aktualisierungs- und Löschvorgänge für Entitäten. Die Vorteile der Datenbankprüfung sind analog zu denen der Versionskontrolle.

Wir werden drei Ansätze zur Einführung von Auditing in eine Anwendung demonstrieren. Zunächst werden wir es mit Standard-JPA implementieren. Als Nächstes werden zwei JPA-Erweiterungen betrachtet, die ihre eigenen Überwachungsfunktionen bereitstellen: eine von Hibernate und eine von Spring Data.

Hier sind die beispielbezogenen Entitäten Bar und Foo, die in diesem Beispiel verwendet werden:

2. Auditing mit JPA

JPA enthält nicht explizit eine Überwachungs-API, aber die Funktionalität kann mithilfe von Entity Lifecycle-Ereignissen erreicht werden.

2.1. @PrePersist, @PreUpdate und @PreRemove

In der JPA- Entitätsklasse kann eine Methode als Rückruf angegeben werden, der während eines bestimmten Entitätslebenszyklusereignisses aufgerufen wird. Da wir an Rückrufen interessiert sind, die vor den entsprechenden DML-Vorgängen ausgeführt werden, stehen für unsere Zwecke Rückrufanmerkungen für @PrePersist , @PreUpdate und @PreRemove zur Verfügung:

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

Interne Rückrufmethoden sollten immer void zurückgeben und keine Argumente annehmen. Sie können einen beliebigen Namen und eine beliebige Zugriffsebene haben, sollten jedoch nicht statisch sein.

Beachten Sie, dass die Annotation @Version in JPA nicht eng mit unserem Thema zusammenhängt - sie hat mehr mit optimistischem Sperren als mit Überwachungsdaten zu tun.

2.2. Rückrufmethoden implementieren

Bei diesem Ansatz gibt es jedoch eine erhebliche Einschränkung. Wie in der JPA 2-Spezifikation (JSR 317) angegeben:

Im Allgemeinen sollte die Lebenszyklusmethode einer tragbaren Anwendung keine EntityManager- oder Abfragevorgänge aufrufen , nicht auf andere Entitätsinstanzen zugreifen oder Beziehungen innerhalb desselben Persistenzkontexts ändern. Eine Lifecycle-Callback-Methode kann den Nicht-Beziehungsstatus der Entität ändern, für die sie aufgerufen wird.

In Ermangelung eines Überwachungsframeworks müssen wir das Datenbankschema und das Domänenmodell manuell verwalten. Fügen wir für unseren einfachen Anwendungsfall der Entität zwei neue Eigenschaften hinzu, da wir nur den "Nicht-Beziehungsstatus der Entität" verwalten können. Eine Operationseigenschaft speichert den Namen einer ausgeführten Operation und eine Zeitstempeleigenschaft ist für den Zeitstempel der Operation:

@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()); } }

Wenn Sie eine solche Überwachung mehreren Klassen hinzufügen müssen, können Sie den Code mit @EntityListeners zentralisieren. Zum Beispiel:

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

3. Envers im Ruhezustand

Mit Hibernate könnten wir Interceptors und EventListeners sowie Datenbank-Trigger verwenden, um die Prüfung durchzuführen . Das ORM-Framework bietet jedoch Envers, ein Modul, das die Überwachung und Versionierung persistenter Klassen implementiert.

3.1. Erste Schritte mit Envers

Um Envers einzurichten, müssen Sie die JAR Hibernate- Envers zu Ihrem Klassenpfad hinzufügen :

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

Fügen Sie dann einfach die Annotation @Audited entweder in einer @ Entity (um die gesamte Entität zu überwachen ) oder in bestimmten @ Column s (wenn Sie nur bestimmte Eigenschaften prüfen müssen) hinzu:

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

Beachten Sie, dass Bar eine Eins-zu-Viele-Beziehung zu Foo hat . In diesem Fall müssen wir entweder auch Foo prüfen, indem wir @Audited auf Foo hinzufügen oder @NotAudited auf der Eigenschaft der Beziehung in Bar festlegen :

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

3.2. Erstellen von Überwachungsprotokolltabellen

Es gibt verschiedene Möglichkeiten, Audit-Tabellen zu erstellen:

  • Stellen Sie hibernate.hbm2ddl.auto so ein , dass es erstellt , gelöscht oder aktualisiert wird , damit Envers sie automatisch erstellen kann
  • Verwenden Sie o rg.hibernate.tool.EnversSchemaGenerator , um das gesamte Datenbankschema programmgesteuert zu exportieren
  • Verwenden Sie eine Ant-Task, um entsprechende DDL-Anweisungen zu generieren
  • Verwenden Sie ein Maven-Plugin zum Generieren eines Datenbankschemas aus Ihren Zuordnungen (z. B. Juplo), um das Envers-Schema zu exportieren (funktioniert mit Hibernate 4 und höher).

Wir werden den ersten Weg gehen, da dies der einfachste ist. Beachten Sie jedoch, dass die Verwendung von hibernate.hbm2ddl.auto in der Produktion nicht sicher ist.

In unserem Fall sollten die Tabellen bar_AUD und foo_AUD (wenn Sie Foo ebenfalls als @Audited festgelegt haben ) automatisch generiert werden. Die Prüftabellen kopieren alle geprüften Felder aus der Entitätstabelle mit zwei Feldern, REVTYPE (Werte sind: "0" zum Hinzufügen, "1" zum Aktualisieren, "2" zum Entfernen einer Entität) und REV .

Außerdem wird standardmäßig eine zusätzliche Tabelle mit dem Namen REVINFO generiert, die zwei wichtige Felder enthält, REV und REVTSTMP, und den Zeitstempel jeder Revision aufzeichnet. Und wie Sie sich vorstellen können, sind bar_AUD.REV und foo_AUD.REV tatsächlich Fremdschlüssel für REVINFO.REV.

3.3. Envers konfigurieren

Sie können Envers-Eigenschaften wie jede andere Hibernate-Eigenschaft konfigurieren.

Ändern wir beispielsweise das Audit-Tabellensuffix (standardmäßig " _AUD ") in " _AUDIT_LOG ". So legen Sie den Wert der entsprechenden Eigenschaft org.hibernate.envers.audit_table_suffix fest :

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

Eine vollständige Liste der verfügbaren Eigenschaften finden Sie in der Envers-Dokumentation.

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.
  • Der Spring Data JPA-Ansatz abstrahiert die Arbeit mit JPA-Rückrufen und bietet praktische Anmerkungen für die Prüfung von Eigenschaften. Es ist auch bereit für die Integration mit Spring Security. Der Nachteil ist, dass es die gleichen Fehler des JPA-Ansatzes erbt, sodass der Löschvorgang nicht überwacht werden kann.

Die Beispiele für diesen Artikel sind in einem GitHub-Repository verfügbar.