1. 概要

このチュートリアルでは、単純な Spring Boot アプリケーションでJaVersを設定および使用して、エンティティの変更を追跡する方法を説明します。

2. JaVers

可変データを処理する場合、通常、データベースに格納されているエンティティの最後の状態のみがあります。 開発者として、私たちはアプリケーションのデバッグに多くの時間を費やし、ログファイルを検索して状態を変更したイベントを探します。 多くの異なるユーザーがシステムを使用している場合、これは実稼働環境ではさらに厄介になります。

幸い、JaVersのような優れたツールがあります。 JaVersは、アプリケーション内のエンティティの変更を追跡するのに役立つ監査ログフレームワークです。

このツールの使用法は、デバッグと監査のみに限定されません。 分析の実行、セキュリティポリシーの強制、およびイベントログの維持にも正常に適用できます。

3. プロジェクトの設定

まず、JaVersの使用を開始するには、エンティティのスナップショットを永続化するための監査リポジトリを構成する必要があります。 次に、JaVersのいくつかの構成可能なプロパティを調整する必要があります。 最後に、ドメインモデルを適切に構成する方法についても説明します。

ただし、JaVersにはデフォルトの構成オプションが用意されているため、ほとんど構成せずに使用を開始できます。

3.1. 依存関係

まず、JaVersSpringBootスターターの依存関係をプロジェクトに追加する必要があります。 永続ストレージのタイプに応じて、 org.javers:javers-spring-boot-starter-sqlorg.javers:javers-spring-boot-starter-mongo[の2つのオプションがあります。 X169X]。 このチュートリアルでは、Spring BootSQLスターターを使用します。

<dependency>
    <groupId>org.javers</groupId>
    <artifactId>javers-spring-boot-starter-sql</artifactId>
    <version>6.5.3</version>
</dependency>

H2データベースを使用するので、この依存関係も含めましょう。

<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
</dependency>

3.2. JaVersリポジトリ設定

JaVersは、コミットとシリアル化されたエンティティを格納するためにリポジトリの抽象化を使用します。 すべてのデータはJSON形式で保存されます。 したがって、NoSQLストレージを使用するのが適切な場合があります。 ただし、簡単にするために、H2インメモリインスタンスを使用します。

デフォルトでは、JaVersはメモリ内リポジトリの実装を利用しており、Spring Bootを使用している場合は、追加の構成は必要ありません。 さらに、 Spring Dataスターターを使用している間、JaVersはアプリケーションのデータベース構成を再利用します。

JaVersは、SQLおよびMongo永続スタック用の2つのスターターを提供します。  これらはSpringDataと互換性があり、デフォルトで追加の構成を必要としません。 ただし、デフォルトの構成Beanをいつでもオーバーライドできます:それぞれJaversSqlAutoConfiguration.javaおよびJaversMongoAutoConfiguration.java

3.3. JaVersプロパティ

JaVersではいくつかのオプションを構成できますが、ほとんどのユースケースではSpringBootのデフォルトで十分です。

newObjectSnapshot の1つだけをオーバーライドして、新しく作成されたオブジェクトのスナップショットを取得できるようにします。

javers.newObjectSnapshot=true

3.4. JaVersドメイン構成

JaVersは、エンティティ、値オブジェクト、値、コンテナ、およびプリミティブのタイプを内部的に定義します。 これらの用語の一部は、 DDD(ドメイン駆動設計)の用語に由来します。

複数のタイプを持つ主な目的は、タイプに応じて異なる差分アルゴリズムを提供することです。 各タイプには、対応する差分戦略があります。 結果として、アプリケーションクラスが正しく構成されていない場合、予測できない結果が得られます。

クラスに使用するタイプをJaVersに伝えるために、いくつかのオプションがあります。

  • Explicitly –最初のオプションはJaversBuilderクラスのregister* メソッドを明示的に使用することです–2番目の方法はアノテーションを使用することです
  • Implicitly – JaVersは、クラスの関係に基づいて型を自動的に検出するためのアルゴリズムを提供します
  • デフォルト–デフォルトでは、JaVersはすべてのクラスをValueObjectsとして扱います。

このチュートリアルでは、アノテーションメソッドを使用してJaVersを明示的に構成します。

すばらしいのは、JaVersがjavax.persistenceアノテーションと互換性があることです。 その結果、エンティティにJaVers固有のアノテーションを使用する必要がなくなります。

4. サンプルプロジェクト

次に、監査するいくつかのドメインエンティティを含む単純なアプリケーションを作成します。

4.1. ドメインモデル

私たちのドメインには、製品を扱う店舗が含まれます。

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<Product> products = new ArrayList<>();
    
    // constructors, getters, setters
}

デフォルトのJPAアノテーションを使用していることに注意してください。 JaVersはそれらを次の方法でマップします。

  • @javax.persistence.Entity@org.javers.core.metamodel.annotation.Entityにマップされます
  • @javax.persistence.Embeddable@org.javers.core.metamodel.annotation.ValueObject。にマップされます

埋め込み可能なクラスは、通常の方法で定義されます。

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

4.2. データリポジトリ

JPAリポジトリーを監査するために、JaVersは@JaversSpringDataAuditableアノテーションを提供します。

そのアノテーションを使用してStoreRepositoryを定義しましょう。

@JaversSpringDataAuditable
public interface StoreRepository extends CrudRepository<Store, Integer> {
}

さらに、 ProductRepository がありますが、注釈はありません。

public interface ProductRepository extends CrudRepository<Product, Integer> {
}

ここで、SpringDataリポジトリを使用していない場合を考えてみましょう。 JaVersには、その目的のための別のメソッドレベルのアノテーションがあります:@JaversAuditable。

たとえば、製品を永続化する方法を次のように定義できます。

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

または、リポジトリインターフェイスのメソッドのすぐ上にこのアノテーションを追加することもできます。

public interface ProductRepository extends CrudRepository<Product, Integer> {
    @Override
    @JaversAuditable
    <S extends Product> S save(S s);
}

4.3. 作成者プロバイダー

JaVersでコミットされた各変更には、作成者が必要です。 さらに、JaVersは SpringSecurityをすぐにサポートします。

その結果、各コミットは特定の認証されたユーザーによって行われます。 ただし、このチュートリアルでは、AuthorProviderインターフェイスの非常に単純なカスタム実装を作成します。

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

最後のステップとして、JaVersにカスタム実装を使用させるには、デフォルトの構成Beanをオーバーライドする必要があります。

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

5. JaVers監査

最後に、アプリケーションを監査する準備が整いました。 単純なコントローラーを使用して、アプリケーションに変更をディスパッチし、JaVersコミットログを取得します。 または、H2コンソールにアクセスして、データベースの内部構造を確認することもできます。

 

 

 

 

 

 

 

初期サンプルデータを取得するには、 EventListener を使用して、データベースにいくつかの製品を入力します。

@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. 初期コミット

オブジェクトが作成されると、JaVersは最初にINITIALタイプのコミットを行います。

アプリケーションの起動後にスナップショットを確認してみましょう。

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

上記のコードでは、StoreクラスのスナップショットをJaVersにクエリしています。 このエンドポイントにリクエストを送信すると、次のような結果が得られます。

[
  {
    "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
  }
]

上記のスナップショットには、ProductRepositoryインターフェイスの注釈がないにもかかわらず、ストアに追加されたすべての製品が含まれていることに注意してください。

デフォルトでは、JaVersは、親とともに永続化されている場合、集約ルートのすべての関連モデルを監査します。

DiffIgnore アノテーションを使用して、特定のクラスを無視するようにJaVersに指示できます。

たとえば、productsフィールドにStoreエンティティの注釈を付けることができます。

@DiffIgnore
private List<Product> products = new ArrayList<>();

したがって、JaVersは、Storeエンティティから発信された製品の変更を追跡しません。

5.2. 更新コミット

次のタイプのコミットは、UPDATEコミットです。 これは、オブジェクトの状態の変化を表すため、最も価値のあるコミットタイプです。

ストアエンティティとストア内のすべての製品を更新するメソッドを定義しましょう。

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

このメソッドを実行すると、デバッグ出力に次の行が表示されます(同じ製品とストア数の場合)。

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)

JaVersは変更を正常に永続化したので、製品のスナップショットを照会してみましょう。

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

以前のINITIALコミットと新しいUPDATEコミットを取得します。

 {
    "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
      }
    }
}

ここでは、行った変更に関するすべての情報を確認できます。

注目に値する JaVersはデータベースへの新しい接続を作成しません。 代わりに、既存の接続を再利用します 。 JaVersデータは、同じトランザクションでアプリケーションデータとともにコミットまたはロールバックされます。

5.3. 変更点

JaVersは、オブジェクトのバージョン間のアトミックな違いとして変更を記録します。 JaVersスキームからわかるように、変更を保存するための個別のテーブルがないため、JaVersはスナップショット間の差として動的に変更を計算します

製品の価格を更新しましょう:

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

次に、JaVersに変更を問い合わせましょう。

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

出力には、変更されたプロパティとその前後の値が含まれます。

[
  {
    "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
  }
]

変更のタイプを検出するために、JaVersはオブジェクトの更新の後続のスナップショットを比較します。 上記の場合、エンティティのプロパティを変更したため、PROPERTY_VALUE_CHANGED変更タイプがあります。

5.4. 影

さらに、JaVersは、Shadowと呼ばれる監査対象エンティティの別のビューを提供します。 シャドウは、スナップショットから復元されたオブジェクトの状態を表します。 この概念は、イベントソーシングと密接に関連しています。

シャドウには4つの異なるスコープがあります。

  • 浅い—JQLクエリ内で選択されたスナップショットからシャドウが作成されます
  • 子値オブジェクト—シャドウには、選択したエンティティが所有するすべての子値オブジェクトが含まれます
  • Commit-deep —選択したエンティティに関連するすべてのスナップショットからシャドウが作成されます
  • Deep + — JaVersは、(おそらく)すべてのオブジェクトがロードされた状態で完全なオブジェクトグラフを復元しようとします。

Child-value-objectスコープを使用して、単一のストアのシャドウを取得しましょう。

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

その結果、Address値オブジェクトを持つストアエンティティを取得します。

{
  "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": []
  }
}

結果に製品を取得するために、Commit-deepスコープを適用する場合があります。

6. 結論

このチュートリアルでは、JaVersがSpring Bootおよび特にSpringDataといかに簡単に統合できるかを見てきました。 全体として、JaVersのセットアップにはほとんど構成が必要ありません。

結論として、JaVersには、デバッグから複雑な分析まで、さまざまなアプリケーションがあります。

この記事の完全なプロジェクトは、GitHubから入手できます。