このバージョンはまだ開発中であり、まだ安定しているとは見なされていません。最新の安定バージョンについては、Spring GraphQL 1.4.1 を使用してください!

クライアント

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 = RestClient.create("https://spring.io/graphql");
HttpSyncGraphQlClient graphQlClient = HttpSyncGraphQlClient.create(restClient);

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

RestClient restClient = RestClient.create("https://spring.io/graphql");
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 = WebClient.create("https://spring.io/graphql");
HttpGraphQlClient graphQlClient = HttpGraphQlClient.create(webClient);

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

WebClient webClient = WebClient.create("https://spring.io/graphql");

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://spring.io/graphql";
WebSocketClient client = new ReactorNettyWebSocketClient();

WebSocketGraphQlClient graphQlClient = WebSocketGraphQlClient.builder(url, client).build();

HttpGraphQlClient とは対照的に、WebSocketGraphQlClient は接続指向です。つまり、リクエストを行う前に接続を確立する必要があります。リクエストを開始すると、接続が透過的に確立されます。または、クライアントの start() メソッドを使用して、リクエストの前に明示的に接続を確立します。

接続指向であることに加えて、WebSocketGraphQlClient は多重化もされています。すべてのリクエストに対して単一の共有接続を維持します。接続が失われた場合は、次のリクエストで、または start() が再度呼び出された場合に再確立されます。進行中のリクエストをキャンセルし、接続を閉じ、新しいリクエストを拒否するクライアントの stop() メソッドを使用することもできます。

そのサーバーへのすべてのリクエストに対して単一の共有接続を確立するには、サーバーごとに単一の WebSocketGraphQlClient インスタンスを使用します。各クライアントインスタンスは独自の接続を確立しますが、これは通常、単一のサーバーを対象とするものではありません。

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

String url = "wss://spring.io/graphql";
WebSocketClient client = new ReactorNettyWebSocketClient();

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 メッセージの送信をサポートしています。これを有効にするには、次の手順を実行します。

String url = "wss://spring.io/graphql";
WebSocketClient client = new ReactorNettyWebSocketClient();

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(uri);

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> project = 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);
	return project;
}
catch (FieldAccessException ex) {
	ClientGraphQlResponse response = ex.getResponse();
	// ...
	ClientResponseField field = ex.getField();
	// return fallback value
	return new Project();
}
Mono<Project> projectMono = graphQlClient.document(document)
		.retrieve("project")
		.toEntity(Project.class)
		.onErrorResume(FieldAccessException.class, (ex) -> {
			ClientGraphQlResponse response = ex.getResponse();
			// ...
			ClientResponseField field = ex.getField();
			// return fallback value
			return Mono.just(new Project());
		});

If the field is present but cannot be decoded to the requested type, a plain GraphQlClientException is thrown instead.

実行

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

例:

  • 同期化

  • ノンブロッキング

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

if (!response.isValid()) {
	// Request failure... (1)
}

ClientResponseField field = response.field("project");
if (field.getValue() == null) {
	if (field.getErrors().isEmpty()) {
		// Optional field set to null... (2)
	}
	else {
		// Field failure... (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.getValue() == null) {
				if (field.getErrors().isEmpty()) {
					// Optional field set to null... (2)
				}
				else {
					// Field failure... (3)
				}
			}

			return field.toEntity(Project.class); (4)
		});
1 レスポンスにはデータがなく、エラーのみがあります
2DataFetcher によって null に設定されたフィールド
3null で、関連するエラーがあるフィールド
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("projectReleases.project")
		.toEntity(Project.class);
1 "projectReleases.graphql" からドキュメントをロードします
2 変数値を提供します。

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.getValue() == null) {
				if (field.getErrors().isEmpty()) {
					// Optional field set to null...
				}
				else {
					// Field failure...
				}
			}

			return field.toEntity(String.class);
		});

インターセプト

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

import org.springframework.graphql.client.ClientGraphQlRequest;
import org.springframework.graphql.client.ClientGraphQlResponse;
import org.springframework.graphql.client.SyncGraphQlClientInterceptor;

public class SyncInterceptor implements SyncGraphQlClientInterceptor {

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

GraphQlClient.Builder で作成されたノンブロッキングトランスポートの場合は、クライアント経由のすべてのリクエストをインターセプトする GraphQlClientInterceptor を作成します。

import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import org.springframework.graphql.client.ClientGraphQlRequest;
import org.springframework.graphql.client.ClientGraphQlResponse;
import org.springframework.graphql.client.GraphQlClientInterceptor;

public 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 = URI.create("wss://localhost:8080/graphql");
WebSocketClient client = new ReactorNettyWebSocketClient();

WebSocketGraphQlClient graphQlClient = WebSocketGraphQlClient.builder(url, client)
		.interceptor(new MyInterceptor())
		.build();

オプション入力

GraphQL の入力型はデフォルトで null 許容かつオプションです。入力値(またはそのいずれかのフィールド)は、null リテラルに設定するか、全く指定しないかを選択できます。この区別は、基になるデータが null に設定されるか、それに応じて全く変更されない可能性がある、ミューテーションによる部分的な更新に役立ちます。

ArgumentValue<T> support in controllers と同様に、Input 型を ArgumentValue<T> でラップしたり、クライアント側のクラス属性レベルで使用したりできます。次のような ProjectInput クラスがあるとします。

import org.springframework.graphql.data.ArgumentValue;

public record ProjectInput(String id, ArgumentValue<String> name) {

}

クライアントを使用してミューテーションリクエストを送信できます。

public void updateProject() {
	ProjectInput projectInput = new ProjectInput("spring-graphql",
			ArgumentValue.ofNullable("Spring for GraphQL")); (1)
	ClientGraphQlResponse response = this.graphQlClient.document("""
					mutation updateProject($project: ProjectInput!) {
						  updateProject($project: $project) {
							id
							name
						  }
					}
					""")
			.variables(Map.of("project", projectInput))
			.executeSync();
}
1 このフィールドを無視するために、代わりに ArgumentValue.omitted() を使用することができます。

これを動作させるには、クライアントは JSON の(デ)シリアライゼーションに Jackson を使用し、org.springframework.graphql.client.json.GraphQlJacksonModule を設定する必要があります。これは、基盤となる HTTP クライアントに手動で登録できます。

public ArgumentValueClient(HttpGraphQlClient graphQlClient) {
	JsonMapper jsonMapper = JsonMapper.builder().addModule(new GraphQlJacksonModule()).build();
	JacksonJsonEncoder jsonEncoder = new JacksonJsonEncoder(jsonMapper);
	WebClient webClient = WebClient.builder()
			.baseUrl("https://example.com/graphql")
			.codecs((codecs) -> codecs.defaultCodecs().jacksonJsonEncoder(jsonEncoder))
			.build();
	this.graphQlClient = HttpGraphQlClient.create(webClient);
}

この GraphQlJacksonModule は、Bean として提供することで、Spring Boot アプリケーションにグローバルに登録できます。

@Configuration
public class GraphQlJsonConfiguration {

	@Bean
	public GraphQlJacksonModule graphQLModule() {
		return new GraphQlJacksonModule();
	}

}
Jackson 2.x サポートは GraphQlJackson2Module でも利用できます。

DGS コードジェネ

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

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

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

type Query {
    books: [Book]
}

type Book {
    id: ID
    name: String
}

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

HttpGraphQlClient client = HttpGraphQlClient.create(WebClient.create("https://example.org/graphql"));
DgsGraphQlClient dgsClient = DgsGraphQlClient.create(client); (1)

List<Book> books = dgsClient.request(BookByIdGraphQLQuery.newRequest().id("42").build()) (2)
		.projection(new BooksProjectionRoot<>().id().name()) (3)
		.retrieveSync("books")
		.toEntityList(Book.class);
1Create DgsGraphQlClient by wrapping any GraphQlClient.
2Specify the operation for the request.
3Define the selection set.

The DgsGraphQlClient also supports multiple queries by chaining query() calls:

HttpGraphQlClient client = HttpGraphQlClient.create(WebClient.create("https://example.org/graphql"));
DgsGraphQlClient dgsClient = DgsGraphQlClient.create(client); (1)

ClientGraphQlResponse response = dgsClient
		.request(BookByIdGraphQLQuery.newRequest().id("42").build()) (2)
		.queryAlias("firstBook")  (3)
		.projection(new BooksProjectionRoot<>().id().name())
		.request(BookByIdGraphQLQuery.newRequest().id("53").build()) (4)
		.queryAlias("secondBook")
		.projection(new BooksProjectionRoot<>().id().name())
		.executeSync(); (5)

Book firstBook = response.field("firstBook").toEntity(Book.class); (6)
Book secondBook = response.field("secondBook").toEntity(Book.class);
1Create DgsGraphQlClient by wrapping any GraphQlClient.
2Specify the operation for the first request.
3When multiple requests are sent, we need to specify an alias for each
4Specify the operation for the second request.
5Get the complete response
6Get the relevant document parts with the configured aliases