Menggunakan JaVers untuk Pengauditan Model Data di Spring Data

1. Ikhtisar

Dalam tutorial ini, kita akan melihat cara mengatur dan menggunakan JaVers dalam aplikasi Spring Boot sederhana untuk melacak perubahan entitas.

2. JaVers

Saat berhadapan dengan data yang bisa berubah, kami biasanya hanya memiliki status terakhir dari entitas yang disimpan dalam database. Sebagai pengembang, kami menghabiskan banyak waktu untuk men-debug aplikasi, mencari melalui file log untuk peristiwa yang mengubah status. Ini semakin rumit dalam lingkungan produksi ketika banyak pengguna yang berbeda menggunakan sistem.

Untungnya, kami memiliki alat hebat seperti JaVers. JaVers adalah kerangka kerja log audit yang membantu melacak perubahan entitas dalam aplikasi.

Penggunaan alat ini tidak terbatas pada debugging dan audit saja. Ini dapat berhasil diterapkan untuk melakukan analisis, memaksa kebijakan keamanan dan memelihara log peristiwa juga.

3. Pengaturan Proyek

Pertama-tama, untuk mulai menggunakan JaVers, kita perlu mengkonfigurasi repositori audit untuk snapshot entitas yang ada. Kedua, kita perlu menyesuaikan beberapa properti JaVers yang dapat dikonfigurasi. Terakhir, kami juga akan membahas cara mengonfigurasi model domain kami dengan benar.

Namun, perlu disebutkan bahwa JaVers menyediakan opsi konfigurasi default, jadi kami dapat mulai menggunakannya dengan hampir tanpa konfigurasi.

3.1. Dependensi

Pertama, kita perlu menambahkan ketergantungan starter JaVers Spring Boot ke proyek kita. Bergantung pada jenis penyimpanan persistensi, kami memiliki dua opsi: org.javers: javers-spring-boot-starter-sql dan org.javers: javers-spring-boot-starter-mongo . Dalam tutorial ini, kita akan menggunakan starter Spring Boot SQL.

 org.javers javers-spring-boot-starter-sql 5.6.3 

Karena kita akan menggunakan database H2, mari sertakan juga dependensi ini:

 com.h2database h2 

3.2. Pengaturan Repositori JaVers

JaVers menggunakan abstraksi repositori untuk menyimpan komit dan entitas serial. Semua data disimpan dalam format JSON. Oleh karena itu, mungkin cocok untuk menggunakan penyimpanan NoSQL. Namun, demi kesederhanaan, kami akan menggunakan instance H2 dalam memori.

Secara default, JaVers memanfaatkan implementasi repositori dalam memori, dan jika kita menggunakan Spring Boot, tidak perlu konfigurasi tambahan. Selanjutnya, saat menggunakan Starter Spring Data, JaVers menggunakan kembali konfigurasi database untuk aplikasi tersebut .

JaVers menyediakan dua permulaan untuk tumpukan persistensi SQL dan Mongo. Mereka kompatibel dengan Spring Data dan tidak memerlukan konfigurasi tambahan secara default. Namun, kami selalu dapat mengganti kacang konfigurasi default: JaversSqlAutoConfiguration.java dan JaversMongoAutoConfiguration.java masing-masing.

3.3. Properti JaVers

JaVers memungkinkan konfigurasi beberapa opsi, meskipun default Spring Boot sudah cukup dalam banyak kasus penggunaan.

Mari kita timpa hanya satu, newObjectSnapshot , sehingga kita bisa mendapatkan snapshot dari objek yang baru dibuat:

javers.newObjectSnapshot=true 

3.4. Konfigurasi Domain JaVers

JaVers secara internal mendefinisikan jenis berikut: Entitas, Objek Nilai, Nilai, Wadah, dan Primitif. Beberapa istilah ini berasal dari terminologi DDD (Domain Driven Design).

Tujuan utama memiliki beberapa tipe adalah untuk menyediakan algoritma diff yang berbeda tergantung pada tipenya . Setiap jenis memiliki strategi perbedaan yang sesuai. Akibatnya, jika kelas aplikasi dikonfigurasi dengan tidak benar, kita akan mendapatkan hasil yang tidak dapat diprediksi.

Untuk memberi tahu JaVers tipe apa yang akan digunakan untuk kelas, kami memiliki beberapa opsi:

  • Secara eksplisit - opsi pertama adalah secara eksplisit menggunakan metode register * dari kelas JaversBuilder - cara kedua adalah menggunakan anotasi
  • Secara implisit - JaVers menyediakan algoritme untuk mendeteksi tipe secara otomatis berdasarkan hubungan kelas
  • Default - secara default, JaVers akan memperlakukan semua kelas sebagai ValueObjects

Dalam tutorial ini, kami akan mengonfigurasi JaVers secara eksplisit, menggunakan metode anotasi.

Hebatnya, JaVers kompatibel dengan anotasi javax.persistence . Akibatnya, kami tidak perlu menggunakan anotasi khusus JaVers di entitas kami.

4. Contoh Proyek

Sekarang kita akan membuat aplikasi sederhana yang akan menyertakan beberapa entitas domain yang akan kita audit.

4.1. Model Domain

Domain kami akan menyertakan toko dengan produk.

Mari tentukan entitas Store :

@Entity public class Store { @Id @GeneratedValue private int id; private String name; @Embedded private Address address; @OneToMany( mappedBy = "store", cascade = CascadeType.ALL, orphanRemoval = true ) private List products = new ArrayList(); // constructors, getters, setters }

Harap perhatikan bahwa kami menggunakan anotasi JPA default. JaVers memetakannya dengan cara berikut:

  • @ javax.persistence.Entity dipetakan ke @ org.javers.core.metamodel.annotation.Entity
  • @ javax.persistence.Embeddable dipetakan ke @ org.javers.core.metamodel.annotation.ValueObject.

Kelas yang dapat disematkan didefinisikan dengan cara biasa:

@Embeddable public class Address { private String address; private Integer zipCode; }

4.2. Repositori Data

Untuk mengaudit repositori JPA, JaVers menyediakan anotasi @JaversSpringDataAuditable .

Mari kita definisikan StoreRepository dengan anotasi itu:

@JaversSpringDataAuditable public interface StoreRepository extends CrudRepository { }

Selain itu, kita akan memiliki ProductRepository , tetapi tidak memiliki anotasi:

public interface ProductRepository extends CrudRepository { }

Sekarang pertimbangkan kasus ketika kami tidak menggunakan repositori Spring Data. JaVers memiliki anotasi tingkat metode lain untuk tujuan itu: @JaversAuditable.

Misalnya, kita dapat mendefinisikan metode untuk mempertahankan produk sebagai berikut:

@JaversAuditable public void saveProduct(Product product) { // save object }

Atau, kami bahkan dapat menambahkan penjelasan ini langsung di atas metode di antarmuka repositori:

public interface ProductRepository extends CrudRepository { @Override @JaversAuditable  S save(S s); }

4.3. Penyedia Penulis

Setiap perubahan yang dilakukan di JaVers harus memiliki penulisnya. Selain itu, JaVers mendukung Keamanan Musim Semi di luar kotak.

As a result, each commit is made by a specific authenticated user. However, for this tutorial we'll create a really simple custom implementation of the AuthorProvider Interface:

private static class SimpleAuthorProvider implements AuthorProvider { @Override public String provide() { return "Baeldung Author"; } }

And as the last step, to make JaVers use our custom implementation, we need to override the default configuration bean:

@Bean public AuthorProvider provideJaversAuthor() { return new SimpleAuthorProvider(); }

5. JaVers Audit

Finally, we are ready to audit our application. We’ll use a simple controller for dispatching changes into our application and retrieving the JaVers commit log. Alternatively, we can also access the H2 console to see the internal structure of our database:

To have some initial sample data, let’s use an EventListener to populate our database with some products:

@EventListener public void appReady(ApplicationReadyEvent event) { Store store = new Store("Baeldung store", new Address("Some street", 22222)); for (int i = 1; i < 3; i++) { Product product = new Product("Product #" + i, 100 * i); store.addProduct(product); } storeRepository.save(store); }

5.1. Initial Commit

When an object is created, JaVers first makes a commit of the INITIAL type.

Let’s check the snapshots after the application startup:

@GetMapping("/stores/snapshots") public String getStoresSnapshots() { QueryBuilder jqlQuery = QueryBuilder.byClass(Store.class); List snapshots = javers.findSnapshots(jqlQuery.build()); return javers.getJsonConverter().toJson(snapshots); }

In the code above, we're querying JaVers for snapshots for the Store class. If we make a request to this endpoint we’ll get a result like the one below:

[ { "commitMetadata": { "author": "Baeldung Author", "properties": [], "commitDate": "2019-08-26T07:04:06.776", "commitDateInstant": "2019-08-26T04:04:06.776Z", "id": 1.00 }, "globalId": { "entity": "com.baeldung.springjavers.domain.Store", "cdoId": 1 }, "state": { "address": { "valueObject": "com.baeldung.springjavers.domain.Address", "ownerId": { "entity": "com.baeldung.springjavers.domain.Store", "cdoId": 1 }, "fragment": "address" }, "name": "Baeldung store", "id": 1, "products": [ { "entity": "com.baeldung.springjavers.domain.Product", "cdoId": 2 }, { "entity": "com.baeldung.springjavers.domain.Product", "cdoId": 3 } ] }, "changedProperties": [ "address", "name", "id", "products" ], "type": "INITIAL", "version": 1 } ]

Note that the snapshot above includes all products added to the store despite the missing annotation for the ProductRepository interface.

By default, JaVers will audit all related models of an aggregate root if they are persisted along with the parent.

We can tell JaVers to ignore specific classes by using the DiffIgnore annotation.

For instance, we may annotate the products field with the annotation in the Store entity:

@DiffIgnore private List products = new ArrayList();

Consequently, JaVers won’t track changes of products originated from the Store entity.

5.2. Update Commit

The next type of commit is the UPDATE commit. This is the most valuable commit type as it represents changes of an object's state.

Let’s define a method that will update the store entity and all products in the store:

public void rebrandStore(int storeId, String updatedName) { Optional storeOpt = storeRepository.findById(storeId); storeOpt.ifPresent(store -> { store.setName(updatedName); store.getProducts().forEach(product -> { product.setNamePrefix(updatedName); }); storeRepository.save(store); }); }

If we run this method we'll get the following line in the debug output (in case of the same products and stores count):

11:29:35.439 [http-nio-8080-exec-2] INFO org.javers.core.Javers - Commit(id:2.0, snapshots:3, author:Baeldung Author, changes - ValueChange:3), done in 48 millis (diff:43, persist:5)

Since JaVers has persisted changes successfully, let’s query the snapshots for products:

@GetMapping("/products/snapshots") public String getProductSnapshots() { QueryBuilder jqlQuery = QueryBuilder.byClass(Product.class); List snapshots = javers.findSnapshots(jqlQuery.build()); return javers.getJsonConverter().toJson(snapshots); }

We'll get previous INITIAL commits and new UPDATE commits:

 { "commitMetadata": { "author": "Baeldung Author", "properties": [], "commitDate": "2019-08-26T12:55:20.197", "commitDateInstant": "2019-08-26T09:55:20.197Z", "id": 2.00 }, "globalId": { "entity": "com.baeldung.springjavers.domain.Product", "cdoId": 3 }, "state": { "price": 200.0, "name": "NewProduct #2", "id": 3, "store": { "entity": "com.baeldung.springjavers.domain.Store", "cdoId": 1 } } }

Here, we can see all the information about the change we made.

It is worth noting that JaVers doesn’t create new connections to the database. Instead, it reuses existing connections. JaVers data is committed or rolled back along with application data in the same transaction.

5.3. Changes

JaVers records changes as atomic differences between versions of an object. As we may see from the JaVers scheme, there is no separate table for storing changes, so JaVers calculates changes dynamically as the difference between snapshots.

Let’s update a product price:

public void updateProductPrice(Integer productId, Double price) { Optional productOpt = productRepository.findById(productId); productOpt.ifPresent(product -> { product.setPrice(price); productRepository.save(product); }); }

Then, let's query JaVers for changes:

@GetMapping("/products/{productId}/changes") public String getProductChanges(@PathVariable int productId) { Product product = storeService.findProductById(productId); QueryBuilder jqlQuery = QueryBuilder.byInstance(product); Changes changes = javers.findChanges(jqlQuery.build()); return javers.getJsonConverter().toJson(changes); }

The output contains the changed property and its values before and after:

[ { "changeType": "ValueChange", "globalId": { "entity": "com.baeldung.springjavers.domain.Product", "cdoId": 2 }, "commitMetadata": { "author": "Baeldung Author", "properties": [], "commitDate": "2019-08-26T16:22:33.339", "commitDateInstant": "2019-08-26T13:22:33.339Z", "id": 2.00 }, "property": "price", "propertyChangeType": "PROPERTY_VALUE_CHANGED", "left": 100.0, "right": 3333.0 } ]

To detect a type of a change JaVers compares subsequent snapshots of an object's updates. In the case above as we've changed the property of the entity we've got the PROPERTY_VALUE_CHANGED change type.

5.4. Shadows

Moreover, JaVers provides another view of audited entities called Shadow. A Shadow represents an object state restored from snapshots. This concept is closely related to Event Sourcing.

There are four different scopes for Shadows:

  • Shallow — shadows are created from a snapshot selected within a JQL query
  • Child-value-object — shadows contain all child value objects owned by selected entities
  • Commit-deep - bayangan dibuat dari semua snapshot yang terkait dengan entitas yang dipilih
  • Deep + - JaVers mencoba memulihkan grafik objek penuh dengan (kemungkinan) semua objek dimuat.

Mari gunakan lingkup objek nilai anak dan dapatkan bayangan untuk satu penyimpanan:

@GetMapping("/stores/{storeId}/shadows") public String getStoreShadows(@PathVariable int storeId) { Store store = storeService.findStoreById(storeId); JqlQuery jqlQuery = QueryBuilder.byInstance(store) .withChildValueObjects().build(); List
    
      shadows = javers.findShadows(jqlQuery); return javers.getJsonConverter().toJson(shadows.get(0)); }
    

Hasilnya, kita akan mendapatkan entitas toko dengan objek nilai Alamat :

{ "commitMetadata": { "author": "Baeldung Author", "properties": [], "commitDate": "2019-08-26T16:09:20.674", "commitDateInstant": "2019-08-26T13:09:20.674Z", "id": 1.00 }, "it": { "id": 1, "name": "Baeldung store", "address": { "address": "Some street", "zipCode": 22222 }, "products": [] } }

Untuk mendapatkan produk dalam hasil, kami dapat menerapkan cakupan dalam-Komit.

6. Kesimpulan

Dalam tutorial ini, kita telah melihat betapa mudahnya JaVers terintegrasi dengan Spring Boot dan Spring Data pada khususnya. Secara keseluruhan, JaVers membutuhkan hampir nol konfigurasi untuk disiapkan.

Kesimpulannya, JaVers dapat memiliki aplikasi yang berbeda, mulai dari debugging hingga analisis kompleks.

Proyek lengkap untuk artikel ini tersedia di GitHub.