事前の最適化

この章では、Spring の事前最適化を基盤とした Spring Data の Ahead of Time (AOT) 最適化について説明します。

ベストプラクティス

ドメイン型にアノテーションを付ける

アプリケーションの起動時に、Spring はエンティティの初期処理のためにクラスパスをスキャンし、ドメインクラスを探します。ドメイン型に Spring データ固有の @Table@Document@Entity アノテーションを付与することで、初期のエンティティスキャンを容易にし、それらの型がランタイムヒントの ManagedTypes に登録されるようにすることができます。ネイティブイメージ配置ではクラスパススキャンが不可能なため、Spring は初期エンティティセットに ManagedTypes を使用する必要があります。

実行時のヒント

アプリケーションをネイティブイメージとして実行するには、通常の JVM ランタイムに比べて追加の情報が必要です。Spring Data は、ネイティブイメージの使用に関する AOT 処理中に実行時のヒントを提供します。これらは特に、以下の点に関するヒントです。

  • 監査

  • クラスパススキャンの結果をキャプチャーするための ManagedTypes 

  • リポジトリ

    • エンティティ、戻り値の型、Spring Data アノテーションのリフレクションヒント

    • リポジトリフラグメント

    • Querydsl Q クラス

    • Kotlin コルーチンサポート

  • Web サポート (Jackson の PagedModel のヒント)

事前リポジトリ

AOT リポジトリは、適切なクエリメソッド実装を事前に生成することで AOT 処理を拡張するものです。クエリメソッドは、その呼び出しで実行されるクエリについて開発者には不透明です。AOT リポジトリは、ビルド時に既知の派生クエリ、アノテーション付きクエリ、名前付きクエリに基づいて、クエリメソッド実装を提供します。この最適化により、クエリメソッド処理が実行時からビルド時へと移行されます。これにより、アプリケーションの起動時にクエリメソッドをリフレクション的に分析する必要がなくなり、パフォーマンスが大幅に向上します。

生成された AOT リポジトリフラグメントは <Repository FQCN>Impl__Aot の命名規則に従い、リポジトリインターフェースと同じパッケージに配置されます。生成されたリポジトリクエリメソッドのすべてのクエリは、文字列形式で確認できます。

AOT リポジトリクラスは内部最適化を目的としています。生成と実装の詳細は将来のリリースで変更される可能性があるため、コード内で直接使用しないでください。

AOT リポジトリでの実行

AOT は Spring アプリケーションをネイティブ実行ファイルに変換するための必須ステップであるため、このモードで実行する場合は自動的に有効になります。AOT が有効になっている場合(ネイティブコンパイルの場合、または spring.aot.enabled=true の設定による場合)、AOT リポジトリも生成されます。

AOT リポジトリ生成を完全に無効にすることも、JDBC AOT リポジトリのみを無効にすることもできます。

  • すべての Spring Data モジュールに対して生成されたリポジトリを無効にするには、spring.aot.repositories.enabled=false プロパティを設定します。

  • spring.aot.jdbc.repositories.enabled=false プロパティを設定して、JDBC AOT リポジトリのみを無効にします。

AOT リポジトリは、生成されたリポジトリフラグメントを登録するために、実際のリポジトリ Bean 登録に構成変更を提供します。

AOT 最適化が組み込まれると、ビルド時に行われた一部の決定がアプリケーション設定にハードコードされます。たとえば、ビルド時に有効化されたプロファイルは、実行時にも自動的に有効化されます。また、リポジトリを実装する Spring Data モジュールは修正されています。実装を変更すると、AOT の再処理が必要になります。
ダイアレクト検出による早期のデータベースアクセスを回避するには、JdbcDialect を指定してください。

適格な方法

AOT リポジトリは、AOT 処理の対象となるメソッドをフィルタリングします。これらのメソッドは通常、実装フラグメントに基づかないすべてのクエリメソッドです。

サポートされている機能

  • 派生クエリメソッド、@Query、名前付きクエリメソッド

  •  voidintlong を返す @Modifying メソッド

  • ページネーション、SliceStreamOptional 戻り値の型

  • DTO とインターフェース射影

  • 値式

制限

  • ScrollPosition を受け入れるメソッド(例: Keyset ページネーション)はまだサポートされていません

除外された方法

  • CrudRepository、Querydsl、Query by Example、およびその他の基本インターフェースメソッドは、その実装が基本クラスのそれぞれのフラグメントによって提供されるため

  • 実装が過度に複雑になる方法

リポジトリメタデータ

AOT 処理はクエリメソッドをイントロスペクトし、リポジトリクエリに関するメタデータを収集します。Spring Data JDBC は、このメタデータをリポジトリインターフェースと同じ名前の JSON ファイルに保存し、リポジトリインターフェースと同列(つまり同じパッケージ内)に保存します。リポジトリ JSON メタデータには、クエリとフラグメントに関する詳細情報が含まれます。以下のリポジトリの例を以下に示します。

interface UserRepository extends CrudRepository<User, Integer> {

  List<User> findUserNoArgumentsBy();                                                  (1)

  Page<User> findPageOfUsersByLastnameStartingWith(String lastname, Pageable page);    (2)

  @Query("select * from User u where username = ?1")
  User findAnnotatedQueryByEmailAddress(String username);                              (3)

  User findByEmailAddress(String emailAddress);                                        (4)
}
1 引数のない派生クエリ。
2 ページ区切りを使用した派生クエリ。
3 アノテーション付きクエリ。
4 名前付きクエリ。ストアドプロシージャメソッドは JSON メタデータに含まれていますが、そのメソッドコードブロックは AOT リポジトリに生成されません。
{
  "name": "com.acme.UserRepository",
  "module": "JDBC",
  "type": "IMPERATIVE",
  "methods": [
    {
      "name": "findUserNoArgumentsBy",
      "signature": "public abstract java.util.List<com.acme.User> com.acme.UserRepository.findUserNoArgumentsBy()",
      "query": {
        "query": "SELECT * FROM User"
      }
    },
    {
      "name": "findPageOfUsersByLastnameStartingWith",
      "signature": "public abstract org.springframework.data.domain.Page<com.acme.User> com.acme.UserRepository.findPageOfUsersByLastnameStartingWith(java.lang.String,org.springframework.data.domain.Pageable)",
      "query": {
        "query": "SELECT * FROM User u WHERE lastname LIKE :lastname",
        "count-query": "SELECT COUNT(*) FROM User WHERE lastname LIKE :lastname"
      }
    },
    {
      "name": "findAnnotatedQueryByEmailAddress",
      "signature": "public abstract com.acme.User com.acme.UserRepository.findAnnotatedQueryByEmailAddress(java.lang.String)",
      "query": {
        "query": "select * from User where emailAddress = ?1"
      }
    },
    {
      "name": "findByEmailAddress",
      "signature": "public abstract com.acme.User com.acme.UserRepository.findByEmailAddress(java.lang.String)",
      "query": {
        "name": "User.findByEmailAddress",
        "query": "SELECT * FROM User WHERE emailAddress = ?1"
      }
    },
    {
      "name": "count",
      "signature": "public abstract long org.springframework.data.repository.CrudRepository.count()",
      "fragment": {
        "fragment": "org.springframework.data.jdbc.repository.support.SimpleJdbcRepository"
      }
    }
  ]
}

クエリには次のフィールドが含まれる場合があります。

  • query: メソッドがクエリメソッドである場合のクエリ記述子。

    • name: クエリが名前付きクエリである場合は、名前付きクエリの名前。

    • query は EntityManager からのクエリメソッドの結果を取得するために使用されるクエリです

    • count-name: カウントクエリが名前付きカウントクエリである場合は、名前付きカウントクエリの名前。

    • count-query: ページ区切りを使用するクエリメソッドのカウントを取得するために使用されるカウントクエリ。

  • fragment: メソッド呼び出しがストア(リポジトリの基底クラス、Querydsl などの関数フラグメント)またはユーザーフラグメントに委譲されている場合のターゲットフラグメント。フラグメントは、他にインターフェースがない場合は fragment のみで記述され、インターフェース(Querydsl やユーザー宣言のフラグメントインターフェースなど)がある場合は interface と fragment のタプルで記述されます。

正規化されたクエリフォーム

クエリの静的解析では、実行時のクエリの挙動を限定的にしか表現できません。クエリは正規化された(事前に解析され書き換えられた)形式で表現されます。

  • 値式はバインドマーカーに置き換えられます。

  • クエリメタデータはバインド値の処理を反映しません。StartingWith/EndingWith クエリは、ワイルドカード文字 % を実際のバインド値の先頭 / 末尾に追加します。

  • 実行時のソート情報はビルド時に詳細が不明であるため、クエリ文字列自体に組み込むことはできません。