エンティティのモデリング

この章では、エンティティをモデル化する方法と、Couchbase サーバー自体での対応するエンティティの表現について説明します。

オブジェクトマッピングの基礎

このセクションでは、Spring Data オブジェクトマッピング、オブジェクト作成、フィールドとプロパティへのアクセス、可変性と不変性の基礎について説明します。このセクションは、基になるデータストア(JPA など)のオブジェクトマッピングを使用しない Spring Data モジュールにのみ適用されることに注意してください。また、インデックス、列名やフィールド名のカスタマイズなど、ストア固有のオブジェクトマッピングについては、ストア固有のセクションを参照してください。

Spring Data オブジェクトマッピングの中心的なロールは、ドメインオブジェクトのインスタンスを作成し、ストアネイティブデータ構造をそれらにマッピングすることです。つまり、2 つの基本的な手順が必要です。

  1. 公開されたコンストラクターの 1 つを使用したインスタンスの作成。

  2. すべての公開されたプロパティを具体化するインスタンスの設定。

オブジェクト作成

Spring Data は、その型のオブジェクトの具体化に使用される永続エンティティのコンストラクターを自動的に検出しようとします。解決アルゴリズムは次のように機能します。

  1. @PersistenceCreator でアノテーションが付けられた単一の静的ファクトリメソッドがある場合は、それが使用されます。

  2. コンストラクターが 1 つしかない場合は、それが使用されます。

  3. 複数のコンストラクターがあり、そのうちの 1 つだけに @PersistenceCreator アノテーションが付けられている場合は、それが使用されます。

  4. 型が Java Record の場合、標準コンストラクターが使用されます。

  5. 引数のないコンストラクターがある場合は、それが使用されます。他のコンストラクターは無視されます。

値の解決では、コンストラクター / ファクトリメソッドの引数名がエンティティのプロパティ名と一致することを前提としています。つまり、マッピングのすべてのカスタマイズ(異なるデータストア列またはフィールド名など)を含め、プロパティが入力されたかのように解決が実行されます。これには、クラスファイルで利用可能なパラメーター名情報、またはコンストラクターに存在する @ConstructorProperties アノテーションも必要です。

値の解決は、ストア固有の SpEL 式を使用した Spring Framework の @Value 値アノテーションを使用してカスタマイズできます。詳細については、ストア固有のマッピングに関するセクションを参照してください。

オブジェクト作成の詳細

リフレクションのオーバーヘッドを回避するために、Spring Data オブジェクトの作成では、デフォルトで実行時に生成されるファクトリクラスを使用します。これにより、ドメインクラスコンストラクターが直接呼び出されます。つまりこの例の型:

class Person {
  Person(String firstname, String lastname) { … }
}

実行時にこれと意味的に同等のファクトリクラスを作成します。

class PersonObjectInstantiator implements ObjectInstantiator {

  Object newInstance(Object... args) {
    return new Person((String) args[0], (String) args[1]);
  }
}

これにより、反射よりも約 10% パフォーマンスが向上します。ドメインクラスがこのような最適化の対象となるには、一連の制約に従う必要があります。

  • プライベートクラスであってはなりません

  • 非静的内部クラスであってはなりません

  • CGLib プロキシクラスであってはなりません

  • Spring Data で使用されるコンストラクターはプライベートであってはなりません

これらの条件のいずれかが一致する場合、Spring Data はリフレクションを介してエンティティのインスタンス化にフォールバックします。

プロパティ設定

エンティティのインスタンスが作成されると、Spring Data はそのクラスの残りのすべての永続プロパティを設定します。エンティティのコンストラクターによってすでに入力されていない場合(つまり、コンストラクターの引数リストを介して使用される場合)、ID プロパティが最初に入力され、循環オブジェクト参照の解決が可能になります。その後、コンストラクターによってまだ設定されていないすべての非一時的なプロパティがエンティティインスタンスに設定されます。そのために、次のアルゴリズムを使用します。

  1. プロパティが不変であるが with …  メソッドを公開している場合(以下を参照)、with …  メソッドを使用して、新しいプロパティ値を持つ新しいエンティティインスタンスを作成します。

  2. プロパティアクセス(つまり、getter および setter を介したアクセス)が定義されている場合、setter メソッドを呼び出しています。

  3. プロパティが変更可能な場合、フィールドを直接設定します。

  4. プロパティが不変の場合、永続化操作(オブジェクト作成を参照)で使用されるコンストラクターを使用して、インスタンスのコピーを作成します。

  5. デフォルトでは、フィールド値を直接設定します。

プロパティ設定の詳細

オブジェクト構築の最適化と同様に、Spring Data ランタイム生成のアクセサークラスを使用して、エンティティインスタンスと対話します。

class Person {

  private final Long id;
  private String firstname;
  private @AccessType(Type.PROPERTY) String lastname;

  Person() {
    this.id = null;
  }

  Person(Long id, String firstname, String lastname) {
    // Field assignments
  }

  Person withId(Long id) {
    return new Person(id, this.firstname, this.lastame);
  }

  void setLastname(String lastname) {
    this.lastname = lastname;
  }
}
生成されたプロパティアクセサー
class PersonPropertyAccessor implements PersistentPropertyAccessor {

  private static final MethodHandle firstname;              (2)

  private Person person;                                    (1)

  public void setProperty(PersistentProperty property, Object value) {

    String name = property.getName();

    if ("firstname".equals(name)) {
      firstname.invoke(person, (String) value);             (2)
    } else if ("id".equals(name)) {
      this.person = person.withId((Long) value);            (3)
    } else if ("lastname".equals(name)) {
      this.person.setLastname((String) value);              (4)
    }
  }
}
1PropertyAccessor は、基礎となるオブジェクトの可変インスタンスを保持します。これは、そうでなければ不変のプロパティの変更を可能にするためです。
2 デフォルトでは、Spring Data はフィールドアクセスを使用してプロパティ値を読み書きします。private フィールドの可視性ルールに従って、MethodHandles はフィールドとの対話に使用されます。
3 クラスは、識別子の設定に使用される withId(…) メソッドを公開します。インスタンスがデータストアに挿入され、識別子が生成されたとき。withId(…) を呼び出すと、新しい Person オブジェクトが作成されます。後続のすべての変更は、新しいインスタンスで行われ、前のインスタンスは変更されません。
4property-access を使用すると、MethodHandles を使用せずに直接メソッドを呼び出すことができます。

これにより、反射よりも約 25% パフォーマンスが向上します。ドメインクラスがこのような最適化の対象となるには、一連の制約に従う必要があります。

  • 型は、デフォルトまたは java パッケージに存在してはなりません。

  • 型とそのコンストラクターは public でなければなりません

  • 内部クラスである型は static でなければなりません。

  • 使用される Java ランタイムは、元の ClassLoader でクラスを宣言できるようにする必要があります。Java 9 以降には特定の制限があります。

デフォルトでは、Spring Data は生成されたプロパティアクセサーを使用しようとし、制限が検出された場合はリフレクションベースのものにフォールバックします。

次のエンティティを見てみましょう。

サンプルエンティティ
class Person {

  private final @Id Long id;                                                (1)
  private final String firstname, lastname;                                 (2)
  private final LocalDate birthday;
  private final int age;                                                    (3)

  private String comment;                                                   (4)
  private @AccessType(Type.PROPERTY) String remarks;                        (5)

  static Person of(String firstname, String lastname, LocalDate birthday) { (6)

    return new Person(null, firstname, lastname, birthday,
      Period.between(birthday, LocalDate.now()).getYears());
  }

  Person(Long id, String firstname, String lastname, LocalDate birthday, int age) { (6)

    this.id = id;
    this.firstname = firstname;
    this.lastname = lastname;
    this.birthday = birthday;
    this.age = age;
  }

  Person withId(Long id) {                                                  (1)
    return new Person(id, this.firstname, this.lastname, this.birthday, this.age);
  }

  void setRemarks(String remarks) {                                         (5)
    this.remarks = remarks;
  }
}
1identifier プロパティは final ですが、コンストラクターで null に設定されます。クラスは、識別子の設定に使用される withId(…) メソッドを公開します。インスタンスがデータストアに挿入され、識別子が生成されたとき。元の Person インスタンスは、新しいインスタンスが作成されるときに変更されません。通常、ストア管理される他のプロパティにも同じパターンが適用されますが、永続化操作のために変更する必要がある場合があります。永続化コンストラクター(6 を参照)は事実上コピーコンストラクターであり、プロパティの設定は新しい識別子値が適用された新しいインスタンスの作成に変換されるため、wither メソッドはオプションです。
2firstname および lastname プロパティは、getter を介して潜在的に公開される通常の不変のプロパティです。
3age プロパティは不変ですが、birthday プロパティから派生しています。示されている設計では、Spring Data は宣言された唯一のコンストラクターを使用するため、データベース値はデフォルト設定よりも優先されます。計算が優先されることを意図している場合でも、このコンストラクターがパラメーターとして age を受け取ることが重要です(無視される可能性があります)。そうしないと、プロパティ生成ステップは age フィールドを設定しようとし、不変で no with …  メソッドが存在します。
4comment プロパティは変更可能で、そのフィールドを直接設定することによって入力されます。
5remarks プロパティは変更可能で、setter メソッドを呼び出すことによって設定されます。
6 このクラスは、オブジェクト作成用のファクトリメソッドとコンストラクターを公開します。ここでの中心的な考え方は、@PersistenceCreator によるコンストラクターの曖昧性解消の必要性を回避するために、追加のコンストラクターの代わりにファクトリメソッドを使用することです。代わりに、プロパティのデフォルト設定はファクトリメソッド内で処理されます。Spring Data でオブジェクトのインスタンス化にファクトリメソッドを使用する場合は、@PersistenceCreator でアノテーションを付けます。

一般的な推奨事項

  • 不変オブジェクトにこだわる — 不変オブジェクトは、オブジェクトを具体化するのはコンストラクターのみを呼び出すだけなので、簡単に作成できます。また、これにより、クライアントオブジェクトがオブジェクトの状態を操作できるようにする setter メソッドがドメインオブジェクトに散らばるのを防ぎます。それらが必要な場合は、同じ場所に配置された限られた型でのみ呼び出せるように、パッケージを保護することをお勧めします。コンストラクターのみの実体化は、プロパティの設定よりも最大 30% 高速です。

  • all-args コンストラクターを提供する  — エンティティを不変の値としてモデル化できない、またはしたくない場合でも、オブジェクトのマッピングがプロパティの設定をスキップできるため、エンティティのすべてのプロパティを引数として取るコンストラクターを提供することには価値があります。最適なパフォーマンスのため。

  • @PersistenceCreator を回避するために、オーバーロードされたコンストラクターの代わりにファクトリメソッドを使用します — 最適なパフォーマンスに必要なすべての引数コンストラクターでは、通常、自動生成識別子などを省略したアプリケーションユースケース固有のコンストラクターを公開します。これらの all-args コンストラクターのバリアントを公開する静的ファクトリメソッド。

  • 生成されたインスタンス生成クラスとプロパティアクセッサクラスを使用できるようにする制約を必ず守ってください。

  • 生成される識別子については、すべての引数の永続化コンストラクター(推奨)または with …  メソッドと組み合わせて final フィールドを使用します

  • Lombok を使用してボイラープレートコードを回避します — 永続化操作は通常、すべての引数を取るコンストラクターを必要とするため、その宣言はフィールド割り当てに対するボイラープレートパラメーターの退屈な繰り返しとなりますが、Lombok の @AllArgsConstructor を使用することで回避することができます。

プロパティのオーバーライド

Java を使用すると、ドメインクラスを柔軟に設計できます。この場合、サブクラスは、スーパークラスで同じ名前ですでに宣言されているプロパティを定義できます。次の例を考えてみましょう。

public class SuperType {

   private CharSequence field;

   public SuperType(CharSequence field) {
      this.field = field;
   }

   public CharSequence getField() {
      return this.field;
   }

   public void setField(CharSequence field) {
      this.field = field;
   }
}

public class SubType extends SuperType {

   private String field;

   public SubType(String field) {
      super(field);
      this.field = field;
   }

   @Override
   public String getField() {
      return this.field;
   }

   public void setField(String field) {
      this.field = field;

      // optional
      super.setField(field);
   }
}

どちらのクラスも、割り当て可能な型を使用して field を定義します。ただし、SubType は SuperType.field をシャドウします。クラスの設計によっては、コンストラクターを使用することが SuperType.field を設定するための唯一のデフォルトのアプローチである可能性があります。または、setter で super.setField(…) を呼び出すと、SuperType で field を設定できます。プロパティは同じ名前を共有しますが、2 つの異なる値を表す可能性があるため、これらすべてのメカニズムはある程度の競合を引き起こします。Spring Data は、型が割り当て可能でない場合、スーパー型のプロパティをスキップします。つまり、オーバーライドされたプロパティの型は、オーバーライドとして登録されるスーパー型のプロパティ型に割り当て可能である必要があります。そうでない場合、スーパー型のプロパティは一時的なものと見なされます。通常、個別のプロパティ名を使用することをお勧めします。

Spring Data モジュールは通常、異なる値を保持するオーバーライドされたプロパティをサポートします。プログラミングモデルの観点から、考慮すべきことがいくつかあります。

  1. どのプロパティを永続化する必要がありますか(デフォルトでは、宣言されたすべてのプロパティになります)? これらに @Transient アノテーションを付けることで、プロパティを除外できます。

  2. データストアのプロパティを表す方法は? 異なる値に同じフィールド / 列名を使用すると、通常、データが破損するため、明示的なフィールド / 列名を使用してプロパティの少なくとも 1 つにアノテーションを付ける必要があります。

  3. @AccessType(PROPERTY) を使用することは、通常、setter 実装のさらなる仮定を行わずにスーパープロパティを設定することができないため、使用できません。

Kotlin サポート

Spring Data は、Kotlin の仕様を適合させて、オブジェクトの作成と変更を可能にします。

Kotlin オブジェクトの作成

Kotlin クラスはインスタンス化がサポートされています。すべてのクラスはデフォルトで不変であり、変更可能なプロパティを定義するには明示的なプロパティ宣言が必要です。

Spring Data は、その型のオブジェクトの具体化に使用される永続エンティティのコンストラクターを自動的に検出しようとします。解決アルゴリズムは次のように機能します。

  1. @PersistenceCreator でアノテーションが付けられたコンストラクターがある場合は、それが使用されます。

  2. 型が Kotlin データクラスの場合、プライマリコンストラクターが使用されます。

  3. @PersistenceCreator でアノテーションが付けられた単一の静的ファクトリメソッドがある場合は、それが使用されます。

  4. コンストラクターが 1 つしかない場合は、それが使用されます。

  5. 複数のコンストラクターがあり、そのうちの 1 つだけに @PersistenceCreator アノテーションが付けられている場合は、それが使用されます。

  6. 型が Java Record の場合、標準コンストラクターが使用されます。

  7. 引数のないコンストラクターがある場合は、それが使用されます。他のコンストラクターは無視されます。

次の data クラス Person を検討してください。

data class Person(val id: String, val name: String)

上記のクラスは、明示的なコンストラクターを持つ典型的なクラスにコンパイルされます。別のコンストラクターを追加してこのクラスをカスタマイズし、@PersistenceCreator でアノテーションを付けてコンストラクターの設定を示します。

data class Person(var id: String, val name: String) {

    @PersistenceCreator
    constructor(id: String) : this(id, "unknown")
}

Kotlin は、パラメーターが提供されない場合にデフォルト値を使用できるようにすることで、パラメーターのオプションをサポートしています。Spring Data がパラメーターのデフォルト設定を持つコンストラクターを検出した場合、データストアが値を提供しない(または単に null を返す)場合、Kotlin はパラメーターのデフォルト設定を適用できるため、これらのパラメーターは存在しません。name のパラメーターのデフォルト設定を適用する次のクラスを検討してください。

data class Person(var id: String, val name: String = "unknown")

name パラメーターが結果の一部ではないか、その値が null であるたびに、name は unknown にデフォルト設定されます。

委譲されたプロパティは Spring Data ではサポートされていません。マッピングメタデータは、Kotlin データクラスの委譲されたプロパティをフィルターします。その他の場合は、プロパティに @delegate:org.springframework.data.annotation.Transient のアノテーションを付けることで、委譲されたプロパティの合成フィールドを除外できます。

Kotlin データクラスのプロパティ設定

Kotlin では、すべてのクラスはデフォルトで不変であり、可変プロパティを定義するには明示的なプロパティ宣言が必要です。次の data クラス Person を検討してください。

data class Person(val id: String, val name: String)

このクラスは事実上不変です。Kotlin が既存のオブジェクトからすべてのプロパティ値をコピーしてメソッドに引数として提供されたプロパティ値を適用する新しいオブジェクトインスタンスを作成する copy(…) メソッドを生成するときに、新しいインスタンスを作成できます。

Kotlin オーバーライドプロパティ

Kotlin では、プロパティのオーバーライド (英語) を宣言して、サブクラスのプロパティを変更できます。

open class SuperType(open var field: Int)

class SubType(override var field: Int = 1) :
	SuperType(field) {
}

このような配置では、field という名前の 2 つのプロパティがレンダリングされます。Kotlin は、各クラスの各プロパティのプロパティアクセサー(getter および setter)を生成します。事実上、コードは次のようになります。

public class SuperType {

   private int field;

   public SuperType(int field) {
      this.field = field;
   }

   public int getField() {
      return this.field;
   }

   public void setField(int field) {
      this.field = field;
   }
}

public final class SubType extends SuperType {

   private int field;

   public SubType(int field) {
      super(field);
      this.field = field;
   }

   public int getField() {
      return this.field;
   }

   public void setField(int field) {
      this.field = field;
   }
}

SubType の Getter および setter は、SubType.field のみを設定し、SuperType.field は設定しません。このような配置では、コンストラクターを使用することが SuperType.field を設定するための唯一のデフォルトのアプローチです。SubType にメソッドを追加して this.SuperType.field = …  を介して SuperType.field を設定することは可能ですが、サポートされている規則の範囲外です。プロパティは同じ名前を共有しますが、2 つの異なる値を表す可能性があるため、プロパティのオーバーライドによってある程度の競合が発生します。通常、個別のプロパティ名を使用することをお勧めします。

Spring Data モジュールは通常、異なる値を保持するオーバーライドされたプロパティをサポートします。プログラミングモデルの観点から、考慮すべきことがいくつかあります。

  1. どのプロパティを永続化する必要がありますか(デフォルトでは、宣言されたすべてのプロパティになります)? これらに @Transient アノテーションを付けることで、プロパティを除外できます。

  2. データストアのプロパティを表す方法は? 異なる値に同じフィールド / 列名を使用すると、通常、データが破損するため、明示的なフィールド / 列名を使用してプロパティの少なくとも 1 つにアノテーションを付ける必要があります。

  3. @AccessType(PROPERTY) を使用すると、スーパープロパティが設定できないため使用できません。

Kotlin 値クラス

Kotlin 値クラスは、基礎となる概念を明示するために、より表現力豊かなドメインモデル用に設計されています。Spring Data は、値クラスを使用してプロパティを定義する型の読み取りと書き込みができます。

次のドメインモデルを検討してください。

@JvmInline
value class EmailAddress(val theAddress: String)                                    (1)

data class Contact(val id: String, val name:String, val emailAddress: EmailAddress) (2)
1Null 非許容値型を持つ単純な値クラス。
2EmailAddress 値クラスを使用してプロパティを定義するデータクラス。
非プリミティブ値型を使用する null 非許容プロパティは、コンパイルされたクラスで値型にフラット化されます。Null 許容プリミティブ値型または Null 許容値内値型は、ラッパー型で表現され、データベース内での値型の表現方法に影響します。

ドキュメントとフィールド

すべてのエンティティには @Document アノテーションを付ける必要がありますが、これは必須ではありません。

また、エンティティ内のすべてのフィールドに @Field アノテーションを付ける必要があります。これは厳密に言えばオプションですが、エッジケースを減らすのに役立ち、エンティティの意図と設計を明確に示します。フィールドを別の名前で保存するために使用することもできます。

常に配置する必要がある特別な @Id アノテーションもあります。ベストプラクティスは、プロパティにも id という名前を付けることです。

以下は非常に単純な User エンティティです。

例 1: フィールドを含む単純なドキュメント
import org.springframework.data.annotation.Id;
import org.springframework.data.couchbase.core.mapping.Field;
import org.springframework.data.couchbase.core.mapping.Document;

@Document
public class User {

    @Id
    private String id;

    @Field
    private String firstname;

    @Field
    private String lastname;

    public User(String id, String firstname, String lastname) {
        this.id = id;
        this.firstname = firstname;
        this.lastname = lastname;
    }

    public String getId() {
        return id;
    }

    public String getFirstname() {
        return firstname;
    }

    public String getLastname() {
        return lastname;
    }
}

Couchbase サーバーは、ドキュメントの自動有効期限切れをサポートします。ライブラリは、@Document アノテーションを通じてそのサポートを実装します。ドキュメントが自動的に削除されるまでの秒数に変換される expiry 値を設定できます。変更後 10 秒で期限切れにしたい場合は、@Document(expiry = 10) のように設定します。あるいは、Spring のプロパティサポートと expiryExpression パラメーターを使用して有効期限を構成し、有効期限値を動的に変更できるようにすることもできます。例: @Document(expiryExpression = "${valid.document.expiry}")。プロパティは int 値に解決できる必要があり、2 つのアプローチを混合することはできません。

エンティティで使用されるフィールド名とは対照的に、ドキュメント内のフィールド名を別の表現にしたい場合は、@Field アノテーションに別の名前を設定できます。たとえば、ドキュメントを小さくしたい場合は、名フィールドを @Field("fname") に設定できます。JSON ドキュメントでは、{"firstname": ".."} ではなく {"fname": ".."} が表示されます。

Couchbase 内のすべてのドキュメントには一意のキーが必要であるため、@Id アノテーションが存在する必要があります。このキーは、最大 250 文字の長さの任意の文字列である必要があります。UUID、メールアドレス、その他何でも、あなたのユースケースに合ったものを自由に使用してください。

Couchbase-Server バケットへの書き込みには、オプションで耐久性要件を割り当てることができます。これにより、書き込みをコミットする前に、Couchbase Server に、メモリ内および / またはクラスタ全体のディスク内の複数のノードで指定されたドキュメントを更新するように指示します。デフォルトの耐久性要件は、@Document または @Durability アノテーションを使用して構成することもできます。たとえば、@Document(durabilityLevel = DurabilityLevel.MAJORITY) は、データサービスノードの大部分にミューテーションを強制的に複製します。両方のアノテーションは、durabilityExpression 属性を介して式ベースの耐久性レベルの割り当てをサポートします (注意: SPEL はサポートされていません)。

データ型とコンバーター

選択した保存形式は JSON です。これは素晴らしいことですが、多くのデータ表現と同様に、Java で直接表現できるデータ型よりも少ないデータ型を使用できます。すべての非プリミティブ型については、サポートされている型との間で何らかの形式の変換を行う必要があります。

次のエンティティフィールド型の場合、特別な処理を追加する必要はありません。

表 1: 基本タイプ
Java 型 JSON 表現

string

文字列

boolean

boolean

byte

番号

short

番号

int

番号

long

番号

float

番号

double

番号

null

書き込み時に無視される

JSON はオブジェクト (「マップ」) とリストをサポートしているため、Map 型と List 型は自然に変換できます。最後の段落のプリミティブフィールド型のみが含まれている場合は、特別な処理を追加する必要もありません。以下に例を示します。

例 2: 地図とリストを含むドキュメント
@Document
public class User {

    @Id
    private String id;

    @Field
    private List<String> firstnames;

    @Field
    private Map<String, Integer> childrenAges;

    public User(String id, List<String> firstnames, Map<String, Integer> childrenAges) {
        this.id = id;
        this.firstnames = firstnames;
        this.childrenAges = childrenAges;
    }

}

ユーザーとサンプルデータを保存すると、JSON 表現として次のようになります。

例 3: マップとリストを含むドキュメント - JSON
{
    "_class": "foo.User",
    "childrenAges": {
        "Alice": 10,
        "Bob": 5
    },
    "firstnames": [
        "Foo",
        "Bar",
        "Baz"
    ]
}

常にすべてをプリミティブ型とリスト / マップに分解する必要はありません。もちろん、これらのプリミティブ値から他のオブジェクトを構成することもできます。List または Children を保存するように最後の例を変更しましょう。

例 4: 構成されたオブジェクトを含むドキュメント
@Document
public class User {

    @Id
    private String id;

    @Field
    private List<String> firstnames;

    @Field
    private List<Child> children;

    public User(String id, List<String> firstnames, List<Child> children) {
        this.id = id;
        this.firstnames = firstnames;
        this.children = children;
    }

    static class Child {
        private String name;
        private int age;

        Child(String name, int age) {
            this.name = name;
            this.age = age;
        }

    }

}

設定されたオブジェクトは次のようになります。

例 5: 構成されたオブジェクトを含むドキュメント - JSON
{
  "_class": "foo.User",
  "children": [
    {
      "age": 4,
      "name": "Alice"
    },
    {
      "age": 3,
      "name": "Bob"
    }
  ],
  "firstnames": [
    "Foo",
    "Bar",
    "Baz"
  ]
}

ほとんどの場合、Date のような一時的な値も保存する必要があります。JSON に直接保存できないため、変換を行う必要があります。このライブラリは、DateCalendar、JodaTime 型 (クラスパス上にある場合) のデフォルトのコンバーターを実装します。これらはすべて、ドキュメント内ではデフォルトで UNIX タイムスタンプ (数値) として表されます。後で示すように、カスタムコンバーターを使用してデフォルトの動作をいつでもオーバーライドできます。以下に例を示します。

例 6: 日付とカレンダーが記載されたドキュメント
@Document
public class BlogPost {

    @Id
    private String id;

    @Field
    private Date created;

    @Field
    private Calendar updated;

    @Field
    private String title;

    public BlogPost(String id, Date created, Calendar updated, String title) {
        this.id = id;
        this.created = created;
        this.updated = updated;
        this.title = title;
    }

}

設定されたオブジェクトは次のようになります。

例 7: 日付とカレンダーを含むドキュメント - JSON
{
  "title": "a blog post title",
  "_class": "foo.BlogPost",
  "updated": 1394610843,
  "created": 1394610843897
}

オプションで、システムプロパティ org.springframework.data.couchbase.useISOStringConverterForDate を true に設定することで、日付を ISO-8601 準拠の文字列との間で変換できます。コンバーターをオーバーライドしたり、独自のコンバーターを実装したりすることも可能です。このライブラリは、一般的な Spring コンバーターパターンを実装します。Bean の作成時に構成でカスタムコンバーターをプラグインできます。(オーバーライドされた AbstractCouchbaseConfiguration で) 設定する方法は次のとおりです。

例 8: カスタムコンバーター
@Override
public CustomConversions customConversions() {
    return new CustomConversions(Arrays.asList(FooToBarConverter.INSTANCE, BarToFooConverter.INSTANCE));
}

@WritingConverter
public static enum FooToBarConverter implements Converter<Foo, Bar> {
    INSTANCE;

    @Override
    public Bar convert(Foo source) {
        return /* do your conversion here */;
    }

}

@ReadingConverter
public static enum BarToFooConverter implements Converter<Bar, Foo> {
    INSTANCE;

    @Override
    public Foo convert(Bar source) {
        return /* do your conversion here */;
    }

}

カスタム変換については、次の点に留意する必要があります。

  • 明確にするために、コンバーターでは常に @WritingConverter および @ReadingConverter アノテーションを使用してください。特にプリミティブ型変換を扱っている場合、これは誤った変換の可能性を減らすのに役立ちます。

  • 書き込みコンバーターを実装する場合は、必ずプリミティブ型、マップ、リストのみにデコードしてください。より複雑なオブジェクト型が必要な場合は、基盤となる変換エンジンでも理解される CouchbaseDocument および CouchbaseList 型を使用します。最善の策は、できるだけ単純な変換に固執することです。

  • 間違ったコンバーターが実行されることを避けるために、常に、より特殊なコンバーターを汎用コンバーターの前に配置してください。

  • 日付の場合、読み取りコンバーターは ( Long だけでなく) 任意の Number から読み取ることができる必要があります。これは N1QL サポートに必要です。

楽観的ロック

状況によっては、ドキュメントに対して変更操作を実行するときに、別のユーザーの変更が上書きされないようにする必要がある場合があります。これには、トランザクション (Couchbase 6.5 以降)、ペシミスティック同時実行 (ロック)、またはオプティミスティック同時実行の 3 つの選択肢があります。

オプティミスティック同時実行は、データに対して実際のロックが保持されず、操作に関する追加情報が保存されない (トランザクションログがない) ため、悲観的同時実行やトランザクションよりも優れたパフォーマンスを提供する傾向があります。

楽観的ロックを実装するために、Couchbase は CAS (比較およびスワップ) アプローチを使用します。ドキュメントが変更されると、CAS 値も変更されます。CAS はクライアントに対して不透明です。唯一知っておく必要があるのは、コンテンツまたはメタ情報が変更されると CAS も変更されるということです。

他のデータストアでは、カウンターが増加する任意のバージョンフィールドを通じて同様の動作を実現できます。Couchbase はこれをより優れた方法でサポートしているため、実装が簡単です。自動オプティミスティックロックのサポートが必要な場合は、次のように長いフィールドに @Version アノテーションを追加するだけです。

例 9: 楽観的ロックを備えたドキュメント。
@Document
public class User {

        @Version
        private long version;

        // constructor, getters, setters...
}

テンプレートまたはリポジトリを通じてドキュメントをロードすると、バージョンフィールドに現在の CAS 値が自動的に入力されます。フィールドにアクセスしたり、自分でフィールドを変更したりしないでください。ドキュメントを再度保存すると、成功するか、OptimisticLockingFailureException で失敗します。このような例外が発生した場合、その後のアプローチは、アプリケーションで何を達成したいかによって異なります。読み込み、更新、書き込みサイクル全体を再試行するか、エラーを上位層に伝播して適切に処理する必要があります。

検証

このライブラリは、エンティティ内のアノテーションに直接基づく JSR 303 検証をサポートしています。もちろん、サービス層にあらゆる種類の検証を追加できますが、この方法では実際のエンティティにうまく結合されます。

これを機能させるには、2 つの追加の依存関係を含める必要があります。JSR 303 とそれを実装するライブラリ (Hibernate でサポートされているものなど):

例 10: 検証の依存関係
<dependency>
  <groupId>javax.validation</groupId>
  <artifactId>validation-api</artifactId>
</dependency>
<dependency>
  <groupId>org.hibernate</groupId>
  <artifactId>hibernate-validator</artifactId>
</dependency>

次に、構成に 2 つの Bean を追加する必要があります。

例 11: 検証 Bean
@Bean
public LocalValidatorFactoryBean validator() {
    return new LocalValidatorFactoryBean();
}

@Bean
public ValidatingCouchbaseEventListener validationEventListener() {
    return new ValidatingCouchbaseEventListener(validator());
}

JSR303 アノテーションを使用してフィールドにアノテーションを付けることができるようになりました。save() の検証が失敗すると、ConstraintViolationException がスローされます。

例 12: サンプル検証アノテーション
@Size(min = 10)
@Field
private String name;

監査

エンティティは、Spring Data 監査メカニズムを通じて自動的に監査できます (どのユーザーがオブジェクトを作成し、オブジェクトを更新したか、その時刻を追跡)。

まず、@Version アノテーション付きフィールドを持つエンティティのみが作成を監査できることに注意してください (そうでない場合、フレームワークは作成を更新として解釈します)。

監査は、フィールドに @CreatedBy@CreatedDate@LastModifiedBy@LastModifiedDate のアノテーションを付けることで機能します。フレームワークは、エンティティを永続化するときに、これらのフィールドに正しい値を自動的に挿入します。xxxDate アノテーションは Date フィールド (または互換性のある、たとえば jodatime クラス) に配置する必要がありますが、xxxBy アノテーションは任意のクラス T のフィールドに配置できます (ただし、両方のフィールドは同じ型である必要があります)。

監査を構成するには、まずコンテキスト内に監査対応の Bean が必要です。この Bean は AuditorAware<T> 型である必要があります (これにより、前述の T 型の xxxBy フィールドに格納できる値を生成できます)。次に、@EnableCouchbaseAuditing アノテーションを使用して、@Configuration クラスで監査を有効にする必要があります。

次に例を示します。

例 13: 監査エンティティのサンプル
@Document
public class AuditedItem {

  @Id
  private final String id;

  private String value;

  @CreatedBy
  private String creator;

  @LastModifiedBy
  private String lastModifiedBy;

  @LastModifiedDate
  private Date lastModification;

  @CreatedDate
  private Date creationDate;

  @Version
  private long version;

  //..omitted constructor/getters/setters/...
}

@CreatedBy と @LastModifiedBy は両方とも String フィールドに配置されているため、AuditorAware は String と連携する必要があることに注意してください。

例 14: サンプル AuditorAware 実装
public class NaiveAuditorAware implements AuditorAware<String> {

  private String auditor = "auditor";

  @Override
  public String getCurrentAuditor() {
    return auditor;
  }

  public void setAuditor(String auditor) {
    this.auditor = auditor;
  }
}

これらすべてを結び付けるために、AuditorAware Bean の宣言と監査のアクティブ化の両方に java 構成を使用します。

例 15: 監査構成のサンプル
@Configuration
@EnableCouchbaseAuditing //this activates auditing
public class AuditConfiguration extends AbstractCouchbaseConfiguration {

    //... a few abstract methods omitted here

    // this creates the auditor aware bean that will feed the annotations
    @Bean
    public NaiveAuditorAware testAuditorAware() {
      return new NaiveAuditorAware();
    }