リクエスト実行

ExecutionGraphQlService は、GraphQL Java を呼び出してリクエストを実行するための主要な Spring 抽象化です。HTTP などの基礎となるトランスポートは、リクエストを処理するために ExecutionGraphQlService に委譲します。

主な実装である DefaultExecutionGraphQlService は、起動する graphql.GraphQL インスタンスにアクセスするための GraphQlSource で構成されています。

GraphQLSource

GraphQlSource は、使用する graphql.GraphQL インスタンスを公開するための契約であり、そのインスタンスを構築するためのビルダー API も含まれています。デフォルトのビルダーは GraphQlSource.schemaResourceBuilder() から入手できます。

Boot スターターはこのビルダーのインスタンスを作成し、それをさらに初期化して、構成可能な場所からスキーマファイルをロードしGraphQlSource.Builder に適用するプロパティを公開しRuntimeWiringConfigurer Bean、GraphQL メトリクス計測 (英語) Bean、および例外解決用の DataFetcherExceptionResolver および SubscriptionExceptionResolver Bean を検出します。さらにカスタマイズするには、GraphQlSourceBuilderCustomizer Bean を宣言することもできます。例:

@Configuration(proxyBeanMethods = false)
class GraphQlConfig {

	@Bean
	public GraphQlSourceBuilderCustomizer sourceBuilderCustomizer() {
		return (builder) ->
				builder.configureGraphQl(graphQlBuilder ->
						graphQlBuilder.executionIdProvider(new CustomExecutionIdProvider()));
	}
}

スキーマリソース

GraphQlSource.Builder は、1 つ以上の Resource インスタンスを使用して構成し、解析およびマージすることができます。つまり、スキーマファイルはほぼすべての場所からロードできます。

デフォルトでは、Boot スターターは、場所 classpath:graphql/** (通常は src/main/resources/graphql) で拡張子が ".graphqls" または ".gqls" のスキーマファイルを探します。ファイルシステムの場所、または Spring Resource 階層でサポートされている任意の場所を使用することもできます。これには、リモート の場所、ストレージ、メモリからスキーマファイルをロードするカスタム実装が含まれます。

classpath*:graphql/**/ を使用して、複数のクラスパスの場所にまたがるスキーマファイルを検索します。複数のモジュールにわたって。

スキーマの作成

デフォルトでは、GraphQlSource.Builder は GraphQL Java SchemaGenerator を使用して graphql.schema.GraphQLSchema を作成します。これは一般的な用途では機能しますが、別のジェネレーターを使用する必要がある場合は、schemaFactory コールバックを登録できます。

GraphQlSource.Builder builder = ...

builder.schemaResources(..)
		.configureRuntimeWiring(..)
		.schemaFactory((typeDefinitionRegistry, runtimeWiring) -> {
			// create GraphQLSchema
		})

Spring Boot でこれを構成する方法については、GraphQlSource セクションを参照してください。

連盟に興味がある場合は、フェデレーションセクションを参照してください。

RuntimeWiringConfigurer

RuntimeWiringConfigurer は、次のものを登録できます。

  • カスタムスカラー型。

  • ディレクティブを処理するコード。

  • 直接 DataFetcher 登録。

  • さらに…

Spring アプリケーションは通常、直接 DataFetcher 登録を実行する必要はありません。代わりに、コントローラーメソッドは、RuntimeWiringConfigurer である AnnotatedControllerConfigurer を介して DataFetcher として登録されます。
GraphQL Java、サーバーアプリケーションは、Jackson をデータのマップとの間の直列化にのみ使用します。クライアント入力はマップに解析されます。サーバー出力は、フィールド選択セットに基づいてマップにアセンブルされます。これは、Jackson シリアライゼーション / デシリアライゼーションアノテーションに依存できないことを意味します。代わりに、カスタムスカラー型 (英語) を使用できます。

Boot スターターは、型 RuntimeWiringConfigurer の Bean を検出し、GraphQlSource.Builder に登録します。つまり、ほとんどの場合、構成には次のようなものがあります。

@Configuration
public class GraphQlConfig {

	@Bean
	public RuntimeWiringConfigurer runtimeWiringConfigurer(BookRepository repository) {
		GraphQLScalarType scalarType = ... ;
		SchemaDirectiveWiring directiveWiring = ... ;
		return wiringBuilder -> wiringBuilder
				.scalar(scalarType)
				.directiveWiring(directiveWiring);
	}
}

WiringFactory を追加する必要がある場合。スキーマ定義を考慮して登録を行うには、RuntimeWiring.Builder と出力 List<WiringFactory> の両方を受け入れる代替 configure メソッドを実装します。これにより、任意の数のファクトリを追加して、順番に呼び出すことができます。

TypeResolver

GraphQlSource.Builder は、RuntimeWiringConfigurer を介してまだ登録されていない GraphQL インターフェースおよびユニオンに使用するデフォルトの TypeResolver として ClassNameTypeResolver を登録します。GraphQL Java の TypeResolver の目的は、GraphQL インターフェースまたは Union フィールドの DataFetcher から返される値の GraphQL オブジェクト型を決定することです。

ClassNameTypeResolver は、値の単純なクラス名を GraphQL オブジェクト型に一致させようとします。一致しない場合は、基本クラスやインターフェースを含むスーパー型をナビゲートして、一致を探します。ClassNameTypeResolver は、Class から GraphQL オブジェクト型名へのマッピングとともに、名前抽出関数を構成するオプションを提供します。これは、より多くのコーナーケースをカバーできます。

GraphQlSource.Builder builder = ...
ClassNameTypeResolver classNameTypeResolver = new ClassNameTypeResolver();
classNameTypeResolver.setClassNameExtractor((klass) -> {
	// Implement Custom ClassName Extractor here
});
builder.defaultTypeResolver(classNameTypeResolver);

Spring Boot でこれを構成する方法については、GraphQlSource セクションを参照してください。

ディレクティブ

GraphQL 言語は、「GraphQL ドキュメントで代替ランタイム実行と型検証動作を記述する」ディレクティブをサポートしています。ディレクティブは Java のアノテーションに似ていますが、GraphQL ドキュメントの型、フィールド、フラグメント、操作で宣言されています。

GraphQL Java は、アプリケーションがディレクティブを検出して処理するのに役立つ SchemaDirectiveWiring 契約を提供します。詳細については、GraphQL Java ドキュメントのスキーマディレクティブ (英語) を参照してください。

Spring GraphQL では、RuntimeWiringConfigurer を介して SchemaDirectiveWiring を登録できます。Boot スターターはそのような Bean を検出するため、次のようなものになる可能性があります。

@Configuration
public class GraphQlConfig {

	 @Bean
	 public RuntimeWiringConfigurer runtimeWiringConfigurer() {
		  return builder -> builder.directiveWiring(new MySchemaDirectiveWiring());
	 }

}
ディレクティブサポートの例については、Graphql Java の拡張検証 [GitHub] (英語) ライブラリを確認してください。

ExecutionStrategy

GraphQL Java の ExecutionStrategy は、リクエストされたフィールドのフェッチを実行します。ExecutionStrategy を作成するには、DataFetcherExceptionHandler を提供する必要があります。デフォルトでは、Spring for GraphQL は例外に従って使用する例外ハンドラーを作成し、それを GraphQL.Builder に設定します。次に、GraphQL Java はそれを使用して、構成された例外ハンドラーを持つ AsyncExecutionStrategy インスタンスを作成します。

カスタム ExecutionStrategy を作成する必要がある場合は、同じ方法で DataFetcherExceptionResolver を検出し、例外ハンドラーを作成し、それを使用してカスタム ExecutionStrategy を作成できます。例: Spring Boot アプリケーションの場合:

@Bean
GraphQlSourceBuilderCustomizer sourceBuilderCustomizer(
		ObjectProvider<DataFetcherExceptionResolver> resolvers) {

	DataFetcherExceptionHandler exceptionHandler =
			DataFetcherExceptionResolver.createExceptionHandler(resolvers.stream().toList());

	AsyncExecutionStrategy strategy = new CustomAsyncExecutionStrategy(exceptionHandler);

	return sourceBuilder -> sourceBuilder.configureGraphQl(builder ->
			builder.queryExecutionStrategy(strategy).mutationExecutionStrategy(strategy));
}

スキーマ変換

スキーマの作成後にスキーマをトラバースして変換し、スキーマに変更を加えたい場合は、builder.schemaResources(..).typeVisitorsToTransformSchema(..) を介して graphql.schema.GraphQLTypeVisitor を登録できます。これはスキーマトラバーサルよりもコストがかかるため、スキーマを変更する必要がない限り、通常は変換よりもトラバーサルを優先することに注意してください。

スキーマトラバーサル

スキーマの作成後にスキーマをトラバースし、変更を GraphQLCodeRegistry に適用する場合は、builder.schemaResources(..).typeVisitors(..) を介して graphql.schema.GraphQLTypeVisitor を登録できます。ただし、そのような訪問者はスキーマを変更できないことに注意してください。スキーマを変更する必要がある場合は、スキーマ変換を参照してください。

スキーママッピングインスペクション

クエリ、ミューテーション、サブスクリプション操作に DataFetcher がない場合、データは返されず、役立つことは何もありません。同様に、DataFetcher 登録によって明示的にカバーされず、一致する Class プロパティを見つけるデフォルトの PropertyDataFetcher によって暗黙的にカバーされないスキーマ型のフィールドは、常に null になります。

GraphQL Java は、すべてのスキーマフィールドがカバーされていることを確認するためのチェックを実行しません。また、下位レベルのライブラリである GraphQL Java は、DataFetcher が何を返すことができるのか、どの引数に依存するのかを単に知らないため、そのような検証を実行できません。これにより、テストカバレッジによっては、クライアントで「サイレント」 null 値または null 以外のフィールドエラーが発生する可能性がある実行時までギャップが検出されない可能性があります。

Spring for GraphQL の SelfDescribingDataFetcher インターフェースを使用すると、DataFetcher は戻り値の型や予期される引数などの情報を公開できます。コントローラーメソッドQuerydsl例示による問い合わせのすべての組み込み Spring DataFetcher 実装は、このインターフェースの実装です。アノテーション付きコントローラーの場合、戻り値の型と予期される引数はコントローラーのメソッドシグネチャーに基づきます。これにより、起動時にスキーママッピングをインスペクションして次のことを確認できるようになります。

  • スキーマフィールドには、DataFetcher 登録または対応する Class プロパティのいずれかがあります。

  • DataFetcher 登録は、存在するスキーマフィールドを参照します。

  • DataFetcher 引数には、一致するスキーマフィールド引数があります。

スキーマインスペクション を有効にするには、以下に示すように GraphQlSource.Builder をカスタマイズします。この場合、レポートは単にログに記録されますが、任意のアクションを実行することもできます。

GraphQlSource.Builder builder = ...

builder.schemaResources(..)
		.inspectSchemaMappings(report -> {
			logger.debug(report);
		});

レポートの例:

GraphQL schema inspection:
    Unmapped fields: {Book=[title], Author[firstName, lastName]} (1)
    Unmapped registrations: {Book.reviews=BookController#reviews[1 args]} (2)
    Unmapped arguments: {BookController#bookSearch[1 args]=[myAuthor]} (3)
    Skipped types: [BookOrAuthor] (4)
1 まったくカバーされていないスキーマフィールド
2 存在しないフィールドへの DataFetcher 登録
3DataFetcher は存在しない引数を期待しました
4 スキップされたスキーマ型 (次に説明します)

場合によっては、スキーマ型の Class 型が不明であることがあります。おそらく DataFetcher が SelfDescribingDataFetcher を実装していないか、宣言された戻り値の型が一般的すぎるか (例: Object)、または不明であるか (例: List<?>)、あるいは DataFetcher が完全に欠落している可能性があります。このような場合、スキーマ型は検証できなかったためスキップされたものとしてリストされます。スキップされた型ごとに、スキップされた理由を説明する DEBUG メッセージが表示されます。

ユニオンとインターフェース

共用体の場合、インスペクション はメンバー型を反復処理し、対応するクラスを見つけようとします。インターフェースの場合、インスペクション は実装型を反復処理し、対応するクラスを探します。

デフォルトでは、次の場合に、対応する Java クラスがすぐに検出されます。

  • Class の単純名は、インターフェース実装型名の GraphQL ユニオンメンバーと一致し Class は、ユニオンまたはインターフェースフィールドにマップされたコントローラーメソッドまたはコントローラークラスの戻り値の型と同じパッケージに配置されます。

  • Class は、マップされたフィールドが具体的なユニオンメンバーまたはインターフェース実装型であるスキーマの他の部分でインスペクションされます。

  • 明示的な Class から GraphQL 型へのマッピングを持つ TypeResolver を登録しました。

上記のヘルプのいずれにも該当せず、GraphQL 型がスキーマインスペクションレポートでスキップされたと報告される場合は、次のカスタマイズを行うことができます。

  • GraphQL 型名を Java クラスに明示的にマップします。

  • GraphQL 型名を単純な Class 名に適合させる方法をカスタマイズする関数を構成します。これは、特定の Java クラスの命名規則に役立ちます。

  • GraphQL 型を Java クラスにマッピングするための ClassNameTypeResolver を提供します。

例:

GraphQlSource.Builder builder = ...

builder.schemaResources(..)
	.inspectSchemaMappings(
		initializer -> initializer.classMapping("Author", Author.class)
		logger::debug);

オペレーションキャッシング

GraphQL Java は、操作を実行する前に、操作を解析して検証する必要があります。これは、パフォーマンスに大きな影響を与える可能性があります。再解析と検証の必要性を回避するために、アプリケーションは Document インスタンスをキャッシュして再利用する PreparsedDocumentProvider を構成できます。GraphQL Java ドキュメント (英語) は、PreparsedDocumentProvider を介したクエリキャッシュの詳細を提供します。

Spring GraphQL では、GraphQlSource.Builder#configureGraphQl を通じて PreparsedDocumentProvider を登録できます。

// Typically, accessed through Spring Boot's GraphQlSourceBuilderCustomizer
GraphQlSource.Builder builder = ...

// Create provider
PreparsedDocumentProvider provider =
        new ApolloPersistedQuerySupport(new InMemoryPersistedQueryCache(Collections.emptyMap()));

builder.schemaResources(..)
		.configureRuntimeWiring(..)
		.configureGraphQl(graphQLBuilder -> graphQLBuilder.preparsedDocumentProvider(provider))

Spring Boot でこれを構成する方法については、GraphQlSource セクションを参照してください。

スレッドモデル

ほとんどの GraphQL リクエストは、ネストされたフィールドの取得における同時実行の恩恵を受けます。これが、今日のほとんどのアプリケーションが GraphQL Java の AsyncExecutionStrategy に依存している理由です。これにより、データフェッチャーは CompletionStage を返し、シリアルではなく同時実行が可能になります。

Java 21 と仮想スレッドは、より多くのスレッドを効率的に使用するための重要な機能を追加しますが、リクエストの実行をより迅速に完了するには、シリアルではなく同時に実行する必要があります。

Spring for GraphQL は以下をサポートします。

  • リアクティブデータフェッチャーであり、これらは AsyncExecutionStrategy によって期待されるように CompletionStage に適応されます。

  • 戻り値として CompletionStage

  • Kotlin コルーチンメソッドであるコントローラーメソッド。

  • @SchemaMapping および @BatchMapping メソッドは、Spring Framework VirtualThreadTaskExecutor などの Executor に送信された Callable を返すことができます。これを有効にするには、AnnotatedControllerConfigurer で Executor を構成する必要があります。

Spring for GraphQL は、トランスポートとして Spring MVC または WebFlux のいずれかで実行されます。Spring MVC は、結果の CompletableFuture が GraphQL Java エンジンが返された直後に実行されない限り、非同期リクエスト実行を使用します。これは、リクエストが十分に単純で、非同期データフェッチを必要としない場合に当てはまります。

リアクティブ DataFetcher

デフォルトの GraphQlSource ビルダーは、DataFetcher が Mono または Flux を返すためのサポートを有効にします。これは、Flux 値が集約されてリストに変換される CompletableFuture に適応させます。ただし、リクエストが GraphQL サブスクリプションリクエストである場合を除きます。この場合、戻り値は Reactive Streams Publisher のままです。GraphQL レスポンスのストリーミング用。

リアクティブ DataFetcher は、トランスポート層 (WebFlux リクエスト処理など) から伝播された Reactor コンテキストへのアクセスに依存できます。WebFlux コンテキストを参照してください。

コンテキストの伝播

Spring for GraphQL は、HTTP トランスポートから GraphQL Java を介して、DataFetcher およびそれが呼び出す他のコンポーネントにコンテキストを透過的に伝播するためのサポートを提供します。これには、Spring MVC リクエスト処理スレッドからの ThreadLocal コンテキストと WebFlux 処理パイプラインからの Reactor Context の両方が含まれます。

WebMvc

GraphQL Java によって呼び出される DataFetcher およびその他のコンポーネントは、たとえば非同期 WebGraphQlInterceptor または DataFetcher が別のスレッドに切り替わる場合など、常に Spring MVC ハンドラーと同じスレッドで実行されるとは限りません。

Spring for GraphQL は、サーブレットコンテナースレッドから、実行するために GraphQL Java によって呼び出される DataFetcher およびその他のコンポーネントのスレッドへの ThreadLocal 値の伝播をサポートします。これを行うには、対象の ThreadLocal 値に対してアプリケーションで io.micrometer.context.ThreadLocalAccessor を実装する必要があります。

public class RequestAttributesAccessor implements ThreadLocalAccessor<RequestAttributes> {

    @Override
    public Object key() {
        return RequestAttributesAccessor.class.getName();
    }

    @Override
    public RequestAttributes getValue() {
        return RequestContextHolder.getRequestAttributes();
    }

    @Override
    public void setValue(RequestAttributes attributes) {
        RequestContextHolder.setRequestAttributes(attributes);
    }

    @Override
    public void reset() {
        RequestContextHolder.resetRequestAttributes();
    }

}

io.micrometer.context.ContextRegistry#getInstance() 経由でアクセスできるグローバル ContextRegistry インスタンスを使用して、起動時に ThreadLocalAccessor を手動で登録できます。java.util.ServiceLoader メカニズムを介して自動的に登録することもできます。

WebFlux

リアクティブ DataFetcher は、チェーン を処理する WebFlux リクエストから発生する Reactor コンテキストへのアクセスに依存できます。これには、WebGraphQlInterceptor コンポーネントによって追加された Reactor コンテキストが含まれます。

例外

GraphQL Java では、DataFetcherExceptionHandler は、レスポンスの「エラー」セクションでデータ取得の例外を表現する方法を決定します。アプリケーションは単一のハンドラーのみを登録できます。

Spring for GraphQL は、デフォルトの処理を提供し、DataFetcherExceptionResolver 契約を有効にする DataFetcherExceptionHandler を登録します。アプリケーションは GraphQLSource ビルダーを介して任意の数のリゾルバーを登録でき、それらは Exception を List<graphql.GraphQLError> に解決するまで順番に登録されます。Spring Boot スターターは、この型の Bean を検出します。

DataFetcherExceptionResolverAdapter は、protected メソッド resolveToSingleError および resolveToMultipleErrors を備えた便利な基本クラスです。

アノテーション付きコントローラープログラミングモデルでは、柔軟なメソッドシグネチャーを持つアノテーション付き例外ハンドラーメソッドを使用してデータフェッチ例外を処理できます。詳細については、@GraphQlExceptionHandler を参照してください。

GraphQLError は、以下を定義する GraphQL Java graphql.ErrorClassification または Spring GraphQL ErrorType に基づいてカテゴリに割り当てることができます。

  • BAD_REQUEST

  • UNAUTHORIZED

  • FORBIDDEN

  • NOT_FOUND

  • INTERNAL_ERROR

例外が未解決のままの場合、デフォルトでは、カテゴリ名と DataFetchingEnvironment からの executionId を含む一般的なメッセージを持つ INTERNAL_ERROR として分類されます。実装の詳細が漏洩しないように、メッセージは意図的に不透明になっています。アプリケーションは、DataFetcherExceptionResolver を使用してエラーの詳細をカスタマイズできます。

未解決の例外は、executionId とともに ERROR レベルでログに記録され、クライアントに送信されたエラーに関連付けられます。解決された例外は DEBUG レベルで記録されます。

例外のリクエスト

GraphQL Java エンジンは、リクエストの解析時に検証またはその他のエラーが発生し、リクエストの実行が妨げられる場合があります。このような場合、レスポンスには、null を含む「データ」キーと、グローバルな、つまりフィールドパスがない 1 つ以上のリクエストレベルの「エラー」が含まれます。

DataFetcherExceptionResolver は、実行が開始される前、DataFetcher が呼び出される前に発生するため、このようなグローバルエラーを処理できません。アプリケーションは、トランスポートレベルのインターセプターを使用して、ExecutionResult のエラーをインスペクションおよび変換できます。WebGraphQlInterceptor の例を参照してください。

サブスクリプションの例外

サブスクリプションリクエストの Publisher は、エラーシグナルで完了する場合があります。この場合、基になるトランスポート (WebSocket など) は、GraphQL エラーのリストを含む最終的な「エラー」型のメッセージを送信します。

データ DataFetcher は最初に Publisher を作成するだけなので、DataFetcherExceptionResolver はサブスクリプション Publisher からのエラーを解決できません。その後、トランスポートは Publisher にサブスクライブし、エラーで完了する可能性があります。

アプリケーションは、サブスクリプション Publisher からの例外を解決して GraphQL エラーに解決し、クライアントに送信するために SubscriptionExceptionResolver を登録できます。

ページネーション

GraphQL カーソル接続仕様 (英語) は、各アイテムがカーソルとペアになっているアイテムのサブセットを一度に返すことによって、大規模な結果セットをナビゲートする方法を定義します。クライアントは、参照されたアイテムの前後に追加のアイテムをリクエストするためにこのカーソルを使用できます。

仕様では、このパターンを「接続」と呼びます。Connection で終わる名前を持つスキーマ型は、ページ分割された結果セットを表す接続です。すべての ~Connection 型には、~Edge 型が実際の項目とカーソルを組み合わせる「エッジ」フィールドと、前後にさらに項目があるかどうかを示すブール値フラグを備えた "pageInfo" フィールドが含まれています。

接続タイプ

Connection 型定義は、ページネーションが必要な型ごとに作成する必要があり、スキーマにボイラープレートとノイズが追加されます。Spring for GraphQL は、解析されたスキーマファイルにこれらの型が存在しない場合に、起動時にこれらの型を追加する ConnectionTypeDefinitionConfigurer を提供します。つまり、スキーマではこれだけが必要です。

Query {
	books(first:Int, after:String, last:Int, before:String): BookConnection
}

type Book {
	id: ID!
	title: String!
}

仕様で定義されている前方ページネーション引数 first および after は、クライアントが指定されたカーソルの後の最初の N 項目をリクエストするために使用できるのに対し、last および before は、指定されたカーソルの前の最後の N 項目をリクエストするための後方ページネーション引数であることに注意してください。

次に、ConnectionTypeDefinitionConfigurer を次のように構成します。

GraphQlSource.schemaResourceBuilder()
		.schemaResources(..)
		.typeDefinitionConfigurer(new ConnectionTypeDefinitionConfigurer)

次の型定義が透過的にスキーマに追加されます。

type BookConnection {
	edges: [BookEdge]!
	pageInfo: PageInfo!
}

type BookEdge {
	node: Book!
	cursor: String!
}

type PageInfo {
	hasPreviousPage: Boolean!
	hasNextPage: Boolean!
	startCursor: String
	endCursor: String
}

Boot スターターはデフォルトで ConnectionTypeDefinitionConfigurer を登録します。

ConnectionAdapter

接続タイプがスキーマで使用可能になったら、同等の Java 型も必要になります。GraphQL Java は、汎用の Connection および Edge に加えて PageInfo を含む提供します。

1 つのオプションは、Connection を設定し、それをコントローラーメソッドまたは DataFetcher から返すことです。ただし、これには、Connection を作成し、カーソルを作成し、各項目を Edge としてラップし、PageInfo を作成するボイラープレートコードが必要です。さらに、Spring Data リポジトリを使用する場合など、基礎となるページネーションメカニズムがすでに存在している場合もあります。

Spring for GraphQL は、項目のコンテナーを Connection に適合させるための ConnectionAdapter 契約を定義します。アダプターは DataFetcher デコレーターを通じて適用され、その後、ConnectionFieldTypeVisitor を通じてインストールされます。次のように設定できます。

ConnectionAdapter adapter = ... ;
GraphQLTypeVisitor visitor = ConnectionFieldTypeVisitor.create(List.of(adapter)) (1)

GraphQlSource.schemaResourceBuilder()
		.schemaResources(..)
		.typeDefinitionConfigurer(..)
		.typeVisitors(List.of(visitor)) (2)
11 つ以上の ConnectionAdapter を使用して型 ビジターを作成します。
2 型ビジターを登録します。

Spring Data の Window および Slice には組み込みの ConnectionAdapter があります。独自のカスタムアダプターを作成することもできます。ConnectionAdapter 実装は、返された項目のカーソルを作成するために CursorStrategy に依存します。同じ戦略は、ページネーション入力を含む Subrange コントローラーメソッド引数をサポートするためにも使用されます。

CursorStrategy

CursorStrategy は、大きな結果セット内の項目の位置を参照する String カーソルをエンコードおよびデコードするための規約です。カーソルはインデックスまたはキーセットに基づくことができます。

ConnectionAdapter はこれを使用して、返された項目のカーソルをエンコードします。アノテーション付きコントローラーメソッド、Querydsl リポジトリ、および例示による問い合わせリポジトリは、これを使用してページ分割リクエストからカーソルをデコードし、Subrange を作成します。

CursorEncoder は、文字列カーソルをさらにエンコードおよびデコードして、クライアントに対して不透明にする関連契約です。EncodingCursorStrategy は CursorStrategy と CursorEncoder を組み合わせたものです。Base64CursorEncoderNoOpEncoder を使用することも、独自に作成することもできます。

Spring Data ScrollPosition には CursorStrategy が内蔵されています。Spring Data が存在する場合、Boot スターターは CursorStrategy<ScrollPosition> を Base64Encoder に登録します。

ソート

GraphQL リクエストでソート情報を提供する標準的な方法はありません。ただし、ページネーションは安定した並べ替え順序に依存します。デフォルトの順序を使用することも、入力型を公開して GraphQL 引数からソートの詳細を抽出することもできます。

コントローラーメソッドの引数として、Spring Data の Sort のサポートが組み込まれています。これが機能するには、SortStrategy Bean が必要です。

バッチ読み込み

Book とその Author を指定すると、書籍用に 1 つの DataFetcher を作成し、その作成者用に別の DataFetcher を作成できます。これにより、作成者の有無にかかわらず本を選択できますが、本と作成者が一緒にロードされないことを意味します。これは、各本の作成者が個別にロードされるため、複数の本を照会する場合に特に効率的ではありません。これは、N+1 選択問題として知られています。

DataLoader

GraphQL Java は、関連するエンティティをバッチで読み込むための DataLoader メカニズムを提供します。詳細は GraphQL Java ドキュメント (英語) で確認できます。以下は、その仕組みの概要です。

  1. 一意のキーを指定して、エンティティをロードできる DataLoaderRegistry に DataLoader を登録します。

  2. DataFetcher は DataLoader にアクセスし、使用して ID でエンティティをロードできます。

  3. DataLoader は、Future を返すことで読み込みを延期し、バッチで実行できるようにします。

  4. DataLoader は、ロードされたエンティティのリクエストごとのキャッシュを維持し、効率をさらに向上させることができます。

BatchLoaderRegistry

GraphQL Java の完全なバッチ読み込みメカニズムでは、いくつかの BatchLoader インターフェースの 1 つを実装し、DataLoader としてラップして DataLoaderRegistry の名前で登録する必要があります。

Spring GraphQL の API は少し異なります。登録の場合、ファクトリメソッドを公開する主要な BatchLoaderRegistry と、任意の数のバッチロード関数を作成および登録するためのビルダーが 1 つだけあります。

@Configuration
public class MyConfig {

	public MyConfig(BatchLoaderRegistry registry) {

		registry.forTypePair(Long.class, Author.class).registerMappedBatchLoader((authorIds, env) -> {
				// return Mono<Map<Long, Author>
		});

		// more registrations ...
	}

}

Boot スターターは、上記のように構成に挿入できる BatchLoaderRegistry Bean を宣言するか、バッチ読み込み関数を登録するためにコントローラーなどの任意のコンポーネントに挿入できます。次に、BatchLoaderRegistry は DefaultExecutionGraphQlService に注入され、リクエストごとの DataLoader 登録を保証します。

デフォルトでは、DataLoader 名はターゲットエンティティのクラス名に基づいています。これにより、@SchemaMapping メソッドはジェネリクス型で DataLoader 引数を宣言でき、名前を指定する必要はありません。ただし、名前は、必要に応じて他の DataLoaderOptions と共に BatchLoaderRegistry ビルダーを介してカスタマイズできます。

デフォルトの DataLoaderOptions をグローバルに構成し、登録の開始点として使用するには、Boot の BatchLoaderRegistry Bean をオーバーライドし、Supplier<DataLoaderOptions> を受け入れる DefaultBatchLoaderRegistry のコンストラクターを使用できます。

多くの場合、関連するエンティティをロードするときに、@BatchMapping コントローラーメソッドを使用できます。これは、BatchLoaderRegistry および DataLoader を直接使用する必要性を回避するショートカットです。

BatchLoaderRegistry には他にも重要な利点があります。バッチ読み込み関数と @BatchMapping メソッドから同じ GraphQLContext へのアクセスをサポートし、それらへのコンテキストの伝播を保証します。これが、アプリケーションがそれを使用することが期待される理由です。独自の DataLoader 登録を直接実行することは可能ですが、そのような登録では上記の利点が失われます。

バッチ読み込みのテスト

BatchLoaderRegistry に DataLoaderRegistry で登録を実行させることから始めます。

BatchLoaderRegistry batchLoaderRegistry = new DefaultBatchLoaderRegistry();
// perform registrations...

DataLoaderRegistry dataLoaderRegistry = DataLoaderRegistry.newRegistry().build();
batchLoaderRegistry.registerDataLoaders(dataLoaderRegistry, graphQLContext);

これで、次のように個々の DataLoader にアクセスしてテストできます。

DataLoader<Long, Book> loader = dataLoaderRegistry.getDataLoader(Book.class.getName());
loader.load(1L);
loader.loadMany(Arrays.asList(2L, 3L));
List<Book> books = loader.dispatchAndJoin(); // actual loading

assertThat(books).hasSize(3);
assertThat(books.get(0).getName()).isEqualTo("...");
// ...