1. 概要
Spring for GraphQL は、GraphQL Java (英語) 上に構築された Spring アプリケーションのサポートを提供します。これは、GraphQL Java チームと Spring エンジニアリングとの共同作業です。
Spring for GraphQL は、GraphQLJava チームの GraphQL Java Spring [GitHub] (英語) プロジェクトの後継です。これは、すべての Spring、GraphQL アプリケーションの基盤となることを目的としています。
課題の報告、設計上の課題の議論、機能のリクエストには、課題トラッカー [GitHub] (英語) を使用してください。
新機能、ベースライン要件、アップグレードノート、その他のバージョン間情報については、Wiki [GitHub] (英語) を参照してください。
開始するには、Boot スターターおよびサンプルセクションを参照してください。
2. サーバートランスポート
Spring for GraphQL は、HTTP、WebSocket、RSocket を介した GraphQL リクエストのサーバー処理をサポートします。
2.1. HTTP
GraphQlHttpHandler は HTTP リクエストを介して GraphQL を処理し、リクエストの実行のためにインターセプトチェーンに委譲します。Spring MVC 用と Spring WebFlux 用の 2 つのバリエーションがあります。どちらもリクエストを非同期に処理し、同等の機能を備えていますが、HTTP レスポンスを書き込むために、それぞれブロッキング I/O とノンブロッキング I/O に依存しています。
リクエストは、コンテンツ型として "application/json" を指定した HTTP POST を使用し、リクエスト本文に JSON として含まれる GraphQL リクエストの詳細を、提案された HTTP 経由の GraphQL [GitHub] (英語) 仕様で定義されているように使用する必要があります。JSON ボディが正常にデコードされると、HTTP レスポンスのステータスは常に 200 (OK) になり、GraphQL リクエストの実行によるエラーは GraphQL レスポンスの「エラー」セクションに表示されます。メディア型のデフォルトで推奨される選択肢は "application/graphql-response+json" ですが、仕様に従って "application/json" もサポートされています。
RouterFunction Bean を宣言し、Spring MVC または WebFlux からの RouterFunctions を使用してルートを作成することにより、GraphQlHttpHandler を HTTP エンドポイントとして公開できます。Boot スターターはこれを行います。詳細については Web エンドポイントセクションを参照するか、実際の構成については含まれている GraphQlWebMvcAutoConfiguration または GraphQlWebFluxAutoConfiguration を確認してください。
このリポジトリの 1.0.x ブランチには、Spring MVC HTTP サンプル [GitHub] (英語) アプリケーションが含まれています。
2.2. WebSocket
GraphQlWebSocketHandler は、graphql-ws [GitHub] (英語) ライブラリで定義されたプロトコル [GitHub] (英語) に基づいて、WebSocket リクエストを介した GraphQL を処理します。WebSocket で GraphQL を使用する主な理由は、GraphQL レスポンスのストリームを送信できるサブスクリプションですが、単一のレスポンスを持つ通常のクエリにも使用できます。ハンドラーは、さらにリクエストを実行するために、すべてのリクエストをインターセプトチェーンに委譲します。
WebSocket プロトコルを介した GraphQL このようなプロトコルは 2 つあります。1 つは subscriptions-transport-ws [GitHub] (英語) ライブラリにあり、もう 1 つは graphql-ws [GitHub] (英語) ライブラリにあります。前者は活動せず、後者に引き継がれています。歴史については、このブログ記事 (英語) を参照してください。 |
GraphQlWebSocketHandler には、Spring MVC 用と Spring WebFlux 用の 2 つのバリアントがあります。どちらもリクエストを非同期に処理し、同等の機能を備えています。WebFlux ハンドラーは、ノンブロッキング I/O とバックプレッシャーも使用してメッセージをストリーミングします。これは、GraphQL Java ではサブスクリプションレスポンスが Reactive Streams Publisher であるため、うまく機能します。
graphql-ws プロジェクトには、クライアントが使用する多数のレシピ [GitHub] (英語) がリストされています。
SimpleUrlHandlerMapping Bean を宣言し、それを使用してハンドラーを URL パスにマップすることにより、GraphQlWebSocketHandler を WebSocket エンドポイントとして公開できます。デフォルトでは、Boot スターターは WebSocket エンドポイントを介して GraphQL を公開しませんが、エンドポイントパスのプロパティを追加することで簡単に有効にできます。詳細については Web エンドポイントセクションを参照するか、実際の Boot スターター構成については GraphQlWebMvcAutoConfiguration または GraphQlWebFluxAutoConfiguration を確認してください。
このリポジトリの 1.0.x ブランチには、WebFlux WebSocket サンプル [GitHub] (英語) アプリケーションが含まれています。
2.3. RSocket
GraphQlRSocketHandler は、GraphQL over RSocket リクエストを処理します。サブスクリプションは request-stream として処理されますが、クエリとミューテーションは RSocket request-response インタラクションとして予期され、処理されます。
GraphQlRSocketHandler は、GraphQL リクエストのルートにマップされた @Controller からのデリゲートとして使用できます。例:
@Controller
public class GraphQlRSocketController {
private final GraphQlRSocketHandler handler;
GraphQlRSocketController(GraphQlRSocketHandler handler) {
this.handler = handler;
}
@MessageMapping("graphql")
public Mono<Map<String, Object>> handle(Map<String, Object> payload) {
return this.handler.handle(payload);
}
@MessageMapping("graphql")
public Flux<Map<String, Object>> handleSubscription(Map<String, Object> payload) {
return this.handler.handleSubscription(payload);
}
}
2.4. インターセプト
サーバートランスポートを使用すると、GraphQL Java エンジンが呼び出されてリクエストを処理する前後に、リクエストをインターセプトできます。
2.4.1. WebGraphQlInterceptor
HTTP および WebSocket トランスポートは、0 以上の WebGraphQlInterceptor の チェーンを呼び出し、その後に GraphQL Java エンジンを呼び出す ExecutionGraphQlService を呼び出します。WebGraphQlInterceptor を使用すると、アプリケーションは受信リクエストをインターセプトし、次のいずれかを実行できます。
HTTP リクエストの詳細を確認する
graphql.ExecutionInputをカスタマイズするHTTP レスポンスヘッダーを追加する
graphql.ExecutionResultをカスタマイズする
例: インターセプターは、HTTP リクエストヘッダーを DataFetcher に渡すことができます。
class RequestHeaderInterceptor implements WebGraphQlInterceptor { (1)
@Override
public Mono<WebGraphQlResponse> intercept(WebGraphQlRequest request, Chain chain) {
String value = request.getHeaders().getFirst("myHeader");
request.configureExecutionInput((executionInput, builder) ->
builder.graphQLContext(Collections.singletonMap("myHeader", value)).build());
return chain.next(request);
}
}
@Controller
class MyContextValueController { (2)
@QueryMapping
Person person(@ContextValue String myHeader) {
...
}
}
| 1 | インターセプターは HTTP リクエストヘッダー値を GraphQLContext に追加します |
| 2 | データコントローラーメソッドは値にアクセスします |
逆に、インターセプターは、コントローラーによって GraphQLContext に追加された値にアクセスできます。
// Subsequent access from a WebGraphQlInterceptor
class ResponseHeaderInterceptor implements WebGraphQlInterceptor {
@Override
public Mono<WebGraphQlResponse> intercept(WebGraphQlRequest request, Chain chain) { (2)
return chain.next(request).doOnNext(response -> {
String value = response.getExecutionInput().getGraphQLContext().get("cookieName");
ResponseCookie cookie = ResponseCookie.from("cookieName", value).build();
response.getResponseHeaders().add(HttpHeaders.SET_COOKIE, cookie.toString());
});
}
}
@Controller
class MyCookieController {
@QueryMapping
Person person(GraphQLContext context) { (1)
context.put("cookieName", "123");
...
}
}
| 1 | GraphQLContext に付加価値を与えるコントローラー |
| 2 | Interceptor は値を使用して HTTP レスポンスヘッダーを追加します |
WebGraphQlHandler は ExecutionResult を変更できます。たとえば、実行開始前に発生し、DataFetcherExceptionResolver では処理できないリクエスト検証エラーをインスペクションおよび変更できます。
class RequestErrorInterceptor implements WebGraphQlInterceptor {
@Override
public Mono<WebGraphQlResponse> intercept(WebGraphQlRequest request, Chain chain) {
return chain.next(request).map(response -> {
if (response.isValid()) {
return response; (1)
}
List<GraphQLError> errors = response.getErrors().stream() (2)
.map(error -> {
GraphqlErrorBuilder<?> builder = GraphqlErrorBuilder.newError();
// ...
return builder.build();
})
.toList();
return response.transform(builder -> builder.errors(errors).build()); (3)
});
}
}
| 1 | ExecutionResult に null 以外の値を持つ「データ」キーがある場合は、同じものを返します。 |
| 2 | GraphQL エラーを確認して変換する |
| 3 | 変更されたエラーで ExecutionResult を更新します |
WebGraphQlHandler を使用して、WebGraphQlInterceptor チェーンを構成します。これは Boot スターターでサポートされています。Web エンドポイントを参照してください。
2.4.2. RSocketQlInterceptor
WebGraphQlInterceptor と同様に、RSocketQlInterceptor を使用すると、GraphQL Java エンジンの実行の前後に、RSocket リクエストを介して GraphQL をインターセプトできます。これを使用して、graphql.ExecutionInput および graphql.ExecutionResult をカスタマイズできます。
3. リクエスト実行
ExecutionGraphQlService は、GraphQL Java を呼び出してリクエストを実行するための主要な Spring 抽象化です。HTTP などの基礎となるトランスポートは、リクエストを処理するために ExecutionGraphQlService に委譲します。
主な実装である DefaultExecutionGraphQlService は、起動する graphql.GraphQL インスタンスにアクセスするための GraphQlSource で構成されています。
3.1. 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()));
}
}
3.1.1. スキーマリソース
GraphQlSource.Builder は、1 つ以上の Resource インスタンスを使用して構成し、解析およびマージすることができます。つまり、スキーマファイルはほぼすべての場所からロードできます。
デフォルトでは、Boot スターターは、場所 classpath:graphql/** (通常は src/main/resources/graphql) で拡張子が ".graphqls" または ".gqls" のスキーマファイルを探します。ファイルシステムの場所、または Spring Resource 階層でサポートされている任意の場所を使用することもできます。これには、リモートの場所、ストレージ、メモリからスキーマファイルをロードするカスタム実装が含まれます。
classpath*:graphql/**/ を使用して、複数のクラスパスの場所にまたがるスキーマファイルを検索します。複数のモジュールにわたって。 |
3.1.2. スキーマの作成
デフォルトでは、GraphQlSource.Builder は GraphQL Java SchemaGenerator を使用して graphql.schema.GraphQLSchema を作成します。これは通常の使用では機能しますが、別のジェネレーターを使用する必要がある場合。フェデレーションの場合、schemaFactory コールバックを登録できます。
GraphQlSource.Builder builder = ...
builder.schemaResources(..)
.configureRuntimeWiring(..)
.schemaFactory((typeDefinitionRegistry, runtimeWiring) -> {
// create GraphQLSchema
})
GraphQlSource セクションは、Spring Boot でそれを構成する方法を説明しています。
Apollo Federation の例については、federation-jvm-spring-example [GitHub] (英語) を参照してください。
3.1.3. スキーマトラバーサル
スキーマの作成後にスキーマをトラバースし、変更を GraphQLCodeRegistry に適用する場合は、builder.schemaResources(..).typeVisitors(..) を介して graphql.schema.GraphQLTypeVisitor を登録できます。ただし、そのような訪問者はスキーマを変更できないことに注意してください。スキーマを変更する必要がある場合は、スキーマ変換を参照してください。
3.1.4. スキーマ変換
スキーマの作成後にスキーマをトラバースして変換し、スキーマに変更を加えたい場合は、builder.schemaResources(..).typeVisitorsToTransformSchema(..) を介して graphql.schema.GraphQLTypeVisitor を登録できます。これはスキーマトラバーサルよりもコストがかかるため、スキーマを変更する必要がない限り、通常は変換よりもトラバーサルを優先することに注意してください。
3.1.5. RuntimeWiringConfigurer
RuntimeWiringConfigurer を使用して登録できます。
カスタムスカラー型。
ディレクティブ処理コード。
TypeResolver、型のデフォルトTypeResolverをオーバーライドする必要がある場合。フィールドには
DataFetcherを使用しますが、ほとんどのアプリケーションは、アノテーション付きのDataFetcherハンドラーメソッドを検出するAnnotatedControllerConfigurerを構成するだけです。Boot スターターはデフォルトでAnnotatedControllerConfigurerを追加します。
| GraphQL Java、サーバーアプリケーションは、Jackson をデータのマップとの間の直列化にのみ使用します。クライアント入力はマップに解析されます。サーバー出力は、フィールド選択セットに基づいてマップにアセンブルされます。これは、Jackson シリアライゼーション / デシリアライゼーションアノテーションに依存できないことを意味します。代わりに、カスタムスカラー型 (英語) を使用できます。 |
Boot スターターは、型 RuntimeWiringConfigurer の Bean を検出し、GraphQlSource.Builder に登録します。つまり、ほとんどの場合、構成には次のようなものがあります。
@Configuration
public class GraphQlConfig {
@Bean
public RuntimeWiringConfigurer runtimeWiringConfigurer(BookRepository repository) {
GraphQLScalarType scalarType = ... ;
SchemaDirectiveWiring directiveWiring = ... ;
DataFetcher dataFetcher = QuerydslDataFetcher.builder(repository).single();
return wiringBuilder -> wiringBuilder
.scalar(scalarType)
.directiveWiring(directiveWiring)
.type("Query", builder -> builder.dataFetcher("book", dataFetcher));
}
}
WiringFactory を追加する必要がある場合。スキーマ定義を考慮して登録を行うには、RuntimeWiring.Builder と出力 List<WiringFactory> の両方を受け入れる代替 configure メソッドを実装します。これにより、任意の数のファクトリを追加して、順番に呼び出すことができます。
3.1.6. デフォルトの 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);
GraphQlSource セクションは、Spring Boot でそれを構成する方法を説明しています。
3.1.7. オペレーションキャッシング
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 = ...
builder.schemaResources(..)
.configureRuntimeWiring(..)
.configureGraphQl(graphQLBuilder -> graphQLBuilder.preparsedDocumentProvider(provider))
GraphQlSource セクションは、Spring Boot でそれを構成する方法を説明しています。
3.1.8. ディレクティブ
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] (英語) ライブラリを確認してください。 |
3.2. リアクティブ DataFetcher
デフォルトの GraphQlSource ビルダーは、DataFetcher が Mono または Flux を返すためのサポートを有効にします。これは、Flux 値が集約されてリストに変換される CompletableFuture に適応させます。ただし、リクエストが GraphQL サブスクリプションリクエストである場合を除きます。この場合、戻り値は Reactive Streams Publisher のままです。GraphQL レスポンスのストリーミング用。
リアクティブ DataFetcher は、トランスポート層 (WebFlux リクエスト処理など) から伝播された Reactor コンテキストへのアクセスに依存できます。WebFlux コンテキストを参照してください。
3.3. コンテキストの伝播
Spring for GraphQL は、HTTP から GraphQL Java を介して透過的にコンテキストを伝搬するためのサポートを提供し、DataFetcher およびそれが呼び出すその他のコンポーネントに伝達します。これには、Spring MVC リクエスト処理スレッドからの ThreadLocal コンテキストと、WebFlux 処理パイプラインからの Reactor Context の両方が含まれます。
3.3.1. 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 メカニズムを介して自動的に登録することもできます。
3.3.2. WebFlux
リアクティブ DataFetcher は、チェーンを処理する WebFlux リクエストから発生する Reactor コンテキストへのアクセスに依存できます。これには、WebGraphQlInterceptor コンポーネントによって追加された Reactor コンテキストが含まれます。
3.4. 例外の解決
GraphQL Java アプリケーションは、DataFetcherExceptionHandler を登録して、GraphQL レスポンスの「エラー」セクションでデータ層からの例外を表す方法を決定できます。
Spring for GraphQL には、デフォルトの GraphQLSource ビルダーで使用するように構成された組み込みの DataFetcherExceptionHandler があります。これにより、アプリケーションは、Exception を graphql.GraphQLError オブジェクトの (場合によっては空の) リストに解決するまで、順次呼び出される 1 つ以上の Spring DataFetcherExceptionResolver コンポーネントを登録できます。
DataFetcherExceptionResolver は非同期契約です。ほとんどの実装では、DataFetcherExceptionResolverAdapter を継承し、例外を同期的に解決する resolveToSingleError または resolveToMultipleErrors メソッドのいずれかをオーバーライドするだけで十分です。
GraphQLError は、graphql.ErrorClassification を介してカテゴリに割り当てることができます。Spring GraphQL では、アプリケーションがエラーを分類するために使用できる次の一般的な分類を持つ ErrorType を介して割り当てることもできます。
BAD_REQUESTUNAUTHORIZEDFORBIDDENNOT_FOUNDINTERNAL_ERROR
例外が未解決のままの場合、デフォルトでは、カテゴリ名と DataFetchingEnvironment からの executionId を含む一般的なメッセージを持つ INTERNAL_ERROR として分類されます。実装の詳細が漏洩しないように、メッセージは意図的に不透明になっています。アプリケーションは、DataFetcherExceptionResolver を使用してエラーの詳細をカスタマイズできます。
未解決の例外は、executionId とともに ERROR レベルでログに記録され、クライアントに送信されたエラーに関連付けられます。解決された例外は DEBUG レベルで記録されます。
3.4.1. 例外のリクエスト
GraphQL Java エンジンは、リクエストの解析時に検証またはその他のエラーが発生し、リクエストの実行が妨げられる場合があります。このような場合、レスポンスには、null を含む「データ」キーと、グローバルな、つまりフィールドパスがない 1 つ以上のリクエストレベルの「エラー」が含まれます。
DataFetcherExceptionResolver は、実行が開始される前、DataFetcher が呼び出される前に発生するため、このようなグローバルエラーを処理できません。アプリケーションは、トランスポートレベルのインターセプターを使用して、ExecutionResult のエラーをインスペクションおよび変換できます。WebGraphQlInterceptor の例を参照してください。
3.4.2. サブスクリプションの例外
サブスクリプションリクエストの Publisher は、エラーシグナルで完了する場合があります。この場合、基になるトランスポート (WebSocket など) は、GraphQL エラーのリストを含む最終的な「エラー」型のメッセージを送信します。
データ DataFetcher は最初に Publisher を作成するだけなので、DataFetcherExceptionResolver はサブスクリプション Publisher からのエラーを解決できません。その後、トランスポートは Publisher にサブスクライブし、エラーで完了する可能性があります。
アプリケーションは、サブスクリプション Publisher からの例外を解決して GraphQL エラーに解決し、クライアントに送信するために SubscriptionExceptionResolver を登録できます。
3.5. バッチ読み込み
Book とその Author を指定すると、書籍用に 1 つの DataFetcher を作成し、その作成者用に別の DataFetcher を作成できます。これにより、作成者の有無にかかわらず本を選択できますが、本と作成者が一緒にロードされないことを意味します。これは、各本の作成者が個別にロードされるため、複数の本を照会する場合に特に効率的ではありません。これは、N+1 選択問題として知られています。
3.5.1. DataLoader
GraphQL Java は、関連するエンティティをバッチで読み込むための DataLoader メカニズムを提供します。詳細は GraphQL Java ドキュメント (英語) で確認できます。以下は、その仕組みの概要です。
一意のキーを指定して、エンティティをロードできる
DataLoaderRegistryにDataLoaderを登録します。DataFetcherはDataLoaderにアクセスし、使用して ID でエンティティをロードできます。DataLoaderは、Future を返すことで読み込みを延期し、バッチで実行できるようにします。DataLoaderは、ロードされたエンティティのリクエストごとのキャッシュを維持し、効率をさらに向上させることができます。
3.5.2. 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 登録を直接実行することは可能ですが、そのような登録では上記の利点が失われます。
3.5.3. バッチ読み込みのテスト
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("...");
// ...
4. データ統合
Spring for GraphQL を使用すると、既存の Spring テクノロジを活用し、一般的なプログラミングモデルに従って、GraphQL を介して基になるデータソースを公開できます。
このセクションでは、@GraphQlRepository でマークされたリポジトリの自動検出および GraphQL クエリ登録のオプションを含む、Querydsl または Query by Example リポジトリを DataFetcher に適応させる簡単な方法を提供する Spring Data の統合レイヤーについて説明します。
4.1. Querydsl
Spring for GraphQL は、Querydsl (英語) を使用して Spring Data QueryDSL 拡張機能を介してデータをフェッチすることをサポートしています。Querydsl は、アノテーションプロセッサーを使用してメタモデルを生成することにより、クエリ述語を表現するための柔軟で型安全なアプローチを提供します。
例: リポジトリを QuerydslPredicateExecutor として宣言します。
public interface AccountRepository extends Repository<Account, Long>,
QuerydslPredicateExecutor<Account> {
}
次に、それを使用して DataFetcher を作成します。
// For single result queries
DataFetcher<Account> dataFetcher =
QuerydslDataFetcher.builder(repository).single();
// For multi-result queries
DataFetcher<Iterable<Account>> dataFetcher =
QuerydslDataFetcher.builder(repository).many();
上記の DataFetcher を RuntimeWiringConfigurer で登録できるようになりました。
DataFetcher は、GraphQL リクエストパラメーターから Querydsl Predicate を構築し、それを使用してデータを取得します。Spring Data は、JPA、MongoDB、LDAP の QuerydslPredicateExecutor をサポートします。
リポジトリが ReactiveQuerydslPredicateExecutor の場合、ビルダーは DataFetcher<Mono<Account>> または DataFetcher<Flux<Account>> を返します。Spring Data は、MongoDB のこのバリアントをサポートしています。
4.1.1. ビルドのセットアップ
ビルドで Querydsl を構成するには、公式のリファレンスドキュメント (英語) に従ってください。
例:
dependencies {
//...
annotationProcessor "com.querydsl:querydsl-apt:$querydslVersion:jpa",
'org.hibernate.javax.persistence:hibernate-jpa-2.1-api:1.0.2.Final',
'javax.annotation:javax.annotation-api:1.3.2'
}
compileJava {
options.annotationProcessorPath = configurations.annotationProcessor
}
<dependencies>
<!-- ... -->
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-apt</artifactId>
<version>${querydsl.version}</version>
<classifier>jpa</classifier>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.hibernate.javax.persistence</groupId>
<artifactId>hibernate-jpa-2.1-api</artifactId>
<version>1.0.2.Final</version>
</dependency>
<dependency>
<groupId>javax.annotation</groupId>
<artifactId>javax.annotation-api</artifactId>
<version>1.3.2</version>
</dependency>
</dependencies>
<plugins>
<!-- Annotation processor configuration -->
<plugin>
<groupId>com.mysema.maven</groupId>
<artifactId>apt-maven-plugin</artifactId>
<version>${apt-maven-plugin.version}</version>
<executions>
<execution>
<goals>
<goal>process</goal>
</goals>
<configuration>
<outputDirectory>target/generated-sources/java</outputDirectory>
<processor>com.querydsl.apt.jpa.JPAAnnotationProcessor</processor>
</configuration>
</execution>
</executions>
</plugin>
</plugins>webmvc-http [GitHub] (英語) サンプルは、artifactRepositories に Querydsl を使用します。
4.1.2. カスタム
QuerydslDataFetcher は、GraphQL 引数をプロパティにバインドして Querydsl Predicate を作成する方法のカスタマイズをサポートしています。デフォルトでは、引数は使用可能な各プロパティに対して「等しい」としてバインドされます。これをカスタマイズするには、QuerydslDataFetcher ビルダーメソッドを使用して QuerydslBinderCustomizer を提供します。
リポジトリ自体が QuerydslBinderCustomizer のインスタンスである場合があります。これは自動検出され、自動登録中に透過的に適用されます。ただし、手動で QuerydslDataFetcher をビルドする場合は、ビルダーメソッドを使用して適用する必要があります。
QuerydslDataFetcher はインターフェースと DTO 射影をサポートしてクエリ結果を変換してから、これらをさらに GraphQL 処理のために返します。
| 射影とは何かについては、Spring Data ドキュメントを参照してください。GraphQL で射影を使用する方法を理解するには、選択セットと射影を参照してください。 |
Querydsl リポジトリで Spring Data 射影を使用するには、射影 インターフェースまたはターゲット DTO クラスのいずれかを作成し、projectAs メソッドを介して構成して、ターゲット型を生成する DataFetcher を取得します。
class Account {
String name, identifier, description;
Person owner;
}
interface AccountProjection {
String getName();
String getIdentifier();
}
// For single result queries
DataFetcher<AccountProjection> dataFetcher =
QuerydslDataFetcher.builder(repository).projectAs(AccountProjection.class).single();
// For multi-result queries
DataFetcher<Iterable<AccountProjection>> dataFetcher =
QuerydslDataFetcher.builder(repository).projectAs(AccountProjection.class).many();
4.1.3. 自動登録
リポジトリに @GraphQlRepository のアノテーションが付けられている場合、まだ DataFetcher が登録されておらず、戻り値の型がリポジトリドメインの型と一致するクエリに対して自動的に登録されます。これには、単一値クエリと複数値クエリの両方が含まれます。
デフォルトでは、クエリによって返される GraphQL 型の名前は、リポジトリドメイン型の単純な名前と一致する必要があります。必要に応じて、@GraphQlRepository の typeName 属性を使用して、ターゲットの GraphQL 型名を指定できます。
自動登録は、特定のリポジトリが QuerydslBinderCustomizer を実装しているかどうかを検出し、QuerydslDataFetcher ビルダーメソッドを介して透過的に適用します。
自動登録は、QuerydslDataFetcher から取得できる組み込みの RuntimeWiringConfigurer を介して実行されます。Boot スターターは @GraphQlRepository Bean を自動的に検出し、使用して RuntimeWiringConfigurer を初期化します。
リポジトリがそれぞれ QuerydslBuilderCustomizer または ReactiveQuerydslBuilderCustomizer を実装している場合、自動登録はリポジトリインスタンスで customize(Builder) を呼び出すことによってカスタマイズを適用します。
4.2. 例示による問い合わせ
Spring Data は、例示による問い合わせを使用したデータのフェッチをサポートしています。例示による問い合わせ (QBE) は、ストア固有のクエリ言語を使用してクエリを記述する必要のない単純なクエリ手法です。
QueryByExampleExecutor であるリポジトリを宣言することから始めます。
public interface AccountRepository extends Repository<Account, Long>,
QueryByExampleExecutor<Account> {
}
QueryByExampleDataFetcher を使用して、リポジトリを DataFetcher に変換します。
// For single result queries
DataFetcher<Account> dataFetcher =
QueryByExampleDataFetcher.builder(repository).single();
// For multi-result queries
DataFetcher<Iterable<Account>> dataFetcher =
QueryByExampleDataFetcher.builder(repository).many();
上記の DataFetcher を RuntimeWiringConfigurer で登録できるようになりました。
DataFetcher は、GraphQL 引数マップを使用してリポジトリのドメイン型を作成し、それをサンプルオブジェクトとして使用してデータをフェッチします。Spring Data は、JPA、MongoDB、Neo4j、Redis の QueryByExampleDataFetcher をサポートします。
リポジトリが ReactiveQueryByExampleExecutor の場合、ビルダーは DataFetcher<Mono<Account>> または DataFetcher<Flux<Account>> を返します。Spring Data は、MongoDB、Neo4j、Redis、R2dbc のこのバリアントをサポートしています。
4.2.1. ビルドのセットアップ
例示による問い合わせは、それがサポートされているデータストアの Spring Data モジュールにすでに含まれているため、有効にするために追加のセットアップは必要ありません。
4.2.2. カスタム
QueryByExampleDataFetcher はインターフェースと DTO 射影をサポートしてクエリ結果を変換してから、これらをさらに GraphQL 処理のために返します。
| 射影とは何かについては、Spring Data ドキュメントを参照してください。GraphQL での射影のロールを理解するには、選択セットと射影を参照してください。 |
例示による問い合わせ リポジトリで Spring Data 射影を使用するには、射影 インターフェースまたはターゲット DTO クラスのいずれかを作成し、projectAs メソッドを介して構成して、ターゲット型を生成する DataFetcher を取得します。
class Account {
String name, identifier, description;
Person owner;
}
interface AccountProjection {
String getName();
String getIdentifier();
}
// For single result queries
DataFetcher<AccountProjection> dataFetcher =
QueryByExampleDataFetcher.builder(repository).projectAs(AccountProjection.class).single();
// For multi-result queries
DataFetcher<Iterable<AccountProjection>> dataFetcher =
QueryByExampleDataFetcher.builder(repository).projectAs(AccountProjection.class).many();
4.2.3. 自動登録
リポジトリに @GraphQlRepository のアノテーションが付けられている場合、まだ DataFetcher が登録されておらず、戻り値の型がリポジトリドメインの型と一致するクエリに対して自動的に登録されます。これには、単一値クエリと複数値クエリの両方が含まれます。
デフォルトでは、クエリによって返される GraphQL 型の名前は、リポジトリドメイン型の単純な名前と一致する必要があります。必要に応じて、@GraphQlRepository の typeName 属性を使用して、ターゲットの GraphQL 型名を指定できます。
自動登録は、QueryByExampleDataFetcher から取得できる組み込みの RuntimeWiringConfigurer を介して実行されます。Boot スターターは @GraphQlRepository Bean を自動的に検出し、使用して RuntimeWiringConfigurer を初期化します。
リポジトリがそれぞれ QueryByExampleBuilderCustomizer または ReactiveQueryByExampleBuilderCustomizer を実装している場合、自動登録はリポジトリインスタンスで customize(Builder) を呼び出すことによってカスタマイズを適用します。
4.3. 選択セットと射影
発生する一般的な質問は、GraphQL 選択セットを Spring Data Projection と比較して、それぞれがどのようなロールを果たしているのかということです。
簡単に言えば、Spring for GraphQL は、GraphQL クエリを直接 SQL または JSON クエリに変換するデータゲートウェイではないということです。代わりに、既存の Spring テクノロジを活用でき、GraphQL スキーマと基礎となるデータモデル間の 1 対 1 のマッピングを想定していません。そのため、データモデルのクライアント主導の選択とサーバー側の変換が補完的なロールを果たすことができます。
理解を深めるために、データ層の複雑さを管理するための推奨されるアプローチとして、Spring Data がドメイン駆動 (DDD) 設計を推進していることを考慮してください。DDD では、集約の制約に従うことが重要です。部分的にロードされた集約は集約機能に制限を課す可能性があるため、定義上、集約は完全にロードされた場合にのみ有効です。
Spring Data では、集約をそのまま公開するか、GraphQL の結果として返す前にデータモデルに変換を適用するかを選択できます。前者を実行するだけで十分な場合もあります。デフォルトでは、Querydsl と例示による問い合わせの統合により、GraphQL の選択セットが、基になる Spring Data モジュールが選択を制限するために使用するプロパティパスのヒントに変わります。
それ以外の場合は、GraphQL スキーマに適応するために、基礎となるデータモデルを縮小または変換することが役立ちます。Spring Data は、インターフェースと DTO 射影を通じてこれをサポートします。
インターフェース射影は、データストアクエリの結果に応じて、プロパティが null である場合とそうでない場合がある場所を公開する固定のプロパティセットを定義します。インターフェース射影には 2 種類あり、どちらも基になるデータソースからどのプロパティを読み込むかを決定します。
閉じたインターフェースの射影は、集約オブジェクトを部分的に実体化できないが、プロパティのサブセットを公開したい場合に役立ちます。
オープンインターフェース射影は、Spring の
@Valueアノテーションと SpEL 式を活用して、連結、計算、静的関数のプロパティへの適用などの軽量データ変換を適用します。
DTO 射影は、コンストラクターまたは getter メソッドのいずれかに変換コードを配置できるため、より高いレベルのカスタマイズを提供します。
DTO 射影は、個々のプロパティが射影自体によって決定されるクエリから具体化されます。DTO 射影は、通常、完全な引数のコンストラクター (Java レコードなど) で使用されるため、必要なすべてのフィールド (または列) がデータベースクエリ結果の一部である場合にのみ構築できます。
5. アノテーション付きコントローラー
Spring for GraphQL は、@Controller コンポーネントがアノテーションを使用して、特定の GraphQL フィールドのデータをフェッチするための柔軟なメソッドシグネチャーを持つハンドラーメソッドを宣言する、アノテーションベースのプログラミングモデルを提供します。例:
@Controller
public class GreetingController {
@QueryMapping (1)
public String hello() { (2)
return "Hello, world!";
}
}
| 1 | このメソッドをクエリ、つまり Query 型のフィールドにバインドします。 |
| 2 | アノテーションで宣言されていない場合は、メソッド名からクエリを決定します。 |
Spring for GraphQL は RuntimeWiring.Builder を使用して、上記のハンドラーメソッドを "hello" という名前のクエリの graphql.schema.DataFetcher として登録します。
5.1. 宣言
@Controller Bean を標準の Spring Bean 定義として定義できます。@Controller ステレオタイプは、クラスパス上の @Controller および @Component クラスを検出し、それらの Bean 定義を自動登録するための Spring 一般サポートと連携して、自動検出を可能にします。また、アノテーション付きクラスのステレオタイプとしても機能し、GraphQL アプリケーションでのデータ取得コンポーネントとしてのロールを示します。
AnnotatedControllerConfigurer は @Controller Bean を検出し、それらのアノテーション付きハンドラーメソッドを RuntimeWiring.Builder を介して DataFetcher として登録します。GraphQlSource.Builder に追加できる RuntimeWiringConfigurer の実装です。Boot スターターは自動的に AnnotatedControllerConfigurer を Bean として宣言し、すべての RuntimeWiringConfigurer Bean を GraphQlSource.Builder に追加します。これにより、アノテーション付き DataFetcher のサポートが有効になります。Boot スタータードキュメントの GraphQL RuntimeWiring セクションを参照してください。
5.2. @SchemaMapping
@SchemaMapping アノテーションは、ハンドラーメソッドを GraphQL スキーマのフィールドにマップし、それがそのフィールドの DataFetcher であることを宣言します。アノテーションは、親の型名とフィールド名を指定できます。
@Controller
public class BookController {
@SchemaMapping(typeName="Book", field="author")
public Author getAuthor(Book book) {
// ...
}
}
@SchemaMapping アノテーションはこれらの属性を除外することもできます。その場合、フィールド名はデフォルトでメソッド名になり、型名はデフォルトでメソッドに注入されたソース / 親オブジェクトの単純なクラス名になります。例: 以下のデフォルトは、型が "Book" で、フィールドが "author" です。
@Controller
public class BookController {
@SchemaMapping
public Author author(Book book) {
// ...
}
}
@SchemaMapping アノテーションをクラスレベルで宣言して、クラス内のすべてのハンドラーメソッドの既定の型名を指定できます。
@Controller
@SchemaMapping(typeName="Book")
public class BookController {
// @SchemaMapping methods for fields of the "Book" type
}
@QueryMapping、@MutationMapping、@SubscriptionMapping はメタアノテーションであり、それ自体は @SchemaMapping でアノテーションが付けられ、typeName はそれぞれ Query、Mutation、または Subscription にプリセットされています。実質的には、これらはそれぞれクエリ、ミューテーション、サブスクリプション型のフィールドのショートカットアノテーションです。例:
@Controller
public class BookController {
@QueryMapping
public Book bookById(@Argument Long id) {
// ...
}
@MutationMapping
public Book addBook(@Argument BookInput bookInput) {
// ...
}
@SubscriptionMapping
public Flux<Book> newPublications() {
// ...
}
}
@SchemaMapping ハンドラーメソッドには柔軟なシグネチャーがあり、さまざまなメソッド引数と戻り値から選択できます。
5.2.1. メソッド署名
スキーママッピングハンドラーメソッドは、次のメソッド引数のいずれかを持つことができます。
| メソッド引数 | 説明 |
|---|---|
| より高いレベルの型付きオブジェクトにバインドされた名前付きフィールド引数へのアクセス用。 |
|
|
| 入力引数が省略されたか、 |
| 上位レベルの型付きオブジェクトにバインドされたすべてのフィールド引数へのアクセス用。 |
| 引数の生のマップへのアクセス用。 |
|
For access to field arguments through a project interface.
See |
"Source" |
For access to the source (i.e. parent/container) instance of the field. See Source. |
|
|
|
|
|
|
|
|
| 利用可能な場合、Spring Security コンテキストから取得されます。 |
| Spring Security コンテキストから |
|
|
|
|
| 基礎となる |
スキーママッピングハンドラーメソッドは、以下を返すことができます。
任意の型の解決された値。
非同期値の
MonoおよびFlux。リアクティブDataFetcherに従って、コントローラーメソッドと任意のDataFetcherでサポートされています。java.util.concurrent.Callableを使用して、値を非同期的に生成します。これを機能させるには、AnnotatedControllerConfigurerをExecutorで構成する必要があります。
5.2.2. @Argument
GraphQL Java では、DataFetchingEnvironment はフィールド固有の引数値のマップへのアクセスを提供します。値は、単純なスカラー値 (例: String、Long)、より複雑な入力用の値の Map、または値の List にすることができます。
@Argument アノテーションを使用して、引数をターゲットオブジェクトにバインドし、ハンドラーメソッドに注入します。バインドは、引数値を予想されるメソッドパラメーターの型のプライマリデータコンストラクターにマップするか、既定のコンストラクターを使用してオブジェクトを作成し、引数値をそのプロパティにマップすることによって実行されます。これが再帰的に繰り返され、ネストされたすべての引数値が使用され、それに応じてネストされたターゲットオブジェクトが作成されます。例:
@Controller
public class BookController {
@QueryMapping
public Book bookById(@Argument Long id) {
// ...
}
@MutationMapping
public Book addBook(@Argument BookInput bookInput) {
// ...
}
}
デフォルトでは、メソッドパラメーター名が使用可能な場合 (Java 8+ を指定した -parameters コンパイラーフラグまたはコンパイラーからのデバッグ情報が必要)、引数の検索に使用されます。必要に応じて、アノテーションを使用して名前をカスタマイズできます。@Argument("bookInput")。
@Argument アノテーションには、「必須」フラグも、デフォルト値を指定するオプションもありません。これらはどちらも GraphQL スキーマレベルで指定でき、GraphQL Java によって適用されます。 |
バインディングが失敗した場合、フィールドエラーとして蓄積されたバインディングの課題とともに BindException が発生します。各エラーの field は、問題が発生した引数パスです。
@Argument を Map<String, Object> 引数とともに使用して、すべての引数値の生のマップを取得できます。@Argument の name 属性は設定しないでください。
5.2.3. ArgumentValue
デフォルトでは、GraphQL の入力引数は null 可能でオプションです。つまり、引数を null リテラルに設定することも、まったく指定しないこともできます。この区別は、基礎となるデータも null に設定されているか、それに応じてまったく変更されていない可能性があるミューテーションによる部分的な更新に役立ちます。@Argument を使用する場合、どちらの場合も null または空の Optional を取得するため、そのような区別を行う方法はありません。
値がまったく提供されていないかどうかを知りたくない場合は、入力引数が完全に省略されたかどうかを示すフラグとともに、結果の値の単純なコンテナーである ArgumentValue メソッドパラメーターを宣言できます。これを @Argument の代わりに使用できます。この場合、引数名はメソッドのパラメーター名から決定されます。または、@Argument と一緒に使用して引数名を指定することもできます。
例:
@Controller
public class BookController {
@MutationMapping
public void addBook(ArgumentValue<BookInput> bookInput) {
if (!bookInput.isOmitted()) {
BookInput value = bookInput.value();
// ...
}
}
}
ArgumentValue は、コンストラクター引数または setter を介して初期化された @Argument メソッドパラメーターのオブジェクト構造内のフィールドとしてもサポートされます。これには、最上位オブジェクトの任意のレベルでネストされたオブジェクトのフィールドが含まれます。
5.2.4. @Arguments
特定の名前付き引数をバインドする @Argument とは対照的に、完全な引数マップを単一のターゲットオブジェクトにバインドする場合は、@Arguments アノテーションを使用します。
例: @Argument BookInput bookInput は引数 "bookInput" の値を使用して BookInput を初期化しますが、@Arguments は完全な引数マップを使用し、その場合、最上位レベルの引数は BookInput プロパティにバインドされます。
@Arguments を Map<String, Object> 引数とともに使用して、すべての引数値の生のマップを取得できます。
5.2.5. @ProjectedPayload インターフェース
@Argument で完全なオブジェクトを使用する代わりに、射影 インターフェースを使用して、適切に定義された最小限のインターフェースを介して GraphQL リクエスト引数にアクセスすることもできます。Spring Data がクラスパス上にある場合、引数射影は Spring Data のインターフェース射影によって提供されます。
これを利用するには、@ProjectedPayload でアノテーションを付けたインターフェースを作成し、それをコントローラーメソッドのパラメーターとして宣言します。パラメーターに @Argument のアノテーションが付けられている場合、DataFetchingEnvironment.getArguments() マップ内の個々の引数に適用されます。@Argument なしで宣言すると、射影は完全な引数マップのトップレベルの引数で機能します。
例:
@Controller
public class BookController {
@QueryMapping
public Book bookById(BookIdProjection bookId) {
// ...
}
@MutationMapping
public Book addBook(@Argument BookInputProjection bookInput) {
// ...
}
}
@ProjectedPayload
interface BookIdProjection {
Long getId();
}
@ProjectedPayload
interface BookInputProjection {
String getName();
@Value("#{target.author + ' ' + target.name}")
String getAuthorAndName();
}
5.2.6. ソース
GraphQL Java では、DataFetchingEnvironment はフィールドのソース (つまり、親 / コンテナー) インスタンスへのアクセスを提供します。これにアクセスするには、予想されるターゲット型のメソッドパラメーターを宣言するだけです。
@Controller
public class BookController {
@SchemaMapping
public Author author(Book book) {
// ...
}
}
ソースメソッドの引数は、マッピングの型名を決定するのにも役立ちます。Java クラスの単純な名前が GraphQL 型と一致する場合、@SchemaMapping アノテーションで型名を明示的に指定する必要はありません。
|
5.2.7. DataLoader
バッチ読み込みに従って、エンティティのバッチ読み込み関数を登録すると、型 DataLoader のメソッド引数を宣言することでエンティティの DataLoader にアクセスし、それを使用してエンティティを読み込むことができます。
@Controller
public class BookController {
public BookController(BatchLoaderRegistry registry) {
registry.forTypePair(Long.class, Author.class).registerMappedBatchLoader((authorIds, env) -> {
// return Map<Long, Author>
});
}
@SchemaMapping
public CompletableFuture<Author> author(Book book, DataLoader<Long, Author> loader) {
return loader.load(book.getAuthorId());
}
}
デフォルトでは、BatchLoaderRegistry は値型の完全なクラス名 (例: Author のクラス名) を登録のキーに使用するため、ジェネリクス型で DataLoader メソッド引数を宣言するだけで、DataLoaderRegistry でそれを見つけるのに十分な情報が提供されます。フォールバックとして、DataLoader メソッド引数リゾルバーもメソッド引数名をキーとして試行しますが、通常は必要ありません。
@SchemaMapping が単純に DataLoader に委譲する関連エンティティをロードする多くの場合、次のセクションで説明するように @BatchMapping メソッドを使用してボイラープレートを減らすことができることに注意してください。
5.2.8. 検証
javax.validation.Validator Bean が見つかると、AnnotatedControllerConfigurer は、アノテーション付きコントローラーメソッドでの Bean バリデーションのサポートを有効にします。通常、Bean の型は LocalValidatorFactoryBean です。
Bean 検証では、型に対する制約を宣言できます。
public class BookInput {
@NotNull
private String title;
@NotNull
@Size(max=13)
private String isbn;
}
次に、コントローラーメソッドパラメーターに @Valid のアノテーションを付けて、メソッド呼び出しの前に検証できます。
@Controller
public class BookController {
@MutationMapping
public Book addBook(@Argument @Valid BookInput bookInput) {
// ...
}
}
検証中にエラーが発生すると、ConstraintViolationException が発生します。例外の解決チェーンを使用して、GraphQL レスポンスに含めるエラーに変換することで、それをクライアントに提示する方法を決定できます。
@Valid に加えて、検証グループを指定できる Spring の @Validated も使用できます。 |
Bean 検証は、@Argument、@Arguments、@ProjectedPayload メソッドパラメーターに役立ちますが、より一般的にはすべてのメソッドパラメーターに適用されます。
検証と Kotlin コルーチン Hibernate Validator は Kotlin コルーチンメソッドと互換性がなく、メソッドパラメーターをイントロスペクトするときに失敗します。関連する課題と推奨される回避策へのリンクについては、spring-projects/spring-graphql#344 (コメント) [GitHub] (英語) を参照してください。 |
5.3. @BatchMapping
バッチ読み込みは、org.dataloader.DataLoader を使用して個々のエンティティインスタンスのロードを延期することで N+1 選択の問題に対処し、一緒にロードできるようにします。例:
@Controller
public class BookController {
public BookController(BatchLoaderRegistry registry) {
registry.forTypePair(Long.class, Author.class).registerMappedBatchLoader((authorIds, env) -> {
// return Map<Long, Author>
});
}
@SchemaMapping
public CompletableFuture<Author> author(Book book, DataLoader<Long, Author> loader) {
return loader.load(book.getAuthorId());
}
}
上記の関連付けられたエンティティをロードする単純なケースでは、@SchemaMapping メソッドは DataLoader に委譲するだけです。これは、@BatchMapping メソッドで回避できるボイラープレートです。例:
@Controller
public class BookController {
@BatchMapping
public Mono<Map<Book, Author>> author(List<Book> books) {
// ...
}
}
上記は、キーが Book インスタンスであり、読み込まれた値が作成者である BatchLoaderRegistry のバッチ読み込み関数になります。さらに、DataFetcher は、型 Book の author フィールドにも透過的にバインドされます。これは、そのソース / 親 Book インスタンスが与えられると、作成者の DataLoader に単純に委譲します。
一意のキーとして使用するには、 |
デフォルトでは、フィールド名はメソッド名にデフォルト設定され、型名は入力 List 要素型の単純なクラス名にデフォルト設定されます。どちらもアノテーション属性を介してカスタマイズできます。型名は、クラスレベル @SchemaMapping から継承することもできます。
5.3.1. メソッド署名
バッチマッピングメソッドは、次の引数をサポートしています。
| メソッド引数 | 説明 |
|---|---|
| ソース / 親オブジェクト。 |
| 利用可能な場合、Spring Security コンテキストから取得されます。 |
|
|
|
|
|
|
バッチマッピングメソッドは以下を返すことができます。
| 戻りの型 | 説明 |
|---|---|
| 親オブジェクトをキーとして、バッチロードされたオブジェクトを値として持つマップ。 |
| メソッドに渡されたソース / 親オブジェクトと同じ順序である必要があるバッチロードオブジェクトのシーケンス。 |
| 命令型のバリアント。リモート呼び出しを行う必要はありません。 |
| 非同期で呼び出される命令型バリアント。これを機能させるには、 |
6. セキュリティ
Web GraphQL エンドポイントへのパスを HTTP URL セキュリティで保護して、認証されたユーザーのみがアクセスできるようにすることができます。ただし、これは、単一の URL 上の共有エンドポイントに対する異なる GraphQL リクエストを区別するものではありません。
よりきめ細かいセキュリティを適用するには、@PreAuthorize や @Secured などの Spring Security アノテーションを、GraphQL レスポンスの特定の部分のフェッチに関連するサービスメソッドに追加します。これは、セキュリティやその他のコンテキストをデータ取得レベルで利用できるようにすることを目的としたコンテキストの伝播により機能するはずです。
このリポジトリの 1.0.x ブランチには、Spring MVC [GitHub] (英語) および WebFlux [GitHub] (英語) のサンプルが含まれています。
7. 可観測性
Micrometer による可観測性のサポート (英語) は Spring for GraphQL に直接組み込まれています。これにより、GraphQL リクエストのメトリクスとトレースの両方と、「重要な」データフェッチ操作が可能になります。GraphQL エンジンはトランスポート層の上で動作するため、Spring Framework でサポートされている場合は、トランスポートからの観測も期待する必要があります。
アプリケーションで ObservationRegistry が構成されている場合にのみ、観測結果が公開されます。Spring Boot でオブザーバビリティインフラストラクチャの構成について詳しく知ることができます。GraphQL 観測で生成されたメタデータをカスタマイズする場合は、インストルメンテーションでカスタム規則を直接構成できます。アプリケーションが Spring Boot を使用している場合、カスタム規則を Bean として提供することが推奨される方法です。
7.1. サーバーリクエストインストルメンテーション
GraphQL サーバーリクエストの観測は、従来のアプリケーションとリアクティブアプリケーション、何よりもサポートされているトランスポートに対して "graphql.request" という名前で作成されます。このインストルメンテーションは、親観測が既知の "micrometer.observation" キーを使用して GraphQL コンテキスト上の現在の観測として設定される必要があることを前提としています。ネットワーク境界を越えたトレース伝播の場合、トランスポートレベルで別の計測を担当する必要があります。HTTP の場合、Spring Framework にはトレースの伝播を処理する専用のインストルメンテーションがあります。
アプリケーションは、アプリケーション内で org.springframework.graphql.observation.GraphQlObservationInstrumentation インストルメンテーションを構成する必要があります。デフォルトでは org.springframework.graphql.observation.DefaultExecutionRequestObservationConvention を使用し、ExecutionRequestObservationContext によってサポートされます。
デフォルトでは、次の KeyValues が作成されます。
名前 | 説明 |
| GraphQL オペレーション名。 |
| GraphQL リクエストの結果。 |
graphql.operation KeyValue は、提供されたクエリのカスタム名を使用するか、何もない場合は操作の標準名 (英語) ("query"、"mutation" または "subscription") を使用します。graphql.outcome KeyValue は、有効な GraphQL レスポンスが送信された場合は "SUCCESS" になり、リクエストを解析できなかった場合は "REQUEST_ERROR" になり、有効な GraphQL レスポンスを生成できなかった場合は "INTERNAL_ERROR" になります。
名前 | 説明 |
| GraphQL リクエストの |
7.2. DataFetcher 計装
GraphQL DataFetcher オブザベーションは "graphql.datafetcher" という名前で作成され、「重要」と見なされるデータフェッチ操作に対してのみ作成されます (Java オブジェクトのプロパティフェッチは簡単な操作です)。アプリケーションは、そのアプリケーションで org.springframework.graphql.observation.GraphQlObservationInstrumentation 計装を構成する必要があります。デフォルトでは org.springframework.graphql.observation.DefaultDataFetcherObservationConvention を使用しており、DataFetcherObservationContext に支えられています。
デフォルトでは、次の KeyValues が作成されます。
名前 | 説明 |
| データ取得エラーのクラス名 |
| フェッチされるフィールドの名前。 |
| GraphQL データ取得オペレーションの結果、"SUCCESS" または "ERROR"。 |
名前 | 説明 |
| フェッチされるフィールドへのパス (例: "/bookById")。 |
8. GraalVM ネイティブサポート
Spring Framework 6.0 は、Spring アプリケーションを GraalVM ネイティブイメージ (英語) にコンパイルするためのサポートインフラストラクチャを導入しました。一般的に GraalVM に精通していない場合、これが JVM にデプロイされたアプリケーションとどのように異なるか、および Spring アプリケーションでそれが何を意味するかについては、専用の Spring Boot 3.0 GraalVM ネイティブイメージのサポートドキュメントを参照してください。Spring Boot は、Spring での GraalVM サポートに関する既知の制限 [GitHub] (英語) もドキュメント化しています。
8.1. GraphQL Java メタデータ
アプリケーションの静的分析はビルド時に行われるため、アプリケーションが静的リソースを検索したり、リフレクションを実行したり、実行時に JDK プロキシを作成したりする場合、GraalVM は追加のヒントを必要とする場合があります。
GraphQL Java は、実行時にネイティブイメージが認識できる 3 つのタスクを実行します。
メッセージの国際化のためのリソースバンドルの読み込み
スキーマインスペクションの内部型に関する考察
アプリケーションがスキーマに登録する Java 型の反映。これは、たとえば、GraphQL Java がアプリケーション型からプロパティを取得しているときに発生します。
最初の 2 つの項目は、Spring チームによって GraalVM 到達可能性メタデータリポジトリ [GitHub] (英語) に提供された到達可能性メタデータを介して処理されます。このメタデータは、GraphQL Java に依存するアプリケーションをビルドするときに、ネイティブコンパイルツールによって自動的に取得されます。リストの 3 番目の項目は対象外です。これらの型はアプリケーション自体によって提供され、別の方法で検出する必要があるためです。
8.2. ネイティブサーバーアプリケーションのサポート
典型的な Spring for GraphQL アプリケーションでは、GraphQL スキーマに結び付けられた Java 型は、パラメーターまたは戻り型として @Controller メソッドシグネチャーで公開されます。ビルドの Ahead Of Time 処理フェーズの間、Spring または GraphQL はその o.s.g.data.method.annotation.support.SchemaMappingBeanFactoryInitializationAotProcessor を使用して関連する型を検出し、それに応じて到達可能性メタデータを登録します。GraalVM をサポートする Spring Boot アプリケーションを構築している場合、これはすべて自動的に行われます。
アプリケーションがデータフェッチャーを「手動で」登録している場合、結果として一部の型が検出されません。次に、Spring Framework の @RegisterReflectionForBinding に登録する必要があります。
@Configuration
@RegisterReflectionForBinding(Book.class) (3)
public class GraphQlConfiguration {
@Bean
RuntimeWiringConfigurer customWiringConfigurer(BookRepository bookRepository) { (1)
DataFetcher<Book> dataFetcher = QuerydslDataFetcher.builder(bookRepository).single();
return wiringBuilder -> wiringBuilder
.type("Query", builder -> builder.dataFetcher("book", dataFetcher)); (2)
}
}
| 1 | このアプリケーションは、DataFetcher を「手動で」追加する RuntimeWiringConfigurer を宣言します。 |
| 2 | この DataFetcher を通じて、BookRepository は Book 型を公開します |
| 3 | @RegisterReflectionForBinding は、Book 型およびフィールドとして公開されるすべての型に関連するヒントを登録します。 |
8.3. クライアントサポート
GraphQlClient は、必ずしもアプリケーションコンテキストで Bean として存在するわけではなく、メソッドシグネチャーのスキーマで使用される Java 型を公開しません。そのため、上記のセクションで説明した AotProcessor 戦略は使用できません。クライアントサポートのために、Spring for GraphQL はクライアントインフラストラクチャに関連する到達可能性メタデータ [GitHub] (英語) を埋め込みます。アプリケーションで使用される Java 型に関しては、アプリケーションは @RegisterReflectionForBinding を使用した「手動」データフェッチャーと同様の戦略を使用する必要があります。
@Component
@RegisterReflectionForBinding(Project.class) (2)
public class ProjectService {
private final GraphQlClient graphQlClient;
public ProjectService(GraphQlClient graphQlClient) {
this.graphQlClient = graphQlClient;
}
public Mono<Project> project(String projectSlug) {
String document = """
query projectWithReleases($projectSlug: ID!) {
project(slug: $projectSlug) {
name
releases {
version
}
}
}
""";
return this.graphQlClient.document(document)
.variable("projectSlug", projectSlug)
.retrieve("project")
.toEntity(Project.class); (1)
}
}
| 1 | ネイティブイメージでは、実行時に Project でリフレクションを実行できるようにする必要があります。 |
| 2 | @RegisterReflectionForBinding は、Project 型およびフィールドとして公開されるすべての型に関連するヒントを登録します。 |
9. クライアント
Spring for GraphQL には、HTTP、WebSocket、RSocket を介して GraphQL リクエストを実行するためのクライアントサポートが含まれています。
9.1. GraphQlClient
GraphQlClient は、基になるトランスポートから独立した GraphQL リクエストの共通ワークフローを宣言する契約です。つまり、基になるトランスポートが何であっても、リクエストは同じ API で実行され、トランスポート固有のものはビルド時に構成されます。
GraphQlClient を作成するには、次の拡張のいずれかが必要です。
それぞれが、トランスポートに関連するオプションを持つ Builder を定義します。すべてのビルダーは、共通のベース GraphQlClient Builder から拡張され、すべての拡張に関連するオプションがあります。
GraphQlClient を取得したら、リクエストを開始できます。
9.1.1. HTTP
HttpGraphQlClient は WebClient を使用して、HTTP 経由で GraphQL リクエストを実行します。
WebClient webClient = ... ;
HttpGraphQlClient graphQlClient = HttpGraphQlClient.create(webClient);
HttpGraphQlClient が作成されると、基礎となるトランスポートとは関係なく、同じ API を使用してリクエストの実行を開始できます。トランスポート固有の詳細を変更する必要がある場合は、既存の HttpGraphQlClient で mutate() を使用して、カスタマイズされた設定で新しいインスタンスを作成します。
WebClient webClient = ... ;
HttpGraphQlClient graphQlClient = HttpGraphQlClient.builder(webClient)
.headers(headers -> headers.setBasicAuth("joe", "..."))
.build();
// Perform requests with graphQlClient...
HttpGraphQlClient anotherGraphQlClient = graphQlClient.mutate()
.headers(headers -> headers.setBasicAuth("peter", "..."))
.build();
// Perform requests with anotherGraphQlClient...
9.1.2. WebSocket
WebSocketGraphQlClient は、共有 WebSocket 接続を介して GraphQL リクエストを実行します。これは Spring WebFlux の WebSocketClient を使用して構築されており、次のように作成できます。
String url = "wss://localhost:8080/graphql";
WebSocketClient client = new ReactorNettyWebSocketClient();
WebSocketGraphQlClient graphQlClient = WebSocketGraphQlClient.builder(url, client).build();
HttpGraphQlClient とは対照的に、WebSocketGraphQlClient は接続指向です。つまり、リクエストを行う前に接続を確立する必要があります。リクエストを開始すると、接続が透過的に確立されます。または、クライアントの start() メソッドを使用して、リクエストの前に明示的に接続を確立します。
接続指向であることに加えて、WebSocketGraphQlClient は多重化もされています。すべてのリクエストに対して単一の共有接続を維持します。接続が失われた場合は、次のリクエストで、または start() が再度呼び出された場合に再確立されます。進行中のリクエストをキャンセルし、接続を閉じ、新しいリクエストを拒否するクライアントの stop() メソッドを使用することもできます。
そのサーバーへのすべてのリクエストに対して単一の共有接続を確立するには、サーバーごとに単一の WebSocketGraphQlClient インスタンスを使用します。各クライアントインスタンスは独自の接続を確立しますが、これは通常、単一のサーバーを対象とするものではありません。 |
WebSocketGraphQlClient が作成されると、基礎となるトランスポートとは関係なく、同じ API を使用してリクエストの実行を開始できます。トランスポート固有の詳細を変更する必要がある場合は、既存の WebSocketGraphQlClient で mutate() を使用して、カスタマイズされた設定で新しいインスタンスを作成します。
URI url = ... ;
WebSocketClient client = ... ;
WebSocketGraphQlClient graphQlClient = WebSocketGraphQlClient.builder(url, client)
.headers(headers -> headers.setBasicAuth("joe", "..."))
.build();
// Use graphQlClient...
WebSocketGraphQlClient anotherGraphQlClient = graphQlClient.mutate()
.headers(headers -> headers.setBasicAuth("peter", "..."))
.build();
// Use anotherGraphQlClient...
インターセプター
WebSocket 上の GraphQL [GitHub] (英語) プロトコルは、実行リクエストに加えて、多数の接続指向メッセージを定義します。例: クライアントは "connection_init" を送信し、サーバーは接続の開始時に "connection_ack" で応答します。
WebSocket トランスポート固有のインターセプトの場合、WebSocketGraphQlClientInterceptor を作成できます。
static class MyInterceptor implements WebSocketGraphQlClientInterceptor {
@Override
public Mono<Object> connectionInitPayload() {
// ... the "connection_init" payload to send
}
@Override
public Mono<Void> handleConnectionAck(Map<String, Object> ackPayload) {
// ... the "connection_ack" payload received
}
}
上記のインターセプターを他の GraphQlClientInterceptor と同様に登録し、それを GraphQL リクエストのインターセプトにも使用しますが、型 WebSocketGraphQlClientInterceptor のインターセプターは最大で 1 つしか存在できないことに注意してください。
9.1.3. RSocket
RSocketGraphQlClient は RSocketRequester を使用して、RSocket リクエストを介して GraphQL リクエストを実行します。
URI uri = URI.create("wss://localhost:8080/rsocket");
WebsocketClientTransport transport = WebsocketClientTransport.create(url);
RSocketGraphQlClient client = RSocketGraphQlClient.builder()
.clientTransport(transport)
.build();
HttpGraphQlClient とは対照的に、RSocketGraphQlClient は接続指向です。つまり、リクエストを行う前にセッションを確立する必要があります。リクエストを開始すると、セッションは透過的に確立されます。または、クライアントの start() メソッドを使用して、リクエストの前に明示的にセッションを確立します。
RSocketGraphQlClient も多重化されています。すべてのリクエストに対して単一の共有セッションを維持します。セッションが失われた場合、次のリクエストで、start() が再度呼び出された場合に再確立されます。進行中のリクエストをキャンセルし、セッションを閉じ、新しいリクエストを拒否するクライアントの stop() メソッドを使用することもできます。
そのサーバーへのすべてのリクエストに対して単一の共有セッションを使用するには、サーバーごとに単一の RSocketGraphQlClient インスタンスを使用します。各クライアントインスタンスは独自の接続を確立しますが、これは通常、単一のサーバーを対象とするものではありません。 |
RSocketGraphQlClient が作成されると、基礎となるトランスポートとは関係なく、同じ API を使用してリクエストの実行を開始できます。
9.1.4. ビルダー
GraphQlClient は、すべての拡張機能のビルダーに共通の構成オプションを持つ親 Builder を定義します。現在、以下を構成できます。
ファイルからリクエストのドキュメントをロードする
DocumentSource戦略実行されたリクエストのインターセプト
9.2. 要求
GraphQlClient を取得したら、retrieve() または execute() を介してリクエストの実行を開始できます。前者は後者のショートカットにすぎません。
9.2.1. 取得
以下は、クエリのデータを取得してデコードします。
String document = "{" +
" project(slug:\"spring-framework\") {" +
" name" +
" releases {" +
" version" +
" }"+
" }" +
"}";
Mono<Project> projectMono = graphQlClient.document(document) (1)
.retrieve("project") (2)
.toEntity(Project.class); (3)
| 1 | 実行する操作。 |
| 2 | デコード元のレスポンスマップの「データ」キーのパス。 |
| 3 | ターゲット型へのパスでデータをデコードします。 |
入力ドキュメントは String であり、リテラルまたはコード生成されたリクエストオブジェクトによって生成されます。ファイルでドキュメントを定義し、ドキュメントソースを使用してファイル名でドキュメントを再発行することもできます。
パスは「データ」キーに相対的であり、ネストされたフィールドには単純なドット ( "." ) で区切られた表記法を使用し、リスト要素のオプションの配列インデックスを使用します。"project.name" または "project.releases[0].version"。
指定されたパスが存在しない場合、またはフィールド値が null でエラーがある場合、デコードすると FieldAccessException になる可能性があります。FieldAccessException は、レスポンスとフィールドへのアクセスを提供します。
Mono<Project> projectMono = graphQlClient.document(document)
.retrieve("project")
.toEntity(Project.class)
.onErrorResume(FieldAccessException.class, ex -> {
ClientGraphQlResponse response = ex.getResponse();
// ...
ClientResponseField field = ex.getField();
// ...
});
9.2.2. 実行
取得は、レスポンスマップ内の単一のパスからデコードするためのショートカットにすぎません。さらに制御するには、execute メソッドを使用してレスポンスを処理します。
例:
Mono<Project> projectMono = graphQlClient.document(document)
.execute()
.map(response -> {
if (!response.isValid()) {
// Request failure... (1)
}
ClientResponseField field = response.field("project");
if (!field.hasValue()) {
if (field.getError() != null) {
// Field failure... (2)
}
else {
// Optional field set to null... (3)
}
}
return field.toEntity(Project.class); (4)
});
| 1 | レスポンスにはデータがなく、エラーのみがあります |
| 2 | null で、関連するエラーがあるフィールド |
| 3 | DataFetcher によって null に設定されたフィールド |
| 4 | 指定されたパスでデータをデコードします |
9.2.3. ドキュメントソース
リクエストのドキュメントは String であり、ローカル変数または定数で定義されるか、コード生成されたリクエストオブジェクトによって生成されます。
クラスパス上の "graphql-documents/" に、拡張子 .graphql または .gql のドキュメントファイルを作成し、ファイル名で参照することもできます。
例: src/main/resources/graphql-documents 内の projectReleases.graphql というファイルが与えられた場合、内容は次のとおりです。
query projectReleases($slug: ID!) {
project(slug: $slug) {
name
releases {
version
}
}
}その後、次のことができます。
Mono<Project> projectMono = graphQlClient.documentName("projectReleases") (1)
.variable("slug", "spring-framework") (2)
.retrieve()
.toEntity(Project.class);
| 1 | "projectReleases.graphql" からドキュメントをロードします |
| 2 | 変数値を提供します。 |
IntelliJ の "JS GraphQL" プラグインは、コード補完 で GraphQL クエリファイルをサポートします。
GraphQlClient ビルダーを使用して、名前でドキュメントをロードするために DocumentSource をカスタマイズできます。
9.3. サブスクリプションリクエスト
GraphQlClient は、サブスクリプションをサポートするトランスポートを介してサブスクリプションを実行できます。現在、WebSocket トランスポートのみが GraphQL ストリームをサポートしているため、WebSocketGraphQlClient を作成する必要があります。
9.3.1. 取得
サブスクリプションストリームを開始するには、retrieveSubscription を使用します。これは、単一のレスポンスを取得するのに似ていますが、それぞれが何らかのデータにデコードされたレスポンスのストリームを返します。
Flux<String> greetingFlux = client.document("subscription { greetings }")
.retrieveSubscription("greeting")
.toEntity(String.class);
サブスクリプションストリームは、次のように終了する場合があります。
サーバーが 1 つ以上の GraphQL エラーを含む明示的な「エラー」メッセージでサブスクリプションを終了する場合は
SubscriptionErrorException。例外は、そのメッセージからデコードされた GraphQL エラーへのアクセスを提供します。WebSocketDisconnectedExceptionなどのGraphQlTransportException(基になる接続が閉じられているか失われている場合)。この場合、retryオペレーターを使用して接続を再確立し、サブスクリプションを再開できます。
9.3.2. 実行
取得は、各レスポンスマップ内の単一のパスからデコードするためのショートカットにすぎません。さらに制御するには、executeSubscription メソッドを使用して、各レスポンスを直接処理します。
Flux<String> greetingFlux = client.document("subscription { greetings }")
.executeSubscription()
.map(response -> {
if (!response.isValid()) {
// Request failure...
}
ClientResponseField field = response.field("project");
if (!field.hasValue()) {
if (field.getError() != null) {
// Field failure...
}
else {
// Optional field set to null... (3)
}
}
return field.toEntity(String.class)
});
9.4. インターセプト
クライアントを介してすべてのリクエストをインターセプトする GraphQlClientInterceptor を作成します。
static class MyInterceptor implements GraphQlClientInterceptor {
@Override
public Mono<ClientGraphQlResponse> intercept(ClientGraphQlRequest request, Chain chain) {
// ...
return chain.next(request);
}
@Override
public Flux<ClientGraphQlResponse> interceptSubscription(ClientGraphQlRequest request, SubscriptionChain chain) {
// ...
return chain.next(request);
}
}
インターセプターが作成されたら、クライアントビルダーを使用して登録します。
URI url = ... ;
WebSocketClient client = ... ;
WebSocketGraphQlClient graphQlClient = WebSocketGraphQlClient.builder(url, client)
.interceptor(new MyInterceptor())
.build();
10. テスト
Spring for GraphQL は、HTTP、WebSocket、RSocket を介した GraphQL リクエストのテスト、およびサーバーに対する直接のテストの専用サポートを提供します。
これを利用するには、ビルドに spring-graphql-test を追加します。
dependencies {
// ...
testImplementation 'org.springframework.graphql:spring-graphql-test:1.1.4'
}
<dependencies>
<!-- ... -->
<dependency>
<groupId>org.springframework.graphql</groupId>
<artifactId>spring-graphql-test</artifactId>
<version>1.1.4</version>
<scope>test</scope>
</dependency>
</dependencies>10.1. GraphQlTester
GraphQlTester は、基礎となるトランスポートから独立した GraphQL リクエストをテストするための共通のワークフローを宣言する契約です。つまり、基礎となるトランスポートに関係なく、リクエストは同じ API でテストされ、トランスポート固有のものはビルド時に構成されます。
クライアント経由でリクエストを実行する GraphQlTester を作成するには、次の拡張のいずれかが必要です。
クライアントなしでサーバー側でテストを実行する GraphQlTester を作成するには:
それぞれが、トランスポートに関連するオプションを持つ Builder を定義します。すべてのビルダーは、共通のベース GraphQlTester Builder から拡張され、すべての拡張に関連するオプションがあります。
10.1.1. HTTP
HttpGraphQlTester は WebTestClient を使用して、WebTestClient の構成方法に応じて、ライブサーバーの有無にかかわらず、HTTP 経由で GraphQL リクエストを実行します。
ライブサーバーなしで Spring WebFlux でテストするには、GraphQL HTTP エンドポイントを宣言する Spring 構成を指定します。
ApplicationContext context = ... ;
WebTestClient client =
WebTestClient.bindToApplicationContext(context)
.configureClient()
.baseUrl("/graphql")
.build();
HttpGraphQlTester tester = HttpGraphQlTester.create(client);
ライブサーバーなしで Spring MVC でテストするには、MockMvcWebTestClient を使用して同じことを行います。
ApplicationContext context = ... ;
WebTestClient client =
MockMvcWebTestClient.bindToApplicationContext(context)
.configureClient()
.baseUrl("/graphql")
.build();
HttpGraphQlTester tester = HttpGraphQlTester.create(client);
または、ポートで実行されているライブサーバーに対してテストするには、次のようにします。
WebTestClient client =
WebTestClient.bindToServer()
.baseUrl("http://localhost:8080/graphql")
.build();
HttpGraphQlTester tester = HttpGraphQlTester.create(client);
HttpGraphQlTester が作成されると、基礎となるトランスポートとは関係なく、同じ API を使用してリクエストの実行を開始できます。トランスポート固有の詳細を変更する必要がある場合は、既存の HttpSocketGraphQlTester で mutate() を使用して、カスタマイズされた設定で新しいインスタンスを作成します。
HttpGraphQlTester tester = HttpGraphQlTester.builder(clientBuilder)
.headers(headers -> headers.setBasicAuth("joe", "..."))
.build();
// Use tester...
HttpGraphQlTester anotherTester = tester.mutate()
.headers(headers -> headers.setBasicAuth("peter", "..."))
.build();
// Use anotherTester...
10.1.2. WebSocket
WebSocketGraphQlTester は、共有 WebSocket 接続を介して GraphQL リクエストを実行します。これは Spring WebFlux の WebSocketClient を使用して構築されており、次のように作成できます。
String url = "http://localhost:8080/graphql";
WebSocketClient client = new ReactorNettyWebSocketClient();
WebSocketGraphQlTester tester = WebSocketGraphQlTester.builder(url, client).build();
WebSocketGraphQlTester は接続指向で多重化されています。各インスタンスは、すべてのリクエストに対して独自の単一の共有接続を確立します。通常、サーバーごとに 1 つのインスタンスのみを使用します。
WebSocketGraphQlTester が作成されると、基礎となるトランスポートとは関係なく、同じ API を使用してリクエストの実行を開始できます。トランスポート固有の詳細を変更する必要がある場合は、既存の WebSocketGraphQlTester で mutate() を使用して、カスタマイズされた設定で新しいインスタンスを作成します。
URI url = ... ;
WebSocketClient client = ... ;
WebSocketGraphQlTester tester = WebSocketGraphQlTester.builder(url, client)
.headers(headers -> headers.setBasicAuth("joe", "..."))
.build();
// Use tester...
WebSocketGraphQlTester anotherTester = tester.mutate()
.headers(headers -> headers.setBasicAuth("peter", "..."))
.build();
// Use anotherTester...
WebSocketGraphQlTester は、WebSocket 接続を閉じるために使用できる stop() メソッドを提供します。テストの実行後。
10.1.3. RSocket
RSocketGraphQlTester は spring-messaging の RSocketRequester を使用して、RSocket 経由で GraphQL リクエストを実行します。
URI uri = URI.create("wss://localhost:8080/rsocket");
WebsocketClientTransport transport = WebsocketClientTransport.create(url);
RSocketGraphQlTester client = RSocketGraphQlTester.builder()
.clientTransport(transport)
.build();
RSocketGraphQlTester は接続指向で多重化されています。各インスタンスは、すべてのリクエストに対して独自の単一の共有セッションを確立します。通常、サーバーごとに 1 つのインスタンスのみを使用します。テスターで stop() メソッドを使用して、セッションを明示的に閉じることができます。
RSocketGraphQlTester が作成されると、基礎となるトランスポートとは関係なく、同じ API を使用してリクエストの実行を開始できます。
10.1.4. GraphQlService
多くの場合、クライアントを使用してトランスポートプロトコル経由でリクエストを送信することなく、サーバー側で GraphQL リクエストをテストするだけで十分です。ExecutionGraphQlService に対して直接テストするには、ExecutionGraphQlServiceTester 拡張機能を使用します。
GraphQlService service = ... ;
ExecutionGraphQlServiceTester tester = ExecutionGraphQlServiceTester.create(service);
ExecutionGraphQlServiceTester が作成されると、基礎となるトランスポートとは関係なく、同じ API を使用してリクエストの実行を開始できます。
ExecutionGraphQlServiceTester.Builder は、ExecutionInput の詳細をカスタマイズするオプションを提供します。
GraphQlService service = ... ;
ExecutionGraphQlServiceTester tester = ExecutionGraphQlServiceTester.builder(service)
.configureExecutionInput((executionInput, builder) -> builder.executionId(id).build())
.build();
10.1.5. WebGraphQlHandler
GraphQlService 拡張機能を使用すると、クライアントなしでサーバー側でテストできます。ただし、場合によっては、指定されたモックトランスポート入力でサーバー側のトランスポート処理を使用すると便利です。
WebGraphQlTester 拡張機能を使用すると、リクエストの実行のために ExecutionGraphQlService にハンドオフする前に、WebGraphQlInterceptor チェーンを介してリクエストを処理できます。
WebGraphQlHandler handler = ... ;
WebGraphQlTester tester = WebGraphQlTester.create(handler);
この拡張機能のビルダーを使用すると、HTTP リクエストの詳細を定義できます。
WebGraphQlHandler handler = ... ;
WebGraphQlTester tester = WebGraphQlTester.builder(handler)
.headers(headers -> headers.setBasicAuth("joe", "..."))
.build();
WebGraphQlServiceTester が作成されると、基礎となるトランスポートとは関係なく、同じ API を使用してリクエストの実行を開始できます。
10.2. 要求
GraphQlTester を取得したら、リクエストのテストを開始できます。以下は、プロジェクトのクエリを実行し、JsonPath [GitHub] (英語) を使用して、レスポンスからプロジェクトリリースバージョンを抽出します。
String document = "{" +
" project(slug:\"spring-framework\") {" +
" releases {" +
" version" +
" }"+
" }" +
"}";
graphQlTester.document(document)
.execute()
.path("project.releases[*].version")
.entityList(String.class)
.hasSizeGreaterThan(1);
JsonPath は、レスポンスの「データ」セクションに関連しています。
クラスパス上の "graphql-test/" に、拡張子 .graphql または .gql のドキュメントファイルを作成し、ファイル名で参照することもできます。
例: src/main/resources/graphql-test 内の projectReleases.graphql というファイルが与えられた場合、内容は次のとおりです。
query projectReleases($slug: ID!) {
project(slug: $slug) {
releases {
version
}
}
}その後、次を使用できます。
graphQlTester.documentName("projectReleases") (1)
.variable("slug", "spring-framework") (2)
.execute()
.path("project.releases[*].version")
.entityList(String.class)
.hasSizeGreaterThan(1);
| 1 | "project" という名前のファイル内のドキュメントを参照してください。 |
| 2 | slug 変数を設定します。 |
IntelliJ の "JS GraphQL" プラグインは、コード補完 で GraphQL クエリファイルをサポートします。 |
リクエストにレスポンスデータがない場合。レスポンスにエラーがないことを確認するには、execute の代わりに executeAndVerify を使用します。
graphQlTester.query(query).executeAndVerify();
エラー処理の詳細については、エラーを参照してください。
10.3. サブスクリプション
サブスクリプションをテストするには、execute の代わりに executeSubscription を呼び出してレスポンスのストリームを取得し、プロジェクト Reactor の StepVerifier を使用してストリームを調べます。
Flux<String> greetingFlux = tester.document("subscription { greetings }")
.executeSubscription()
.toFlux("greetings", String.class); // decode at JSONPath
StepVerifier.create(greetingFlux)
.expectNext("Hi")
.expectNext("Bonjour")
.expectNext("Hola")
.verifyComplete();
サブスクリプションは、WebSocketGraphQlTester、またはサーバー側の GraphQlService および WebGraphQlHandler 拡張機能でのみサポートされています。
10.4. エラー
verify() を使用すると、レスポンスの "errors" キーにエラーがあると、アサーションエラーが発生します。特定のエラーを抑制するには、verify() の前にエラーフィルターを使用します。
graphQlTester.query(query)
.execute()
.errors()
.filter(error -> ...)
.verify()
.path("project.releases[*].version")
.entityList(String.class)
.hasSizeGreaterThan(1);
ビルダーレベルでエラーフィルターを登録して、すべてのテストに適用できます。
WebGraphQlTester graphQlTester = WebGraphQlTester.builder(client)
.errorFilter(error -> ...)
.build();
エラーが存在することを確認したい場合、filter とは対照的に、存在しない場合はアサーションエラーをスローし、代わりに exepect を使用します。
graphQlTester.query(query)
.execute()
.errors()
.expect(error -> ...)
.verify()
.path("project.releases[*].version")
.entityList(String.class)
.hasSizeGreaterThan(1);
Consumer を介してすべてのエラーをインスペクションすることもできます。そうすることで、フィルター処理済みとしてマークされるため、レスポンスのデータをインスペクションすることもできます。
graphQlTester.query(query)
.execute()
.errors()
.satisfy(errors -> {
// ...
});
11. Boot スターター
Spring Boot は、Spring for GraphQL を使用して GraphQL アプリケーションを構築するためのスターターを提供します。バージョン情報については、Spring for GraphQL バージョン [GitHub] (英語) wiki ページを参照してください。
開始する最も簡単な方法は、HTTP または WebSocket、または RSocket を介した WebFlux の Spring MVC などの基になるトランスポートと共に "Spring for GraphQL" を選択して start.spring.io を使用することです。サポートされているトランスポート、自動構成関連の機能などの詳細については、Spring Boot リファレンスの Spring for GraphQL スターターセクションを参照してください。テストのサポートについては、自動構成された GraphQL テストを参照してください。
詳細については、次の GraphQL 関連を確認してください。
12. サンプル
「GraphQL サービスの構築」入門ガイドを参照してください。
さらに、このリポジトリの 1.0.x ブランチには、さまざまなシナリオのサンプルアプリケーション [GitHub] (英語) が含まれています。これらのサンプルは main ブランチには存在せず、別のリポジトリに移動する [GitHub] (英語) 予定です。これらのサンプルを実行するには、1.0.x ブランチを確認して、IDE またはコマンドラインからメインアプリケーションクラスを実行します。
$ ./gradlew :samples:{sample-directory-name}:bootRun