リクエスト実行
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 を宣言することもできます。例:
import org.springframework.boot.autoconfigure.graphql.GraphQlSourceBuilderCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration(proxyBeanMethods = false)
public 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 登録 |
3 | DataFetcher は存在しない引数を期待しました |
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 コンテキストを参照してください。
サブスクリプションリクエストの場合、GraphQL Java は、アイテムが利用可能になり、リクエストされたフィールドがすべてフェッチされるとすぐにアイテムを生成します。これには複数の非同期データフェッチレイヤーが含まれるため、アイテムは元の順序とは別の順序でネットワーク経由で送信される可能性があります。GraphQL Java でアイテムをバッファリングして元の順序を維持するには、GraphQLContext
で SubscriptionExecutionStrategy.KEEP_SUBSCRIPTION_EVENTS_ORDERED
構成フラグを設定します。これは、たとえばカスタム Instrumentation
を使用して実行できます。
import graphql.ExecutionResult;
import graphql.execution.SubscriptionExecutionStrategy;
import graphql.execution.instrumentation.InstrumentationContext;
import graphql.execution.instrumentation.InstrumentationState;
import graphql.execution.instrumentation.SimpleInstrumentationContext;
import graphql.execution.instrumentation.SimplePerformantInstrumentation;
import graphql.execution.instrumentation.parameters.InstrumentationExecutionParameters;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration(proxyBeanMethods = false)
public class GraphQlConfig {
@Bean
public SubscriptionOrderInstrumentation subscriptionOrderInstrumentation() {
return new SubscriptionOrderInstrumentation();
}
static class SubscriptionOrderInstrumentation extends SimplePerformantInstrumentation {
@Override
public InstrumentationContext<ExecutionResult> beginExecution(InstrumentationExecutionParameters parameters,
InstrumentationState state) {
// Enable option for keeping subscription results in upstream order
parameters.getGraphQLContext().put(SubscriptionExecutionStrategy.KEEP_SUBSCRIPTION_EVENTS_ORDERED, true);
return SimpleInstrumentationContext.noOp();
}
}
}
コンテキストの伝播
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
で終わるスキーマ型はページ分割された結果セットを表す接続型です。すべての接続型には「エッジ」と呼ばれるフィールドが含まれており、~Edge
型には実際のアイテム、カーソル、前方および後方にさらにアイテムが存在するかどうかを示す "pageInfo" と呼ばれるフィールドが含まれます。
接続タイプ
接続型には、明示的に宣言されていない場合、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 項目をリクエストできます。
仕様では、first と last の両方を含めることは推奨されておらず、ページ区切りの結果が不明確になることも示されています。Spring for GraphQL では、first または after が存在する場合、last と before は無視されます。 |
接続型を生成するには、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
など、提供します。
コントローラーメソッドから Connection
を返すことはできますが、基礎となるデータページ区切りメカニズムを Connection
に適合させ、カーソルを作成し、~Edge
ラッパーを追加し、PageInfo
を作成するための定型コードが必要になります。
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)
1 | 1 つ以上の ConnectionAdapter を使用して型 ビジターを作成します。 |
2 | 型ビジターを登録します。 |
Spring Data の Window
と Slice
には、組み込みの ConnectionAdapter
があります。独自のカスタムアダプターを作成することもできます。ConnectionAdapter
実装は、返される項目のカーソルを作成するために CursorStrategy
に依存します。同じ戦略は、ページ区切り入力を含む Subrange
コントローラーメソッド引数をサポートするためにも使用されます。
CursorStrategy
CursorStrategy
は、大きな結果セット内の項目の位置を参照する String カーソルをエンコードおよびデコードするための規約です。カーソルはインデックスまたはキーセットに基づくことができます。
ConnectionAdapter
はこれを使用して、返された項目のカーソルをエンコードします。アノテーション付きコントローラーメソッド、Querydsl リポジトリ、および例示による問い合わせリポジトリは、これを使用してページ分割リクエストからカーソルをデコードし、Subrange
を作成します。
CursorEncoder
は、文字列カーソルをさらにエンコードおよびデコードして、クライアントに対して不透明にする関連契約です。EncodingCursorStrategy
は CursorStrategy
と CursorEncoder
を組み合わせたものです。Base64CursorEncoder
、NoOpEncoder
を使用することも、独自に作成することもできます。
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 ドキュメント (英語) で確認できます。以下は、その仕組みの概要です。
一意のキーを指定して、エンティティをロードできる
DataLoaderRegistry
にDataLoader
を登録します。DataFetcher
はDataLoader
にアクセスし、使用して ID でエンティティをロードできます。DataLoader
は、Future を返すことで読み込みを延期し、バッチで実行できるようにします。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("...");
// ...