マッピング

豊富なオブジェクトマッピングのサポートは、MappingCassandraConverter によって提供されます。MappingCassandraConverter には、ドメインオブジェクトを CQL テーブルにマップする機能の完全な機能セットを提供する豊富なメタデータモデルがあります。

マッピングメタデータモデルは、ドメインオブジェクトのアノテーションを使用して設定されます。ただし、インフラストラクチャは、メタデータの唯一のソースとしてアノテーションを使用することに限定されません。MappingCassandraConverter では、一連の規則に従って、追加のメタデータを提供せずにドメインオブジェクトをテーブルにマップすることもできます。

この章では、MappingCassandraConverter の機能、ドメインオブジェクトをテーブルにマッピングするための規則の使用メソッド、およびアノテーションベースのマッピングメタデータでそれらの規則をオーバーライドする方法について説明します。

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

このセクションでは、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 にデフォルト設定されます。

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 許容値内値型は、ラッパー型で表現され、データベース内での値型の表現方法に影響します。

データマッピングと型変換

このセクションでは、型が Apache Cassandra 表現にマッピングされる方法と、Apache Cassandra 表現からマッピングされる方法について説明します。

Apache Cassandra の Spring Data は、Apache Cassandra によって提供されるいくつかの型をサポートしています。これらの型に加えて、Apache Cassandra の Spring Data は、追加の型をマップするための組み込みコンバーターのセットを提供します。型変換を調整するために、独自のカスタムコンバーターを提供することもできます。詳細については、"カスタムコンバーターによるデフォルトマッピングのオーバーライド" を参照してください。次の表は、Spring Data 型を Cassandra 型にマップします。

表 1: タイプ
タイプ Cassandra 型

String

text (default), varchar, ascii

double, Double

double

float, Float

float

long, Long

bigint (default), counter

int, Integer

int

short, Short

smallint

byte, Byte

tinyint

boolean, Boolean

boolean

BigInteger

varint

BigDecimal

decimal

java.util.Date

timestamp

com.datastax.driver.core.LocalDate

date

InetAddress

inet

ByteBuffer

blob

java.util.UUID

uuid

TupleValue, mapped Tuple Types

tuple<…>

UDTValue, mapped User-Defined Types

user type

java.util.Map<K, V>

map

java.util.List<E>

list

java.util.Set<E>

set

Enum

text (default), bigint, varint, int, smallint, tinyint

LocalDate
(Joda, Java 8, JSR310-BackPort)

date

LocalTime+ (Joda, Java 8, JSR310-BackPort)

time

LocalDateTime, LocalTime, Instant
(Joda, Java 8, JSR310-BackPort)

timestamp

ZoneId (Java 8, JSR310-BackPort)

text

サポートされている各型は、デフォルトの Cassandra データ型 (英語) にマップされます。次の例に示すように、Java 型は @CassandraType を使用して他の Cassandra 型にマップできます。

例 1: 数値型への列挙型マッピング
@Table
public class EnumToOrdinalMapping {

  @PrimaryKey String id;

  @CassandraType(type = Name.INT) Condition asOrdinal;
}

public enum Condition {
  NEW, USED
}

規約ベースのマッピング

MappingCassandraConverter は、追加のマッピングメタデータが提供されない場合、ドメインオブジェクトを CQL テーブルにマッピングするためにいくつかの規則を使用します。規約は次のとおりです。

  • 単純な (短い) Java クラス名は、小文字に変更されてテーブル名にマップされます。例: com.bigbank.SavingsAccount は savingsaccount という名前のテーブルにマップされます。

  • コンバーターは、登録されている Spring Converter インスタンスを使用して、オブジェクトプロパティのテーブル列へのデフォルトのマッピングをオーバーライドします。

  • オブジェクトのプロパティは、テーブル内の列との間の変換に使用されます。

CassandraMappingContext 上で NamingStrategy を構成することで、規則を調整できます。命名戦略オブジェクトは、テーブル、列、またはユーザー定義型がエンティティクラスおよび実際のプロパティから派生する規則を実装します。

次の例は、NamingStrategy を構成する方法を示しています。

例 2: CassandraMappingContext での NamingStrategy の構成
		CassandraMappingContext context = new CassandraMappingContext();

		// default naming strategy
		context.setNamingStrategy(NamingStrategy.INSTANCE);

		// snake_case converted to upper case (SNAKE_CASE)
		context.setNamingStrategy(NamingStrategy.SNAKE_CASE.transform(String::toUpperCase));

マッピング設定

明示的に構成されていない限り、CassandraTemplate の作成時に、デフォルトで MappingCassandraConverter のインスタンスが作成されます。MappingCassandraConverter の独自のインスタンスを作成して、起動時にドメインクラスのクラスパスをスキャンしてメタデータを抽出し、インデックスを構築する場所を指定できます。

また、独自のインスタンスを作成することにより、データベースとの間で特定のクラスをマッピングするために使用する Spring Converter インスタンスを登録できます。次の構成クラスの例では、Cassandra マッピングサポートをセットアップします。

例 3: Cassandra マッピングのサポートを構成するための @Configuration クラス
@Configuration
public class SchemaConfiguration extends AbstractCassandraConfiguration {

	@Override
	protected String getKeyspaceName() {
		return "bigbank";
	}

	// the following are optional

	@Override
	public CassandraCustomConversions customConversions() {

		return CassandraCustomConversions.create(config -> {
			config.registerConverter(new PersonReadConverter()));
			config.registerConverter(new PersonWriteConverter()));
		});
	}

	@Override
	public SchemaAction getSchemaAction() {
		return SchemaAction.RECREATE;
	}

	// other methods omitted...
}

AbstractCassandraConfiguration では、キースペースを定義するメソッドを実装する必要があります。AbstractCassandraConfiguration には getEntityBasePackages(…) というメソッドもあります。これをオーバーライドして、@Table アノテーションが付けられたクラスをスキャンする場所をコンバーターに指示できます。

customConversions メソッドをオーバーライドすることで、MappingCassandraConverter にコンバーターを追加できます。

AbstractCassandraConfiguration は CassandraTemplate インスタンスを作成し、cassandraTemplate という名前でコンテナーに登録します。

メタデータベースのマッピング

Apache Cassandra サポート用の Spring Data 内のオブジェクトマッピング機能を最大限に活用するには、マップされたドメインオブジェクトに @Table アノテーションを付ける必要があります。これにより、クラスパススキャナーがドメインオブジェクトを見つけて前処理し、必要なメタデータを抽出できるようになります。アノテーションが付けられたエンティティのみがスキーマアクションの実行に使用されます。最悪の場合、SchemaAction.RECREATE_DROP_UNUSED 操作によってテーブルが削除され、データが失われます。次の例は、単純なドメインオブジェクトを示しています。

例 4: ドメインオブジェクトの例
package com.mycompany.domain;

@Table
public class Person {

  @Id
  private String id;

  @CassandraType(type = Name.VARINT)
  private Integer ssn;

  private String firstName;

  private String lastName;
}
@Id アノテーションは、Cassandra 主キーに使用するプロパティをマッパーに伝えます。複合主キーには、わずかに異なるデータモデルが必要になる場合があります。

主キーの操作

Cassandra では、CQL テーブルに少なくとも 1 つのパーティションキーフィールドが必要です。テーブルでは、1 つ以上のクラスタリングキーフィールドを追加で宣言できます。CQL テーブルに複合主キーがある場合は、@PrimaryKeyClass を作成して複合主キーの構造を定義する必要があります。この文脈では、「複合主キー」とは、1 つまたは複数のクラスタリング列とオプションで組み合わせられる 1 つまたは複数のパーティション列を意味します。

主キーでは、任意の単数の単純な Cassandra 型またはマップされたユーザー定義型を使用できます。コレクション型の主キーはサポートされていません。

単純な主キー

単純な主キーは、エンティティクラス内の 1 つのパーティションキーフィールドで構成されます。これは 1 つのフィールドのみであるため、パーティションキーであると想定して問題ありません。次のリストは、user_id の主キーを持つ Cassandra で定義された CQL テーブルを示しています。

例 5: Cassandra で定義された CQL テーブル
CREATE TABLE user (
  user_id text,
  firstname text,
  lastname text,
  PRIMARY KEY (user_id))
;

次の例は、前のリストで定義された Cassandra に対応するようにアノテーションが付けられた Java クラスを示しています。

例 6: アノテーション付きエンティティ
@Table(value = "login_event")
public class LoginEvent {

  @PrimaryKey("user_id")
  private String userId;

  private String firstname;
  private String lastname;

  // getters and setters omitted

}

複合キー

複合主キー (または複合キー) は、複数の主キーフィールドで構成されます。ただし、複合主キーは、複数のパーティションキー、パーティションキーとクラスタリングキー、または多数の主キーフィールドで構成される場合があります。

複合キーは、Spring Data または Apache Cassandra の 2 つの方法で表現できます。

  • エンティティに埋め込まれています。

  • @PrimaryKeyClass を使用します。

複合キーの最も単純な形式は、1 つのパーティションキーと 1 つのクラスタリングキーを持つキーです。

次の例は、テーブルとその複合キーを表す CQL ステートメントを示しています。

例 7: 複合主キーを含む CQL テーブル
CREATE TABLE login_event(
  person_id text,
  event_code int,
  event_time timestamp,
  ip_address text,
  PRIMARY KEY (person_id, event_code, event_time))
  WITH CLUSTERING ORDER BY (event_time DESC)
;

フラット複合主キー

フラット複合主キーは、フラットフィールドとしてエンティティ内に埋め込まれます。主キーフィールドには @PrimaryKeyColumn というアノテーションが付けられます。選択には、クエリに個々のフィールドの述語を含めるか、MapId を使用する必要があります。次の例は、フラットな複合主キーを持つクラスを示しています。

例 8: フラット複合主キーの使用
@Table(value = "login_event")
class LoginEvent {

  @PrimaryKeyColumn(name = "person_id", ordinal = 0, type = PrimaryKeyType.PARTITIONED)
  private String personId;

  @PrimaryKeyColumn(name = "event_code", ordinal = 1, type = PrimaryKeyType.PARTITIONED)
  private int eventCode;

  @PrimaryKeyColumn(name = "event_time", ordinal = 2, type = PrimaryKeyType.CLUSTERED, ordering = Ordering.DESCENDING)
  private LocalDateTime eventTime;

  @Column("ip_address")
  private String ipAddress;

  // getters and setters omitted
}

主キークラス

主キークラスは、エンティティの複数のフィールドまたはプロパティにマップされる複合主キークラスです。これには @PrimaryKeyClass のアノテーションが付けられており、equals メソッドと hashCode メソッドを定義する必要があります。これらのメソッドの値の等価性のセマンティクスは、キーがマップされるデータベース型のデータベースの等価性と一致している必要があります。主キークラスは、リポジトリ (Id 型として) とともに使用し、単一の複雑なオブジェクトでエンティティの ID を表すことができます。次の例は、複合主キークラスを示しています。

例 9: 複合主キークラス
@PrimaryKeyClass
class LoginEventKey implements Serializable {

  @PrimaryKeyColumn(name = "person_id", ordinal = 0, type = PrimaryKeyType.PARTITIONED)
  private String personId;

  @PrimaryKeyColumn(name = "event_code", ordinal = 1, type = PrimaryKeyType.PARTITIONED)
  private int eventCode;

  @PrimaryKeyColumn(name = "event_time", ordinal = 2, type = PrimaryKeyType.CLUSTERED, ordering = Ordering.DESCENDING)
  private LocalDateTime eventTime;

  // other methods omitted
}

次の例は、複合主キーの使用方法を示しています。

例 10: 複合主キーの使用
@Table(value = "login_event")
public class LoginEvent {

  @PrimaryKey
  private LoginEventKey key;

  @Column("ip_address")
  private String ipAddress;

  // getters and setters omitted
}

埋め込みエンティティのサポート

埋め込みエンティティは、プロパティがテーブルにフラット化される Java ドメインモデル内の値オブジェクトを設計するために使用されます。次の例では、User.name に @Embedded というアノテーションが付けられていることがわかります。この結果、UserName のすべてのプロパティが 3 つの列 (user_idfirstnamelastname) で構成される user テーブルに組み込まれます。

埋め込みエンティティには、単純なプロパティ型のみを含めることができます。埋め込まれたエンティティを別の埋め込まれたエンティティにネストすることはできません。

ただし、結果セット内の firstname および lastname 列の値が実際には null である場合、すべてのネストされたプロパティが null である場合、@Embedded の onEmpty に従ってプロパティ name 全体が null に設定されます。この null のオブジェクトは null です。
この動作とは逆に、USE_EMPTY は、デフォルトのコンストラクター、または結果セットから null 許容パラメーター値を受け入れるコンストラクターを使用して、新しいインスタンスを作成しようとします。

例 11: 埋め込みオブジェクトのサンプルコード
public class User {

	@PrimaryKey("user_id")
    private String userId;

    @Embedded(onEmpty = USE_NULL) (1)
    UserName name;
}

public class UserName {
    private String firstname;
    private String lastname;
}
1firstname および lastname が null の場合、プロパティは null です。onEmpty=USE_EMPTY を使用して、そのプロパティの潜在的な null 値を使用して UserName をインスタンス化します。

@Embedded アノテーションのオプションの prefix 要素を使用すると、値オブジェクトをエンティティに複数回埋め込むことができます。この要素は接頭辞を表し、埋め込みオブジェクト内の各列名の先頭に付加されます。複数のプロパティが同じ列名にレンダリングされる場合、プロパティは互いに上書きされることに注意してください。

@Embedded(onEmpty = USE_NULL) および @Embedded(onEmpty = USE_EMPTY) のショートカット @Embedded.Nullable および @Embedded.Empty を使用して冗長性を減らし、同時にそれに応じて JSR-305 @javax.annotation.Nonnull を設定します。

public class MyEntity {

    @Id
    Integer id;

    @Embedded.Nullable (1)
    EmbeddedEntity embeddedEntity;
}
1@Embedded(onEmpty = USE_NULL) のショートカット。

マッピングアノテーションの概要

MappingCassandraConverter はメタデータを使用して、Cassandra テーブル内の行へのオブジェクトのマッピングを実行できます。アノテーションの概要は次のとおりです。

  • @Id: フィールドまたはプロパティレベルで適用され、ID 目的で使用されるプロパティをマークします。

  • @Table: クラスレベルで適用され、このクラスがデータベースへのマッピングの候補であることを示します。オブジェクトが保存されているテーブルの名前を指定できます。

  • @PrimaryKey@Id に似ていますが、列名を指定できます。

  • @PrimaryKeyColumn: 主キー列の Cassandra 固有のアノテーション。クラスター化またはパーティション化などの主キー列属性を指定できます。単一または複数の属性で使用して、単一または複合 (複合) 主キーを示すことができます。エンティティ内のプロパティで使用する場合は、必ず @Id アノテーションも適用してください。

  • @PrimaryKeyClass: クラスレベルで適用され、このクラスが複合主キークラスであることを示します。エンティティクラスの @PrimaryKey で参照する必要があります。

  • @Transient: デフォルトでは、すべてのプライベートフィールドが行にマップされます。このアノテーションは、それが適用されるフィールドをデータベースへの保存から除外します。コンバーターはコンストラクター引数の値を具体化できないため、一時プロパティは永続コンストラクター内で使用できません。

  • @PersistenceConstructor: 指定されたコンストラクターをマークします — パッケージでさえ保護されたもの — データベースからオブジェクトをインスタンス化するときに使用します。コンストラクターの引数は、取得した行のキー値に名前でマップされます。

  • @Value: このアノテーションは Spring Framework の一部です。マッピングフレームワーク内では、コンストラクターの引数に適用できます。これにより、Spring 式言語ステートメントを使用して、データベースで取得したキーの値を、ドメインオブジェクトの構築に使用する前に変換できます。特定の Row/UdtValue/TupleValue のプロパティを参照するには、次のような式を使用する必要があります。: @Value("#root.getString(0)") ここで、root は指定されたドキュメントのルートを指します。

  • @ReadOnlyProperty: フィールドレベルで適用して、プロパティを読み取り専用としてマークします。エンティティにバインドされた挿入および更新ステートメントには、このプロパティは含まれません。

  • @Column: フィールドレベルで適用されます。Cassandra テーブルで表される列名を記述します。名前はクラスのフィールド名とは異なります。コンストラクターの引数で使用して、コンストラクターの作成中に列名をカスタマイズできます。

  • @Embedded: フィールドレベルで適用されます。テーブルまたはユーザー定義型にマップされた型の埋め込みオブジェクトの使用を有効にします。埋め込みオブジェクトのプロパティは、その親の構造にフラット化されます。

  • @Indexed: フィールドレベルで適用されます。セッション初期化時に作成されるインデックスを記述します。

  • @SASI: フィールドレベルで適用されます。セッションの初期化中に SASI インデックスを作成できるようにします。

  • @CassandraType: Cassandra データ型を指定するためにフィールドレベルで適用されます。デフォルトでは、型はプロパティ宣言から派生します。

  • @Frozen: フィールドレベルでクラス型とパラメーター化された型に適用されます。List<@Frozen UserDefinedPersonType> のような凍結された UDT 列または凍結されたコレクションを宣言します。

  • @UserDefinedType: Cassandra ユーザー定義データ型 (UDT) を指定するために型レベルで適用されます。デフォルトでは、型は宣言から派生します。

  • @Tuple: 型をマップされたタプルとして使用するために型レベルで適用されます。

  • @Element: マップされたタプル内の要素またはフィールドの序数を指定するためにフィールドレベルで適用されます。デフォルトでは、型はプロパティ宣言から派生します。コンストラクターの引数で使用して、コンストラクターの作成中にタプル要素の序数をカスタマイズできます。

  • @Version: フィールドレベルで適用されると、オプティミスティックロックに使用され、保存操作時に変更がチェックされます。初期値は zero で、更新のたびに自動的に増加します。

マッピングメタデータインフラストラクチャは、テクノロジーにもデータストアにも依存しない別の spring-data-commons プロジェクトで定義されます。

次の例は、より複雑なマッピングを示しています。

例 12: マッピングされた Person クラス
@Table("my_person")
public class Person {

	@PrimaryKeyClass
	public static class Key implements Serializable {

		@PrimaryKeyColumn(ordinal = 0, type = PrimaryKeyType.PARTITIONED)
		private String type;

		@PrimaryKeyColumn(ordinal = 1, type = PrimaryKeyType.PARTITIONED)
		private String value;

		@PrimaryKeyColumn(name = "correlated_type", ordinal = 2, type = PrimaryKeyType.CLUSTERED)
		private String correlatedType;

		// other getters/setters omitted
	}

	@PrimaryKey
	private Person.Key key;

	@CassandraType(type = CassandraType.Name.VARINT)
	private Integer ssn;

	@Column("f_name")
	private String firstName;

	@Column
	@Indexed
	private String lastName;

	private Address address;

	@CassandraType(type = CassandraType.Name.UDT, userTypeName = "myusertype")
	private UdtValue usertype;

	private Coordinates coordinates;

	@Transient
	private Integer accountTotal;

	@CassandraType(type = CassandraType.Name.SET, typeArguments = CassandraType.Name.BIGINT)
	private Set<Long> timestamps;

	private Map<@Indexed String, InetAddress> sessions;

	public Person(Integer ssn) {
		this.ssn = ssn;
	}

	public Person.Key getKey() {
		return key;
	}

	// no setter for Id.  (getter is only exposed for some unit testing)

	public Integer getSsn() {
		return ssn;
	}

	public void setFirstName(String firstName) {
		this.firstName = firstName;
	}

	// other getters/setters omitted
}

次の例は、UDT Address をマップする方法を示しています。

例 13: マップされたユーザー定義型 Address
@UserDefinedType("address")
public class Address {

  @CassandraType(type = CassandraType.Name.VARCHAR)
  private String street;

  private String city;

  private Set<String> zipcodes;

  @CassandraType(type = CassandraType.Name.SET, typeArguments = CassandraType.Name.BIGINT)
  private List<Long> timestamps;

  // other getters/setters omitted
}
ユーザー定義型を使用するには、マッピングコンテキストで構成された UserTypeResolver が必要です。UserTypeResolver の設定方法については、設定の章を参照してください。

次の例は、タプルをマップする方法を示しています。

例 14: マップされたタプル
@Tuple
class Coordinates {

  @Element(0)
  @CassandraType(type = CassandraType.Name.VARCHAR)
  private String description;

  @Element(1)
  private long longitude;

  @Element(2)
  private long latitude;

  // other getters/setters omitted
}

インデックスの作成

アプリケーションの起動時にセカンダリインデックスを作成する場合は、特定のエンティティプロパティに @Indexed または @SASI のアノテーションを付けることができます。インデックスを作成すると、スカラー型、ユーザー定義型、コレクション型の単純なセカンダリインデックスが作成されます。

StandardAnalyzer または NonTokenizingAnalyzer (それぞれ @StandardAnalyzed および @NonTokenizingAnalyzed を使用) などのアナライザーを適用するように SASI インデックスを構成できます。

マップ型は ENTRYKEYSVALUES インデックスを区別します。インデックスを作成すると、アノテーションが付けられた要素からインデックス型が派生します。次の例は、インデックスを作成するさまざまな方法を示しています。

例 15: マップのインデックス作成のバリエーション
@Table
class PersonWithIndexes {

  @Id
  private String key;

  @SASI
  @StandardAnalyzed
  private String names;

  @Indexed("indexed_map")
  private Map<String, String> entries;

  private Map<@Indexed String, String> keys;

  private Map<String, @Indexed String> values;

  // …
}

@Indexed アノテーションは、埋め込まれたエンティティの単一プロパティに適用することも、@Embedded アノテーションと一緒に適用することもできます。その場合、埋め込まれたエンティティのすべてのプロパティにインデックスが付けられます。

セッションの初期化時にインデックスを作成すると、アプリケーションの起動時にパフォーマンスに重大な影響を与える可能性があります。