射影

Spring Data クエリメソッドは通常、リポジトリによって管理される集約ルートの 1 つまたは複数のインスタンスを返します。ただし、これらの型の特定の属性に基づいて射影を作成することが望ましい場合があります。Spring Data では、専用の戻り値型をモデル化して、管理対象集合体の部分ビューをより選択的に取得できます。

次の例のようなリポジトリおよび集約ルート型を想像してください。

サンプルの集約とリポジトリ
class Person {

  @Id UUID id;
  String firstname, lastname;
  Address address;

  static class Address {
    String zipCode, city, street;
  }
}

interface PersonRepository extends Repository<Person, UUID> {

  Collection<Person> findByLastname(String lastname);
}

ここで、人の名前属性のみを取得することを想像してください。Spring Data はこれを達成するためにどのような意味を持っていますか? この章の残りはその質問に回答します。

射影型は、エンティティの型階層の外部に存在する型です。エンティティによって実装されたスーパークラスとインターフェースは型階層内にあるため、スーパー型 (または実装されたインターフェース) を返すと、完全にマテリアライズされたエンティティのインスタンスが返されます。

インターフェースベースの射影

クエリの結果を名前属性のみに制限する最も簡単な方法は、次の例に示すように、読み取るプロパティのアクセサーメソッドを公開するインターフェースを宣言することです。

属性のサブセットを取得する射影インターフェース
interface NamesOnly {

  String getFirstname();
  String getLastname();
}

ここで重要なことは、ここで定義されたプロパティが集約ルートのプロパティと正確に一致することです。これにより、クエリメソッドを次のように追加できます。

クエリメソッドでインターフェースベースの射影を使用するリポジトリ
interface PersonRepository extends Repository<Person, UUID> {

  Collection<NamesOnly> findByLastname(String lastname);
}

クエリ実行エンジンは、返された各要素に対して実行時にそのインターフェースのプロキシインスタンスを作成し、公開されたメソッドへの呼び出しをターゲットオブジェクトに転送します。

基本メソッド(たとえば、CrudRepository、ストア固有のリポジトリインターフェース、Simple … Repository で宣言されている)をオーバーライドするメソッドを Repository で宣言すると、宣言された戻り値の型に関係なく、基本メソッドが呼び出されます。基本メソッドは射影に使用できないため、互換性のある戻り値の型を使用してください。一部のストアモジュールは、@Query アノテーションをサポートして、オーバーライドされたベースメソッドをクエリメソッドに変換します。このクエリメソッドを使用して、射影を返すことができます。

射影は再帰的に使用できます。Address 情報の一部も含めたい場合は、次の例に示すように、そのための射影インターフェースを作成し、getAddress() の宣言からそのインターフェースを返します。

属性のサブセットを取得する射影インターフェース
interface PersonSummary {

  String getFirstname();
  String getLastname();
  AddressSummary getAddress();

  interface AddressSummary {
    String getCity();
  }
}

メソッドの呼び出し時に、ターゲットインスタンスの address プロパティが取得され、順番に投影プロキシにラップされます。

閉じた射影

アクセサーメソッドがすべてターゲット集合体のプロパティに一致する射影インターフェースは、閉じた射影と見なされます。次の例(この章の前半でも使用しました)は、閉じた射影です。

閉じた射影
interface NamesOnly {

  String getFirstname();
  String getLastname();
}

閉じた射影を使用する場合、Spring Data はクエリの実行を最適化できます。これは、射影プロキシのバックアップに必要なすべての属性がわかっているためです。詳細については、リファレンスドキュメントのモジュール固有の部分を参照してください。

開いた射影

次の例に示すように、@Value アノテーションを使用して、射影インターフェースのアクセサーメソッドを使用して新しい値を計算することもできます。

開いた射影
interface NamesOnly {

  @Value("#{target.firstname + ' ' + target.lastname}")
  String getFullName();
  …
}

射影を支える集約ルートは、target 変数で利用可能です。@Value を使用した射影インターフェースは、オープン射影です。この場合、Spring Data はクエリ実行最適化を適用できません。これは、SpEL 式が集約ルートの任意の属性を使用できるためです。

@Value で使用される式は複雑すぎてはいけません — String 変数でのプログラミングは避けたいです。非常に単純な式の場合、次の例に示すように、1 つのオプションはデフォルトのメソッド(Java 8 で導入)に頼ることです。

カスタムロジックにデフォルトのメソッドを使用する射影インターフェース
interface NamesOnly {

  String getFirstname();
  String getLastname();

  default String getFullName() {
    return getFirstname().concat(" ").concat(getLastname());
  }
}

このアプローチでは、射影インターフェースで公開される他のアクセサーメソッドに純粋に基づいてロジックを実装できる必要があります。次の例に示すように、2 番目のより柔軟なオプションは、Spring Bean にカスタムロジックを実装し、SpEL 式からそれを呼び出すことです。

サンプル Person オブジェクト
@Component
class MyBean {

  String getFullName(Person person) {
    …
  }
}

interface NamesOnly {

  @Value("#{@myBean.getFullName(target)}")
  String getFullName();
  …
}

SpEL 式が myBean を参照し、getFullName(…) メソッドを呼び出し、射影ターゲットをメソッドパラメーターとして転送する方法に注目してください。SpEL 式の評価に裏付けられたメソッドは、メソッドパラメーターを使用することもできます。このパラメーターは、式から参照できます。メソッドのパラメーターは、args という名前の Object 配列を介して使用できます。次の例は、args 配列からメソッドパラメーターを取得する方法を示しています。

サンプル Person オブジェクト
interface NamesOnly {

  @Value("#{args[0] + ' ' + target.firstname + '!'}")
  String getSalutation(String prefix);
}

繰り返しますが、より複雑な式の場合は、に説明したように、Spring Bean を使用し、式でメソッドを呼び出す必要があります。

null 可能ラッパー

射影インターフェースの Getter は、null 許容ラッパーを使用して null の安全性を向上させることができます。現在サポートされているラッパー型は次のとおりです。

  • java.util.Optional

  • com.google.common.base.Optional

  • scala.Option

  • io.vavr.control.Option

null 許容ラッパーを使用した射影インターフェース
interface NamesOnly {

  Optional<String> getFirstname();
}

基になる射影値が null でない場合、値はラッパー型の現在の表現を使用して返されます。バッキング値が null の場合、getter メソッドは使用されたラッパー型の空の表現を返します。

クラスベースの射影 (DTO)

射影を定義するもう 1 つの方法は、取得することになっているフィールドのプロパティを保持する値型 DTO(データ転送オブジェクト)を使用することです。これらの DTO 型は、プロキシが発生せず、ネストされた射影を適用できないことを除いて、射影インターフェースとまったく同じ方法で使用できます。

ストアがロードするフィールドを制限することでクエリの実行を最適化する場合、ロードされるフィールドは公開されているコンストラクターのパラメーター名から決定されます。

次の例は、投影 DTO を示しています。

投影 DTO
record NamesOnly(String firstname, String lastname) {
}

Java レコードは、値のセマンティクスに準拠しているため、DTO 型を定義するのに理想的です。すべてのフィールドは private final であり、equals(…)/hashCode()/toString() メソッドは自動的に作成されます。または、投影するプロパティを定義する任意のクラスを使用できます。

動的射影

これまで、コレクションの戻り値型または要素型として射影型を使用しました。ただし、呼び出し時に使用する型を選択することもできます(これにより、動的になります)。動的射影を適用するには、次の例に示すようなクエリメソッドを使用します。

動的射影パラメーターを使用するリポジトリ
interface PersonRepository extends Repository<Person, UUID> {

  <T> Collection<T> findByLastname(String lastname, Class<T> type);
}

この方法では、次の例に示すように、メソッドを使用して、そのままで、または射影を適用して集約を取得できます。

動的射影でリポジトリを使用する
void someMethod(PersonRepository people) {

  Collection<Person> aggregates =
    people.findByLastname("Matthews", Person.class);

  Collection<NamesOnly> aggregates =
    people.findByLastname("Matthews", NamesOnly.class);
}
型 Class のクエリパラメーターは、動的射影パラメーターとして適格かどうかがインスペクションされます。クエリの実際の戻り値の型が Class パラメーターのジェネリクスパラメーター型と等しい場合、一致する Class パラメーターはクエリまたは SpEL 式内で使用できません。Class パラメーターをクエリ引数として使用する場合は、必ず別のジェネリクスパラメーター(Class<?> など)を使用してください。