射影

導入

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<?> など)を使用してください。

クラスベースの射影を使用する場合、型は単一のコンストラクターを宣言して、Spring Data が入力プロパティを決定できるようにする必要があります。クラスが複数のコンストラクターを定義している場合は、DTO 射影の追加のヒントなしで型を使用することはできません。このような場合は、以下に示すように、必要なコンストラクターに @PersistenceCreator をアノテーション付けして、Spring Data が選択するプロパティを決定できるようにします。

public class NamesOnly {

  private final String firstname;
  private final String lastname;

  protected NamesOnly() { }

  @PersistenceCreator
  public NamesOnly(String firstname, String lastname) {
      this.firstname = firstname;
      this.lastname = lastname;
  }

  // ...
}

JPA での射影の使用

JPA では、いくつかの方法で Projections を使用できます。手法とクエリ型に応じて、特定の考慮事項を適用する必要があります。

Spring Data JPA は通常、Tuple クエリを使用してインターフェースベースの射影のインターフェースプロキシを構築します。

派生クエリ

クエリ派生は、返された型をイントロスペクトすることにより、クラスベースとインターフェースの両方の射影をサポートします。クラスベースの射影は、JPA のインスタンス化メカニズム (コンストラクター式) を使用して射影インスタンスを作成します。

射影は、ターゲットエンティティの最上位プロパティへの選択を制限します。結合に解決されるネストされたプロパティは、ネストされたプロパティ全体を選択し、完全な結合を実現します。

文字列ベースのクエリ

文字列ベースのクエリのサポートは、JPQL クエリ (@Query) とネイティブクエリ (@NativeQuery) の両方をカバーします。

JPQL クエリ

JPA が JPQL を使ってクラスベースの射影を返す仕組みは、コンストラクター式です。クエリでは SELECT new com.example.NamesOnly(u.firstname, u.lastname) from User u のようなコンストラクター式を定義する必要があります。(DTO 型に FQDN を使用することに注意してください)この JPQL 式は、@Query アノテーションでも、名前付きクエリを定義する際に使用できます。回避策として、ResultSetMapping または Hibernate 固有の ResultListTransformer (英語) を使用した名前付きクエリを使用することもできます。

Spring Data JPA は、クエリがプライマリエンティティまたは選択項目のリストを選択する場合に、クエリをコンストラクター式に書き換えるのに役立ちます。

DTO 射影 JPQL クエリ書き換え

JPQL クエリでは、コンストラクター式を使用してルートオブジェクト、個々のプロパティ、DTO オブジェクトを選択できます。コンストラクター式を使用すると、クエリに大量のテキストがすぐに追加され、実際のクエリが読みにくくなる可能性があります。Spring Data JPA は、利便性のためにコンストラクター式を導入することで、JPQL クエリをサポートします。

次のクエリを検討してください。

例 1: 射影クエリ
interface UserRepository extends Repository<User, Long> {

  @Query("SELECT u FROM USER u WHERE u.lastname = :lastname")                       (1)
  List<UserDto> findByLastname(String lastname);

  @Query("SELECT u.firstname, u.lastname FROM USER u WHERE u.lastname = :lastname") (2)
  List<UserDto> findMultipleColumnsByLastname(String lastname);
}

record UserDto(String firstname, String lastname){}
1 最上位エンティティの選択。このクエリは SELECT new UserDto(u.firstname, u.lastname) FROM USER u WHERE u.lastname = :lastname に書き換えられます。
2firstname および lastname プロパティの複数選択。このクエリは SELECT new UserDto(u.firstname, u.lastname) FROM USER u WHERE u.lastname = :lastname に書き換えられます。

JPQL コンストラクター式には、選択した列のエイリアスを含めることはできません。また、クエリ書き換えによってエイリアスが削除されることはありません。SELECT u as user, count(u.roles) as roleCount FROM USER u …  は、返された Tuple の列名に依存するインターフェースベースの射影では有効なクエリですが、DTO をリクエストする際に SELECT u, count(u.roles) FROM USER u …が必要となる場合は、同じ構文は無効です。
永続化プロバイダの中には、この点に関して寛容なプロバイダもありますが、そうでないプロバイダもあります。

DTO 射影型(ドメイン型階層外の Java 型)を返すリポジトリクエリメソッドは、クエリ書き換えの対象となります。@Query アノテーション付きクエリがすでにコンストラクター式を使用している場合、Spring Data は DTO コンストラクター式の書き換えを適用しません。

DTO 型が射影のすべての引数コンストラクターを提供していることを確認してください。そうでない場合、クエリは失敗します。

ネイティブクエリ

クラスベースの射影を使用する場合、以下の点に応じて、使用方法を少し考慮する必要があります。

  • 結果の型のプロパティが結果に直接マップされる場合 (列の順序とその型がコンストラクターの引数と一致する場合)、追加のヒントなしでクエリ結果の型を DTO 型として宣言できます (または、動的射影を通じて DTO クラスを使用します)。

  • プロパティが一致しない、または変換が必要な場合は、JPA のアノテーションを通じて @SqlResultSetMapping を使用して結果セットを DTO にマップし、@NativeQuery(resultSetMapping = " … ") を通じて結果マッピング名を提供します。