クライアント

Spring for GraphQL には、HTTP、WebSocket、RSocket 経由で GraphQL リクエストを実行するためのクライアントサポートが含まれています。

GraphQlClient

GraphQlClient は、基になるトランスポートとは独立した GraphQL リクエストの共通ワークフローを定義するため、どのトランスポートが使用されているかに関係なく、リクエストを実行する方法は同じです。

次のトランスポート固有の GraphQlClient 拡張機能が使用可能です。

それぞれが、トランスポートに関連するオプションを備えた Builder を定義します。すべてのビルダーは、すべてのトランスポートに適用できるオプションを備えた共通のベース GraphQlClient Builder から拡張されています。

GraphQlClient が構築されたら、リクエストの作成を開始できます。

通常、リクエストの GraphQL オペレーションはテキストとして提供されます。あるいは、上記の GraphQlClient 拡張機能のいずれかをラップできる DgsGraphQlClient を通じて DGS コードジェネ [GitHub] (英語) クライアント API クラスを使用することもできます。

HTTP 同期

HttpSyncGraphQlClient は、RestClient を使用して、ブロッキングトランスポート 契約とインターセプターの チェーン を通じて HTTP 経由で GraphQL リクエストを実行します。

RestClient restClient = ... ;
HttpSyncGraphQlClient graphQlClient = HttpSyncGraphQlClient.create(restClient);

HttpSyncGraphQlClient が作成されると、基礎となるトランスポートとは関係なく、同じ API を使用してリクエストの実行を開始できます。トランスポート固有の詳細を変更する必要がある場合は、既存の HttpSyncGraphQlClient で mutate() を使用して、カスタマイズされた設定で新しいインスタンスを作成します。

   RestClient restClient = ... ;

HttpSyncGraphQlClient graphQlClient = HttpSyncGraphQlClient.builder(restClient)
		.headers(headers -> headers.setBasicAuth("joe", "..."))
		.build();

// Perform requests with graphQlClient...

HttpSyncGraphQlClient anotherGraphQlClient = graphQlClient.mutate()
		.headers(headers -> headers.setBasicAuth("peter", "..."))
		.build();

// Perform requests with anotherGraphQlClient...

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...

WebSocket

WebSocketGraphQlClient は、共有 WebSocket 接続を介して GraphQL リクエストを実行します。これは Spring WebFluxWebSocketClient を使用して構築されており、次のように作成できます。

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...

WebSocketGraphQlClient は、他のメッセージが送受信されていないときに接続をアクティブに保つために、定期的な ping メッセージの送信をサポートしています。これを有効にするには、次の手順を実行します。

URI url = ... ;
WebSocketClient client = ... ;

WebSocketGraphQlClient graphQlClient = WebSocketGraphQlClient.builder(url, client)
		.keepAlive(Duration.ofSeconds(30))
		.build();

インターセプター

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 つしか存在できないことに注意してください。

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 を使用してリクエストの実行を開始できます。

ビルダー

GraphQlClient は、すべての拡張機能のビルダーに共通の構成オプションを持つ親 BaseBuilder を定義します。現在、以下を構成できます。

  • ファイルからリクエストのドキュメントをロードする DocumentSource 戦略

  • 実行されたリクエストのインターセプト

BaseBuilder は次のようにさらに拡張されています。

  • SyncBuilder - SyncGraphQlInterceptor の チェーン で実行スタックをブロックしています。

  • Builder - GraphQlInterceptor の チェーン を使用したノンブロッキング実行スタック。

要求

GraphQlClient を取得すると、取得メソッドまたは実行メソッドを介してリクエストの実行を開始できます。

取得

以下は、クエリのデータを取得してデコードします。

  • 同期化

  • ノンブロッキング

String document = "{" +
		"  project(slug:\"spring-framework\") {" +
		"	name" +
		"	releases {" +
		"	  version" +
		"	}"+
		"  }" +
		"}";

Project project = graphQlClient.document(document) (1)
		.retrieveSync("project") (2)
		.toEntity(Project.class); (3)
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 は、レスポンスとフィールドへのアクセスを提供します。

  • 同期化

  • ノンブロッキング

try {
	Project project = graphQlClient.document(document)
			.retrieveSync("project")
			.toEntity(Project.class);
}
catch (FieldAccessException ex) {
	ClientGraphQlResponse response = ex.getResponse();
	// ...
	ClientResponseField field = ex.getField();
	// ...
}
Mono<Project> projectMono = graphQlClient.document(document)
		.retrieve("project")
		.toEntity(Project.class)
		.onErrorResume(FieldAccessException.class, ex -> {
			ClientGraphQlResponse response = ex.getResponse();
			// ...
			ClientResponseField field = ex.getField();
			// ...
		});

実行

取得は、レスポンスマップ内の単一のパスからデコードするためのショートカットにすぎません。さらに制御するには、execute メソッドを使用してレスポンスを処理します。

例:

  • 同期化

  • ノンブロッキング

ClientGraphQlResponse response = graphQlClient.document(document).executeSync();

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)
	}
}

Project project = field.toEntity(Project.class); (4)
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 レスポンスにはデータがなく、エラーのみがあります
2null で、関連するエラーがあるフィールド
3DataFetcher によって null に設定されたフィールド
4 指定されたパスでデータをデコードします

ドキュメントソース

リクエストのドキュメントは String であり、ローカル変数または定数で定義されるか、コード生成されたリクエストオブジェクトによって生成されます。

クラスパス上の "graphql-documents/" に、拡張子 .graphql または .gql のドキュメントファイルを作成し、ファイル名で参照することもできます。

例: src/main/resources/graphql-documents 内の projectReleases.graphql というファイルが与えられた場合、内容は次のとおりです。

src/main/resources/graphql-documents/projectReleases.graphql
query projectReleases($slug: ID!) {
	project(slug: $slug) {
		name
		releases {
			version
		}
	}
}

その後、次のことができます。

Project project = graphQlClient.documentName("projectReleases") (1)
		.variable("slug", "spring-framework") (2)
		.retrieveSync()
		.toEntity(Project.class);
1 "projectReleases.graphql" からドキュメントを読み込みます
2 変数値を提供します。

このアプローチは、クエリのフラグメントをロードする場合にも機能します。フラグメントは、リクエストドキュメント内の繰り返しを回避する再利用可能なフィールド選択セットです。例: 複数のクエリで …​releases フラグメントを使用できます。

src/main/resources/graphql-documents/projectReleases.graphql
query frameworkReleases {
	project(slug: "spring-framework") {
		name
		...releases
	}
}
query graphqlReleases {
       project(slug: "spring-graphql") {
           name
           ...releases
       }
   }

このフラグメントは、再利用のために別のファイルで定義できます。

src/main/resources/graphql-documents/ リリース .graphql
fragment releases on Project {
   	releases {
           version
       }
   }

次に、このフラグメントをクエリドキュメントとともに送信できます。

Project project = graphQlClient.documentName("projectReleases") (1)
		.fragmentName("releases") (2)
		.retrieveSync()
		.toEntity(Project.class);
1 "projectReleases.graphql" からドキュメントを読み込みます
2"releases.graphql" からフラグメントをロードし、ドキュメントに追加します

IntelliJ の "JS GraphQL" プラグインは、コード補完 で GraphQL クエリファイルをサポートします。

GraphQlClient  ビルダーを使用して、名前でドキュメントをロードするために DocumentSource をカスタマイズできます。

サブスクリプションリクエスト

サブスクリプションリクエストには、データをストリーミングできるクライアントトランスポートが必要です。これをサポートする GraphQlClient を作成する必要があります。

取得

サブスクリプションストリームを開始するには、retrieveSubscription を使用します。これは、単一のレスポンスを取得するのに似ていますが、それぞれが何らかのデータにデコードされたレスポンスのストリームを返します。

Flux<String> greetingFlux = client.document("subscription { greetings }")
		.retrieveSubscription("greeting")
		.toEntity(String.class);

サブスクリプションがサーバー側から「エラー」メッセージで終了した場合、Flux は SubscriptionErrorException で終了する可能性があります。この例外により、「エラー」メッセージからデコードされた GraphQL エラーへのアクセスが提供されます。

基礎となる接続が閉じられるか失われた場合、Flux は WebSocketDisconnectedException などの GraphQlTransportException で終了することがあります。その場合、retry オペレーターを使用してサブスクリプションを再開できます。

クライアント側からサブスクリプションを終了するには、Flux をキャンセルする必要があり、次に WebSocket トランスポートが「完了」メッセージをサーバーに送信します。Flux の解約方法は使い方によって異なります。take や timeout などの一部のオペレーターは、それ自体で Flux をキャンセルします。Subscriber とともに Flux をサブスクライブしている場合は、Subscription への参照を取得し、それを通じてキャンセルできます。onSubscribe オペレーターは、Subscription へのアクセスも提供します。

実行

取得は、各レスポンスマップ内の単一のパスからデコードするためのショートカットにすぎません。さらに制御するには、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)
		});

インターセプト

GraphQlClient.SyncBuilder で作成されたトランスポートをブロックするには、クライアント経由のすべてのリクエストをインターセプトする SyncGraphQlClientInterceptor を作成します。

static class MyInterceptor implements SyncGraphQlClientInterceptor {

	@Override
	public ClientGraphQlResponse intercept(ClientGraphQlRequest request, Chain chain) {
		// ...
		return chain.next(request);
	}
}

GraphQlClient.Builder で作成されたノンブロッキングトランスポートの場合は、クライアント経由のすべてのリクエストをインターセプトする 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();

DGS コードジェネ

ミューテーション、クエリ、サブスクリプションなどの操作をテキストとして提供する代わりに、DGS コードジェネ [GitHub] (英語) ライブラリを使用してクライアント API クラスを生成し、流れるような API を使用してリクエストを定義できます。

Spring for GraphQL は、GraphQlClient をラップし、生成されたクライアント API クラスを使用してリクエストを準備するのに役立つ DgsGraphQlClient を提供します。

例: 次のスキーマがある場合:

type Query {
    books: [Book]
}

type Book {
    id: ID
    name: String
}

リクエストは次のように実行できます。

HttpGraphQlClient client = ... ;
DgsGraphQlClient dgsClient = DgsGraphQlClient.create(client); (1)

List<Book> books = dgsClient.request(new BooksGraphQLQuery()) (2)
		.projection(new BooksProjectionRoot<>().id().name()) (3)
		.retrieveSync()
		.toEntityList(Book.class);
1- 任意の GraphQlClient をラップして DgsGraphQlClient を作成します。
2 ・リクエストに対する動作を指定します。
3- 選択セットを定義します。