オブジェクトマッピングの基礎
このセクションでは、Spring Data オブジェクトマッピング、オブジェクト作成、フィールドとプロパティへのアクセス、可変性と不変性の基礎について説明します。このセクションは、基になるデータストア(JPA など)のオブジェクトマッピングを使用しない Spring Data モジュールにのみ適用されることに注意してください。また、インデックス、列名やフィールド名のカスタマイズなど、ストア固有のオブジェクトマッピングについては、ストア固有のセクションを参照してください。
Spring Data オブジェクトマッピングの中心的なロールは、ドメインオブジェクトのインスタンスを作成し、ストアネイティブデータ構造をそれらにマッピングすることです。つまり、2 つの基本的な手順が必要です。
公開されたコンストラクターの 1 つを使用したインスタンスの作成。
すべての公開されたプロパティを具体化するインスタンスの設定。
オブジェクト作成
Spring Data は、その型のオブジェクトの具体化に使用される永続エンティティのコンストラクターを自動的に検出しようとします。解決アルゴリズムは次のように機能します。
@PersistenceCreator
でアノテーションが付けられた単一の静的ファクトリメソッドがある場合は、それが使用されます。コンストラクターが 1 つしかない場合は、それが使用されます。
複数のコンストラクターがあり、そのうちの 1 つだけに
@PersistenceCreator
アノテーションが付けられている場合は、それが使用されます。型が Java
Record
の場合、標準コンストラクターが使用されます。引数のないコンストラクターがある場合は、それが使用されます。他のコンストラクターは無視されます。
値の解決では、コンストラクター / ファクトリメソッドの引数名がエンティティのプロパティ名と一致することを前提としています。つまり、マッピングのすべてのカスタマイズ(異なるデータストア列またはフィールド名など)を含め、プロパティが入力されたかのように解決が実行されます。これには、クラスファイルで利用可能なパラメーター名情報、またはコンストラクターに存在する @ConstructorProperties
アノテーションも必要です。
値の解決は、ストア固有の SpEL 式を使用した Spring Framework の @Value
値アノテーションを使用してカスタマイズできます。詳細については、ストア固有のマッピングに関するセクションを参照してください。
プロパティ設定
エンティティのインスタンスが作成されると、Spring Data はそのクラスの残りのすべての永続プロパティを設定します。エンティティのコンストラクターによってすでに入力されていない場合(つまり、コンストラクターの引数リストを介して使用される場合)、ID プロパティが最初に入力され、循環オブジェクト参照の解決が可能になります。その後、コンストラクターによってまだ設定されていないすべての非一時的なプロパティがエンティティインスタンスに設定されます。そのために、次のアルゴリズムを使用します。
プロパティが不変であるが
with …
メソッドを公開している場合(以下を参照)、with …
メソッドを使用して、新しいプロパティ値を持つ新しいエンティティインスタンスを作成します。プロパティアクセス(つまり、getter および setter を介したアクセス)が定義されている場合、setter メソッドを呼び出しています。
プロパティが変更可能な場合、フィールドを直接設定します。
プロパティが不変の場合、永続化操作(オブジェクト作成を参照)で使用されるコンストラクターを使用して、インスタンスのコピーを作成します。
デフォルトでは、フィールド値を直接設定します。
次のエンティティを見てみましょう。
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;
}
}
1 | identifier プロパティは final ですが、コンストラクターで null に設定されます。クラスは、識別子の設定に使用される withId(…) メソッドを公開します。インスタンスがデータストアに挿入され、識別子が生成されたとき。元の Person インスタンスは、新しいインスタンスが作成されるときに変更されません。通常、ストア管理される他のプロパティにも同じパターンが適用されますが、永続化操作のために変更する必要がある場合があります。永続化コンストラクター(6 を参照)は事実上コピーコンストラクターであり、プロパティの設定は新しい識別子値が適用された新しいインスタンスの作成に変換されるため、wither メソッドはオプションです。 |
2 | firstname および lastname プロパティは、getter を介して潜在的に公開される通常の不変のプロパティです。 |
3 | age プロパティは不変ですが、birthday プロパティから派生しています。示されている設計では、Spring Data は宣言された唯一のコンストラクターを使用するため、データベース値はデフォルト設定よりも優先されます。計算が優先されることを意図している場合でも、このコンストラクターがパラメーターとして age を受け取ることが重要です(無視される可能性があります)。そうしないと、プロパティ生成ステップは age フィールドを設定しようとし、不変で no with … メソッドが存在します。 |
4 | comment プロパティは変更可能で、そのフィールドを直接設定することによって入力されます。 |
5 | remarks プロパティは変更可能で、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 モジュールは通常、異なる値を保持するオーバーライドされたプロパティをサポートします。プログラミングモデルの観点から、考慮すべきことがいくつかあります。
どのプロパティを永続化する必要がありますか(デフォルトでは、宣言されたすべてのプロパティになります)? これらに
@Transient
アノテーションを付けることで、プロパティを除外できます。データストアのプロパティを表す方法は? 異なる値に同じフィールド / 列名を使用すると、通常、データが破損するため、明示的なフィールド / 列名を使用してプロパティの少なくとも 1 つにアノテーションを付ける必要があります。
@AccessType(PROPERTY)
を使用することは、通常、setter 実装のさらなる仮定を行わずにスーパープロパティを設定することができないため、使用できません。
Kotlin サポート
Spring Data は、Kotlin の仕様を適合させて、オブジェクトの作成と変更を可能にします。
Kotlin オブジェクトの作成
Kotlin クラスはインスタンス化がサポートされています。すべてのクラスはデフォルトで不変であり、変更可能なプロパティを定義するには明示的なプロパティ宣言が必要です。
Spring Data は、その型のオブジェクトの具体化に使用される永続エンティティのコンストラクターを自動的に検出しようとします。解決アルゴリズムは次のように機能します。
@PersistenceCreator
でアノテーションが付けられたコンストラクターがある場合は、それが使用されます。型が Kotlin データクラスの場合、プライマリコンストラクターが使用されます。
@PersistenceCreator
でアノテーションが付けられた単一の静的ファクトリメソッドがある場合は、それが使用されます。コンストラクターが 1 つしかない場合は、それが使用されます。
複数のコンストラクターがあり、そのうちの 1 つだけに
@PersistenceCreator
アノテーションが付けられている場合は、それが使用されます。型が Java
Record
の場合、標準コンストラクターが使用されます。引数のないコンストラクターがある場合は、それが使用されます。他のコンストラクターは無視されます。
次の 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 モジュールは通常、異なる値を保持するオーバーライドされたプロパティをサポートします。プログラミングモデルの観点から、考慮すべきことがいくつかあります。
どのプロパティを永続化する必要がありますか(デフォルトでは、宣言されたすべてのプロパティになります)? これらに
@Transient
アノテーションを付けることで、プロパティを除外できます。データストアのプロパティを表す方法は? 異なる値に同じフィールド / 列名を使用すると、通常、データが破損するため、明示的なフィールド / 列名を使用してプロパティの少なくとも 1 つにアノテーションを付ける必要があります。
@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)
1 | Null 非許容値型を持つ単純な値クラス。 |
2 | EmailAddress 値クラスを使用してプロパティを定義するデータクラス。 |
非プリミティブ値型を使用する null 非許容プロパティは、コンパイルされたクラスで値型にフラット化されます。Null 許容プリミティブ値型または Null 許容値内値型は、ラッパー型で表現され、データベース内での値型の表現方法に影響します。 |