Pengauditan dengan JPA, Hibernate, dan Spring Data JPA

1. Gambaran keseluruhan

Dalam konteks ORM, pengauditan pangkalan data bermaksud mengesan dan mencatat peristiwa yang berkaitan dengan entiti yang berterusan, atau hanya versi entiti. Diilhamkan oleh pencetus SQL, acara tersebut memasukkan, mengemas kini dan menghapus operasi pada entiti. Manfaat pengauditan pangkalan data serupa dengan yang diberikan oleh kawalan versi sumber.

Kami akan menunjukkan tiga pendekatan untuk memperkenalkan audit ke dalam aplikasi. Pertama, kami akan melaksanakannya menggunakan JPA standard. Seterusnya, kita akan melihat dua sambungan JPA yang menyediakan fungsi pengauditan mereka sendiri: satu disediakan oleh Hibernate, yang lain oleh Spring Data.

Berikut adalah contoh entiti yang berkaitan, Bar dan Foo, yang akan digunakan dalam contoh ini:

2. Pengauditan Dengan JPA

JPA tidak secara eksplisit mengandungi API audit, tetapi fungsi dapat dicapai dengan menggunakan peristiwa kitaran hidup entiti.

2.1. @PrePersist, @PreUpdate dan @PreRemove

Dalam kelas Entiti JPA , kaedah dapat ditentukan sebagai panggilan balik yang akan dipanggil semasa acara kitaran hidup entiti tertentu. Oleh kerana kami berminat dengan panggilan balik yang dijalankan sebelum operasi DML yang sesuai, terdapat anotasi panggilan balik @PrePersist , @PreUpdate dan @PreRemove yang tersedia untuk tujuan kami:

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

Kaedah panggilan balik dalaman harus selalu tidak sah dan tidak membantah. Mereka boleh mempunyai nama dan tahap akses apa pun tetapi tidak boleh statik.

Ketahuilah bahawa penjelasan @Version dalam JPA tidak berkaitan sepenuhnya dengan topik kami - ia berkaitan dengan penguncian optimis daripada data audit.

2.2. Melaksanakan Kaedah Panggilan Balik

Terdapat sekatan yang signifikan dengan pendekatan ini. Seperti yang dinyatakan dalam spesifikasi JPA 2 (JSR 317):

Secara umum, kaedah kitaran hayat aplikasi mudah alih tidak boleh menggunakan operasi EntityManager atau Query , mengakses contoh entiti lain, atau mengubah hubungan dalam konteks kegigihan yang sama. Kaedah panggilan balik kitaran hayat boleh mengubah keadaan bukan hubungan entiti yang digunakan.

Sekiranya tiada kerangka kerja audit, kita mesti mengekalkan skema pangkalan data dan model domain secara manual. Untuk kes penggunaan mudah kami, mari tambahkan dua sifat baru ke entiti, kerana kami hanya dapat menguruskan "keadaan bukan hubungan entiti". Properti operasi akan menyimpan nama operasi yang dilakukan dan harta cap waktu adalah untuk cap waktu operasi:

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

Sekiranya anda perlu menambahkan pengauditan sedemikian ke beberapa kelas, anda boleh menggunakan @EntityListeners untuk memusatkan kod. Sebagai contoh:

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

3. Penyabar Hibernate

Dengan Hibernate, kami dapat memanfaatkan Interceptors dan EventListeners serta pencetus pangkalan data untuk menyelesaikan pengauditan. Tetapi kerangka ORM menawarkan Envers, modul yang melaksanakan pengauditan dan versi kelas berterusan.

3.1. Bermula dengan Penukar

Untuk menyediakan Envers, anda perlu menambahkan JAR hibernate-envers ke dalam classpath anda:

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

Kemudian tambahkan anotasi @Audited sama ada pada @Entity (untuk mengaudit keseluruhan entiti) atau pada @Column tertentu (jika anda perlu mengaudit hartanah tertentu sahaja):

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

Perhatikan bahawa Bar mempunyai hubungan satu-dengan-banyak dengan Foo . Dalam kes ini, kita juga perlu mengaudit Foo dengan menambahkan @Audited on Foo atau menetapkan @NotAudited pada harta hubungan di Bar :

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

3.2. Membuat Jadual Log Audit

Terdapat beberapa cara untuk membuat jadual audit:

  • tetapkan hibernate.hbm2ddl.auto untuk membuat , membuat-drop atau mengemas kini , sehingga Envers dapat membuatnya secara automatik
  • gunakan o rg.hibernate.tool.EnversSchemaGenerator untuk mengeksport skema pangkalan data yang lengkap secara teratur
  • gunakan tugas Semut untuk menghasilkan pernyataan DDL yang sesuai
  • use a Maven plugin for generating a database schema from your mappings (such as Juplo) to export Envers schema (works with Hibernate 4 and higher)

We’ll go the first route, as it is the most straightforward, but be aware that using hibernate.hbm2ddl.auto is not safe in production.

In our case, bar_AUD and foo_AUD (if you've set Foo as @Audited as well) tables should be generated automatically. The audit tables copy all audited fields from the entity's table with two fields, REVTYPE (values are: “0” for adding, “1” for updating, “2” for removing an entity) and REV.

Besides these, an extra table named REVINFO will be generated by default, it includes two important fields, REV and REVTSTMP and records the timestamp of every revision. And as you can guess, bar_AUD.REV and foo_AUD.REV are actually foreign keys to REVINFO.REV.

3.3. Configuring Envers

You can configure Envers properties just like any other Hibernate property.

For example, let's change the audit table suffix (which defaults to “_AUD“) to “_AUDIT_LOG“. Here is how to set the value of the corresponding property org.hibernate.envers.audit_table_suffix:

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

A full listing of available properties can be found in the Envers documentation.

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.
  • Pendekatan Spring Data JPA abstrak bekerja dengan panggilan balik JPA dan memberikan anotasi yang berguna untuk harta audit. Ia juga siap untuk disatukan dengan Spring Security. Kelemahannya adalah bahawa ia mewarisi kelemahan pendekatan JPA yang sama, jadi operasi penghapusan tidak dapat diaudit.

Contoh untuk artikel ini terdapat di repositori GitHub.