Mengaudit dengan JPA, Hibernate, dan Spring Data JPA

1. Ikhtisar

Dalam konteks ORM, audit database berarti pelacakan dan pencatatan peristiwa yang terkait dengan entitas persisten, atau hanya pembuatan versi entitas. Terinspirasi oleh pemicu SQL, kejadiannya adalah operasi penyisipan, pembaruan, dan penghapusan pada entitas. Manfaat audit database serupa dengan yang disediakan oleh kontrol versi sumber.

Kami akan mendemonstrasikan tiga pendekatan untuk memperkenalkan audit ke dalam aplikasi. Pertama, kami akan menerapkannya menggunakan JPA standar. Selanjutnya, kita akan melihat dua ekstensi JPA yang menyediakan fungsionalitas auditnya sendiri: satu disediakan oleh Hibernate, satu lagi oleh Spring Data.

Berikut adalah contoh entitas terkait, Bar dan Foo, yang akan digunakan dalam contoh ini:

2. Mengaudit Dengan JPA

JPA tidak secara eksplisit berisi API audit, tetapi fungsinya dapat dicapai dengan menggunakan peristiwa siklus hidup entitas.

2.1. @PrePersist, @PreUpdate dan @PreRemove

Di kelas Entitas JPA , metode dapat ditetapkan sebagai callback yang akan dipanggil selama peristiwa siklus hidup entitas tertentu. Karena kami tertarik dengan callback yang dijalankan sebelum operasi DML terkait, ada anotasi callback @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() { ... } }

Metode callback internal harus selalu mengembalikan void dan tidak menggunakan argumen. Mereka dapat memiliki nama dan tingkat akses apa pun tetapi tidak boleh statis.

Ketahuilah bahwa anotasi @Version di JPA tidak sepenuhnya terkait dengan topik kita - ini lebih berkaitan dengan penguncian yang optimis daripada dengan data audit.

2.2. Menerapkan Metode Callback

Namun, ada batasan signifikan dengan pendekatan ini. Sebagaimana tercantum dalam spesifikasi JPA 2 (JSR 317):

Secara umum, metode siklus hidup aplikasi portabel tidak boleh memanggil operasi EntityManager atau Query , mengakses instance entitas lain, atau mengubah hubungan dalam konteks persistensi yang sama. Metode callback siklus hidup dapat mengubah status non-relasi dari entitas tempat ia dipanggil.

Dengan tidak adanya kerangka kerja audit, kita harus memelihara skema database dan model domain secara manual. Untuk kasus penggunaan sederhana kita, mari tambahkan dua properti baru ke entitas, karena kita hanya dapat mengelola "status non-hubungan dari entitas". Sebuah operasi properti akan menyimpan nama dari operasi yang dilakukan dan timestamp properti 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()); } }

Jika Anda perlu menambahkan pengauditan tersebut ke beberapa kelas, Anda dapat menggunakan @EntityListeners untuk memusatkan kode. Sebagai contoh:

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

3. Hibernate Envers

Dengan Hibernate, kita dapat menggunakan Interceptors dan EventListeners serta pemicu database untuk menyelesaikan audit. Tapi kerangka kerja ORM menawarkan Envers, sebuah modul yang mengimplementasikan audit dan pembuatan versi kelas persisten.

3.1. Mulailah Dengan Envers

Untuk menyiapkan Envers, Anda perlu menambahkan JAR hibernate-envers ke jalur kelas Anda:

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

Kemudian tambahkan saja @Audited penjelasan baik pada @ Entity (untuk mengaudit seluruh entitas) atau tertentu @column s (jika Anda perlu untuk sifat tertentu Audit saja):

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

Perhatikan bahwa Bar memiliki hubungan satu-ke-banyak dengan Foo . Dalam kasus ini, kita perlu mengaudit Foo juga dengan menambahkan @Audited di Foo atau menyetel @NotAudited pada properti relasi di Bar :

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

3.2. Membuat Tabel Log Audit

Ada beberapa cara untuk membuat tabel audit:

  • atur hibernate.hbm2ddl.auto untuk membuat , membuat-jatuhkan atau memperbarui , sehingga Envers dapat membuatnya secara otomatis
  • gunakan o rg.hibernate.tool.EnversSchemaGenerator untuk mengekspor skema database lengkap secara terprogram
  • menggunakan tugas Ant untuk menghasilkan pernyataan DDL yang sesuai
  • menggunakan plugin Maven untuk membuat skema database dari pemetaan Anda (seperti Juplo) untuk mengekspor skema Envers (berfungsi dengan Hibernate 4 dan lebih tinggi)

Kami akan pergi ke rute pertama, karena ini adalah yang paling mudah, tetapi perlu diketahui bahwa menggunakan hibernate.hbm2ddl.auto tidak aman dalam produksi.

Dalam kasus kami, tabel bar_AUD dan foo_AUD (jika Anda telah menyetel Foo sebagai @Audited juga) harus dibuat secara otomatis. Tabel audit menyalin semua bidang yang diaudit dari tabel entitas dengan dua bidang, REVTYPE (nilainya adalah: "0" untuk menambahkan, "1" untuk memperbarui, "2" untuk menghapus entitas) dan REV .

Selain itu, tabel tambahan bernama REVINFO akan dibuat secara default, ini mencakup dua bidang penting, REV dan REVTSTMP dan mencatat stempel waktu dari setiap revisi. Dan seperti yang bisa Anda tebak, bar_AUD.REV dan foo_AUD.REV sebenarnya adalah kunci asing untuk REVINFO.REV.

3.3. Konfigurasi Envers

Anda dapat mengkonfigurasi properti Envers seperti properti Hibernate lainnya.

Misalnya, mari kita ubah sufiks tabel audit (yang defaultnya adalah " _AUD ") menjadi " _AUDIT_LOG ". Berikut adalah cara menyetel nilai properti org.hibernate.envers.audit_table_suffix terkait :

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

Daftar lengkap properti yang tersedia dapat ditemukan di dokumentasi 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.
  • Pendekatan Spring Data JPA abstrak bekerja dengan callback JPA dan menyediakan anotasi praktis untuk properti audit. Ini juga siap untuk diintegrasikan dengan Spring Security. Kerugiannya adalah ia mewarisi kelemahan yang sama dari pendekatan JPA, sehingga operasi penghapusan tidak dapat diaudit.

Contoh artikel ini tersedia di repositori GitHub.