Menggunakan JaVers untuk Pengauditan Model Data di Spring Data

1. Gambaran keseluruhan

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

2. JaVers

Semasa berurusan dengan data yang dapat diubah, kita biasanya hanya mempunyai keadaan terakhir entiti yang tersimpan dalam pangkalan data. Sebagai pembangun, kami menghabiskan banyak waktu untuk men-debug aplikasi, mencari melalui fail log untuk acara yang mengubah keadaan. Ini menjadi lebih sukar dalam persekitaran pengeluaran apabila banyak pengguna yang berbeza menggunakan sistem ini.

Nasib baik, kami mempunyai alat hebat seperti JaVers. JaVers adalah kerangka log audit yang membantu mengesan perubahan entiti dalam aplikasi.

Penggunaan alat ini tidak terhad kepada penyahpepijatan dan pengauditan sahaja. Ia dapat diterapkan dengan sukses untuk melakukan analisis, memaksakan kebijakan keamanan dan menjaga log peristiwa juga.

3. Penyediaan Projek

Pertama sekali, untuk mula menggunakan JaVers kita perlu mengkonfigurasi repositori audit untuk snapshot entiti yang berterusan. Kedua, kita perlu menyesuaikan beberapa sifat JaVers yang boleh dikonfigurasi. Akhirnya, kami juga akan membahas bagaimana mengkonfigurasi model domain kami dengan betul.

Tetapi, perlu disebutkan bahawa JaVers menyediakan pilihan konfigurasi lalai, jadi kita dapat mulai menggunakannya dengan hampir tanpa konfigurasi.

3.1. Kebergantungan

Pertama, kita perlu menambahkan pergantungan starter JaVers Spring Boot ke projek kita. Bergantung pada jenis penyimpanan ketekunan, kami mempunyai dua pilihan: org.javers: javers-spring-boot-starter-sql dan org.javers: javers-spring-boot-starter-mongo . Dalam tutorial ini, kami akan menggunakan starter Spring Boot SQL.

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

Oleh kerana kita akan menggunakan pangkalan data H2, mari kita sertakan juga kebergantungan ini:

 com.h2database h2 

3.2. Persediaan Repositori JaVers

JaVers menggunakan abstraksi repositori untuk menyimpan komit dan entiti bersiri. Semua data disimpan dalam format JSON. Oleh itu, mungkin sesuai untuk menggunakan storan NoSQL. Namun, demi kesederhanaan, kami akan menggunakan contoh H2 dalam memori.

Secara lalai, JaVers memanfaatkan pelaksanaan repositori dalam memori, dan jika kita menggunakan Spring Boot, tidak perlu konfigurasi tambahan. Selanjutnya, semasa menggunakan permulaan Data Spring, JaVers menggunakan semula konfigurasi pangkalan data untuk aplikasi .

JaVers menyediakan dua permulaan untuk tumpukan ketekunan SQL dan Mongo. Mereka serasi dengan Spring Data dan tidak memerlukan konfigurasi tambahan secara lalai. Walau bagaimanapun, kita selalu boleh mengatasi kacang konfigurasi lalai: JaversSqlAutoConfiguration.java dan JaversMongoAutoConfiguration.java masing-masing.

3.3. Hartanah JaVers

JaVers membolehkan mengkonfigurasi beberapa pilihan, walaupun default Boot Boot mencukupi dalam kebanyakan kes penggunaan.

Mari ganti hanya satu, newObjectSnapshot , supaya kita dapat memperoleh gambar objek yang baru dibuat:

javers.newObjectSnapshot=true 

3.4. Konfigurasi Domain JaVers

JaVers secara dalaman menentukan jenis berikut: Entiti, Objek Nilai, Nilai, Kontena, dan Primitif. Sebilangan istilah ini berasal dari istilah DDD (Domain Driven Design).

Tujuan utama mempunyai beberapa jenis adalah untuk menyediakan algoritma perbezaan yang berbeza bergantung pada jenisnya . Setiap jenis mempunyai strategi perbezaan yang sesuai. Akibatnya, jika kelas aplikasi dikonfigurasi dengan tidak betul kita akan mendapat hasil yang tidak dapat diramalkan.

Untuk memberitahu JaVers jenis apa yang akan digunakan untuk kelas, kami mempunyai beberapa pilihan:

  • Secara eksplisit - pilihan pertama adalah dengan menggunakan kaedah register * secara eksplisit dari kelas JaversBuilder - cara kedua adalah menggunakan anotasi
  • Secara implisit - JaVers menyediakan algoritma untuk mengesan jenis secara automatik berdasarkan hubungan kelas
  • Secara lalai - secara lalai, JaVers akan menganggap semua kelas sebagai ValueObjects

Dalam tutorial ini, kita akan mengkonfigurasi JaVers secara eksplisit, menggunakan kaedah anotasi.

Perkara yang hebat ialah JaVers serasi dengan anotasi javax.persistence . Hasilnya, kami tidak perlu menggunakan anotasi khusus JaVers pada entiti kami.

4. Contoh Projek

Sekarang kita akan membuat aplikasi mudah yang akan merangkumi beberapa entiti domain yang akan kita audit.

4.1. Model Domain

Domain kami akan merangkumi kedai dengan produk.

Mari tentukan entiti Kedai :

@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 maklum bahawa kami menggunakan anotasi JPA lalai. 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 tertanam 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 tentukan StoreRepository dengan anotasi itu:

@JaversSpringDataAuditable public interface StoreRepository extends CrudRepository { }

Selanjutnya, kami akan mempunyai ProductRepository , tetapi tidak diberi penjelasan:

public interface ProductRepository extends CrudRepository { }

Sekarang pertimbangkan kes apabila kita tidak menggunakan repositori Spring Data. JaVers mempunyai anotasi tahap kaedah lain untuk tujuan tersebut: @JaversAuditable.

Sebagai contoh, kami dapat menentukan kaedah untuk mengekalkan produk seperti berikut:

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

Sebagai alternatif, kita bahkan boleh menambahkan penjelasan ini tepat di atas kaedah dalam antara muka repositori:

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

4.3. Penyedia Pengarang

Setiap perubahan yang dilakukan dalam JaVers harus mempunyai pengarangnya. Lebih-lebih lagi, JaVers menyokong Spring Security 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
  • Komit-mendalam - bayangan dibuat dari semua gambar yang berkaitan dengan entiti terpilih
  • Deep + - JaVers cuba memulihkan grafik objek penuh dengan (mungkin) semua objek dimuat.

Mari gunakan ruang lingkup objek-nilai Anak dan dapatkan bayangan untuk satu kedai:

@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 mendapat entiti kedai dengan Alamat objek nilai:

{ "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 hasilnya, kami dapat menerapkan ruang lingkup Komitmen mendalam.

6. Kesimpulannya

Dalam tutorial ini, kita telah melihat betapa mudahnya JaVers berintegrasi dengan Spring Boot dan Spring Data khususnya. Secara keseluruhan, JaVers memerlukan konfigurasi hampir sifar untuk disiapkan.

Sebagai kesimpulan, JaVers boleh mempunyai aplikasi yang berbeza, dari debugging hingga analisis yang kompleks.

Projek penuh untuk artikel ini boleh didapati di GitHub.