データ統合

Spring for GraphQL を使用すると、既存の Spring テクノロジを活用し、一般的なプログラミングモデルに従って、GraphQL を介して基になるデータソースを公開できます。

このセクションでは、@GraphQlRepository でマークされたリポジトリの自動検出および GraphQL クエリ登録のオプションを含む、Querydsl または Query by Example リポジトリを DataFetcher に適応させる簡単な方法を提供する Spring Data の統合レイヤーについて説明します。

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

// For paginated queries
DataFetcher<Iterable<Account>> dataFetcher =
		QuerydslDataFetcher.builder(repository).scrollable();

上記の DataFetcher を RuntimeWiringConfigurer で登録できるようになりました。

DataFetcher は、GraphQL 引数から Querydsl Predicate を構築し、それを使用してデータをフェッチします。Spring Data は、JPA、MongoDB、Neo4j、LDAP の QuerydslPredicateExecutor をサポートします。

GraphQL 入力型である単一の引数の場合、QuerydslDataFetcher は 1 レベル下にネストし、引数のサブマップの値を使用します。

リポジトリが ReactiveQuerydslPredicateExecutor の場合、ビルダーは DataFetcher<Mono<Account>> または DataFetcher<Flux<Account>> を返します。Spring Data は、MongoDB および Neo4j のこのバリアントをサポートしています。

ビルドのセットアップ

ビルドで 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 を使用します。

カスタム

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

自動登録

リポジトリに @GraphQlRepository のアノテーションが付けられている場合、DataFetcher がまだ登録されておらず、戻り値の型がリポジトリのドメイン型と一致するクエリに対して自動的に登録されます。これには、単一値クエリ、複数値クエリ、ページ分割されたクエリが含まれます。

デフォルトでは、クエリによって返される GraphQL 型の名前は、リポジトリドメイン型の単純な名前と一致する必要があります。必要に応じて、@GraphQlRepository の typeName 属性を使用して、ターゲットの GraphQL 型名を指定できます。

ページ分割されたクエリの場合、リポジトリドメイン型の単純名は、末尾 Connection を除いた Connection 型名と一致する必要があります (例: Book は BooksConnection と一致します)。自動登録の場合、ページ分割はオフセットベースで、1 ページあたり 20 項目になります。

自動登録は、特定のリポジトリが QuerydslBinderCustomizer を実装しているかどうかを検出し、QuerydslDataFetcher ビルダーメソッドを介して透過的に適用します。

自動登録は、QuerydslDataFetcher から取得できる組み込みの RuntimeWiringConfigurer を介して実行されます。Boot スターターは @GraphQlRepository Bean を自動的に検出し、使用して RuntimeWiringConfigurer を初期化します。

リポジトリがそれぞれ QuerydslBuilderCustomizer または ReactiveQuerydslBuilderCustomizer を実装している場合、自動登録はリポジトリインスタンスで customize(Builder) を呼び出すことによってカスタマイズを適用します。

例示による問い合わせ

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

// For paginated queries
DataFetcher<Iterable<Account>> dataFetcher =
		QueryByExampleDataFetcher.builder(repository).scrollable();

上記の DataFetcher を RuntimeWiringConfigurer で登録できるようになりました。

DataFetcher は、GraphQL 引数マップを使用してリポジトリのドメイン型を作成し、それをサンプルオブジェクトとして使用してデータをフェッチします。Spring Data は、JPA、MongoDB、Neo4j、Redis の QueryByExampleDataFetcher をサポートします。

GraphQL 入力型である単一の引数の場合、QueryByExampleDataFetcher は 1 レベル下にネストし、引数のサブマップの値とバインドします。

リポジトリが ReactiveQueryByExampleExecutor の場合、ビルダーは DataFetcher<Mono<Account>> または DataFetcher<Flux<Account>> を返します。Spring Data は、MongoDB、Neo4j、Redis、R2dbc のこのバリアントをサポートしています。

ビルドのセットアップ

例示による問い合わせは、それがサポートされているデータストアの Spring Data モジュールにすでに含まれているため、有効にするために追加のセットアップは必要ありません。

カスタム

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

自動登録

リポジトリに @GraphQlRepository のアノテーションが付けられている場合、DataFetcher がまだ登録されておらず、戻り値の型がリポジトリのドメイン型と一致するクエリに対して自動的に登録されます。これには、単一値クエリ、複数値クエリ、ページ分割されたクエリが含まれます。

デフォルトでは、クエリによって返される GraphQL 型の名前は、リポジトリドメイン型の単純な名前と一致する必要があります。必要に応じて、@GraphQlRepository の typeName 属性を使用して、ターゲットの GraphQL 型名を指定できます。

ページ分割されたクエリの場合、リポジトリドメイン型の単純名は、末尾 Connection を除いた Connection 型名と一致する必要があります (例: Book は BooksConnection と一致します)。自動登録の場合、ページ分割はオフセットベースで、1 ページあたり 20 項目になります。

自動登録は、QueryByExampleDataFetcher から取得できる組み込みの RuntimeWiringConfigurer を介して実行されます。Boot スターターは @GraphQlRepository Bean を自動的に検出し、使用して RuntimeWiringConfigurer を初期化します。

リポジトリがそれぞれ QueryByExampleBuilderCustomizer または ReactiveQueryByExampleBuilderCustomizer を実装している場合、自動登録はリポジトリインスタンスで customize(Builder) を呼び出すことによってカスタマイズを適用します。

選択セットと射影

発生する一般的な質問は、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 種類あり、どちらも基になるデータソースからどのプロパティを読み込むかを決定します。

DTO 射影は、コンストラクターまたは getter メソッドのいずれかに変換コードを配置できるため、より高いレベルのカスタマイズを提供します。

DTO 射影は、個々のプロパティが射影自体によって決定されるクエリから具体化されます。DTO 射影は、通常、完全な引数のコンストラクター (Java レコードなど) で使用されるため、必要なすべてのフィールド (または列) がデータベースクエリ結果の一部である場合にのみ構築できます。

スクロール

ページネーションで説明したように、GraphQL カーソル接続仕様は ConnectionEdgePageInfo スキーマ型によるページネーションのメカニズムを定義しますが、GraphQL Java は同等の Java 型表現を提供します。

Spring for GraphQL は、Spring Data ページネーション型 Window および Slice を透過的に適応させるための組み込み ConnectionAdapter 実装を提供します。次のように設定できます。

CursorStrategy<ScrollPosition> strategy = CursorStrategy.withEncoder(
		new ScrollPositionCursorStrategy(),
		CursorEncoder.base64()); (1)

GraphQLTypeVisitor visitor = ConnectionFieldTypeVisitor.create(List.of(
		new WindowConnectionAdapter(strategy),
		new SliceConnectionAdapter(strategy))); (2)

GraphQlSource.schemaResourceBuilder()
		.schemaResources(..)
		.typeDefinitionConfigurer(..)
		.typeVisitors(List.of(visitor)); (3)
1ScrollPosition を Base64 エンコードされたカーソルに変換する戦略を作成します。
2DataFetcher から返される Window および Slice を適応させるための型 ビジターを作成します。
3 型の訪問者を登録します。

リクエスト側では、コントローラーメソッドで ScrollSubrange メソッド引数を宣言して、前方または後方にページネーションできます。これが機能するには、CursorStrategy が ScrollPosition を Bean としてサポートすることを宣言する必要があります。

Boot スターターは CursorStrategy<ScrollPosition> Bean を宣言し、Spring Data がクラスパス上にある場合は上記のように ConnectionFieldTypeVisitor を登録します。

キーセットの位置

KeysetScrollPosition の場合、カーソルはキーセットから作成する必要があります。キーセットは基本的にキーと値のペアの Map です。キーセットからカーソルを作成する方法を決定するには、ScrollPositionCursorStrategy を CursorStrategy<Map<String, Object>> で構成します。デフォルトでは、JsonKeysetCursorStrategy はキーセット Map を JSON に書き込みます。これは、String、Boolean、Integer、Double などの単純な型には機能しますが、その他の型は、ターゲットの型情報がないと同じ型に戻すことができません。Jackson ライブラリには、JSON に型情報を含めることができるデフォルトの型指定機能があります。安全に使用するには、許可される型のリストを指定する必要があります。例:

PolymorphicTypeValidator validator = BasicPolymorphicTypeValidator.builder()
		.allowIfBaseType(Map.class)
		.allowIfSubType(ZonedDateTime.class)
		.build();

ObjectMapper mapper = new ObjectMapper();
mapper.activateDefaultTyping(validator, ObjectMapper.DefaultTyping.NON_FINAL);

その後、JsonKeysetCursorStrategy を作成できます。

ObjectMapper mapper = ... ;

CodecConfigurer configurer = ServerCodecConfigurer.create();
configurer.defaultCodecs().jackson2JsonDecoder(new Jackson2JsonDecoder(mapper));
configurer.defaultCodecs().jackson2JsonEncoder(new Jackson2JsonEncoder(mapper));

JsonKeysetCursorStrategy strategy = new JsonKeysetCursorStrategy(configurer);

デフォルトでは、JsonKeysetCursorStrategy が CodecConfigurer なしで作成され、Jackson ライブラリがクラスパス上にある場合、上記のようなカスタマイズが DateCalendarjava.time の任意の型に適用されます。

ソート

Spring for GraphQL は、GraphQL 引数から Sort を作成するための SortStrategy を定義します。AbstractSortStrategy は、ソート方向とプロパティを抽出するための抽象メソッドを使用して契約を実装します。コントローラーメソッドの引数として Sort のサポートを有効にするには、SortStrategy Bean を宣言する必要があります。