GraphQL StitchingのType Mergingを深掘りする
最近GraphQL Stitchingを勉強しているのですが、Type Mergingが何度読んでもしっくりこなかったので、深掘りしようと頑張ってみました。
Type Mergingに関しては公式ドキュメントでは以下の箇所となります。
本記事の前提
本記事ではGraphQL GatewayやGraphQL Stitchingがどういうものなのか、ということに関しては説明しません🙏そのあたりを説明している良き記事はいろいろとありますので、ぜひそちらをまず参照ください。
参考までに公式ドキュメントは以下となります。
GraphQL Stitching における Type Merging
Type Merging とは
Type Mergingは端的に言うと複数のスキーマで同じ型を定義しているときに、使う側はあたかも一つの型であるかのようにマージして扱えるようにする仕組みです。
例えば、以下のような「storefronts」、「products」、そして「manufacturers」3つの異なるサービスがあったとします。
別サービスではあるものの、お互いに密接に関連しています。
-
storefrontsサービス:
- お店そのものの情報を管理
- さらに、そのお店が持っている商品のリストや情報もここで管理
-
productsサービス:
- 商品そのものの詳細(商品名や価格)を管理
- どのメーカーがどの商品を提供しているのかという情報もここで管理
-
manufacturersサービス:
- メーカーに関する情報、例えば会社の名前を管理
要するに、storefrontsは「どの店が何の商品を持っているか」、productsは「商品の詳細やそのメーカーは誰か」、manufacturersは「メーカーの基本情報」をそれぞれ取り扱っているとします。
これらのサービスが下図のようにGraphQL Gatewayの背後にあったとします。
サーバーは分かれているのですが、それぞれで Product
だったり Manufacturer
の型が定義されているのがわかります。これらをマージして、Gatewayでは以下のようなSchemaを提供できます。
type Query {
manufacturer(id: ID!): Manufacturer
product(upc: ID!): Product
_manufacturer(id: ID!): Manufacturer
storefront(id: ID!): Storefront
}
type Manufacturer {
id: ID!
name: String! # manufacturersのスキーマから
products: [Product]! # productsのスキーマから
}
type Product { # 全体的にproductsのスキーマから
upc: ID!
name: String!
price: Float!
manufacturer: Manufacturer
}
type Storefront { # storefrontsのスキーマから
id: ID!
name: String!
products: [Product]!
}
ここまでは感覚的にもわかりやすいと思うのですが、では「どうやってマージするのか?」という部分がドキュメントを読んだだけでは微妙にわかりづらく、読んでは忘れ、読んでは忘れを繰り返していました。
ということで、公式のサンプルを基にもう少し詳しく中の動きを見てみることにしました。
Merging Flow を実際に確認する
公式ドキュメントの MergingFlow を確認すると、以下の図とそれぞれのステップに関する記述があり、どのようにマージが行われているのか説明されています。
この図が微妙に省略している説明があったりするので、個人的にはパッと見では理解するのが難しいです。
これを上でも述べた Storefront
, Product
, Manufacturer
の例でもう少し詳しく見てみたフローを図にしてみました。(全画面でみたい場合は こちら )
順番に見ていきましょう。
1. クライアントのリクエスト
まずはクライアントが投げるリクエストです。
Gatewayのスキーマは上に記載したものになっているので、これは通常のGraphQL同様で、欲しい物をリクエストしている普通のクエリです。
2. オリジナルのリクエストがstorefrontサーバーへ
次に、このリクエストがまずはstorefront向けになっているので、gatewayを経由してstorefrontサーバーに投げられます。このときにgatewayは、元々のクエリからstorefrontに関係している部分にフィルタして投げます。storefrontが理解している Product
型は、あくまで upc
のみなので、 元々のクエリの name
や manufacturer
は省かれます。
ただし、これだけだと products
で取得したいものが無くなってしまい、他のサーバーから取得したい情報が取れなくなってしまいます。そこで登場するのが selectionSet
です。 selectionSet
は公式ドキュメントを引用すると以下のとおりです。
selectionSet specifies one or more key fields required from other services to perform this query. Query planning will automatically resolve these fields from other subschemas in dependency order.
要は、「○○型の情報をとるためには、△△の情報が最低限必要だよ」という定義があるということです。今回の場合であれば、 productsサーバーに以下のような設定があります。
merge: {
Product: {
// This service provides _all_ unique fields for the `Product` type.
// Again, there's unique data here so the gateway needs a query configured to fetch it.
// This config delegates to `product(upc: $upc)`.
selectionSet: '{ upc }',
fieldName: 'product',
args: ({ upc }) => ({ upc }),
},
},
selectionSet
に { upc }
があるのが確認できます。これは「Product型の情報をマージするときには upc
が必要だよ」と宣言しています。この情報があることで、 gatewayがstorefrontサーバーにリクエストを投げるときに暗黙的に upc
を追加してくれます。実際のクエリのログを確認すると以下の通りで、 upc
があるのが確認できます。
3. storefront のレスポンス
先のリクエストから、storefrontのレスポンスは以下になります。
4. merger query の作成
storefrontのレスポンスが来たので、次はproductsサーバーへリクエストをします。マージを担ってくれる人(以下 merger
)がクエリを投げてくれるのですが、そのときに merge
の設定が活用されます。
上記がgatewayに設定しているproductsサーバー用の merge
設定です。注目するのは fieldName
と args
です。これは Product
型をマージするときには自分自身(productsサーバー)の product
クエリを使い、引数には 元となるオブジェクトの upc
プロパティを使う、という意味です。
ちょっと分かりづらいですが、以下のような関係性です。
この情報を使って手順5のクエリが生成されます
5. リクエストがproductsサーバーへ
- からの情報を基に、 productsサーバーへのクエリが生成されます。具体的には以下のようになります。
args
定義のおかげで upc
が 6
で注入されているのがわかります。
また、ここからは繰り返しになるのですが、productsサーバーがハンドリングできるクエリにのみフィルタが成されます。オリジナルのクエリは以下のように manufacturer
の name
や products
をリクエストしていますが、productsサーバーは name
プロパティを把握していません。
よって、 name
は省略されます。
そして、 Manufacturer型
にはmanufacturerサーバーの selectionSet
として以下のように { id }
が設定されています
merge: {
// This schema provides one unique field of data for the `Manufacturer` type (`name`).
// The gateway needs a query configured so it can fetch this data...
// this config delegates to `manufacturer(id: $id)`.
Manufacturer: {
selectionSet: '{ id }',
fieldName: 'manufacturer',
args: ({ id }) => ({ id }),
},
},
これが暗黙的にクエリに注入されることが以下の実際のリクエストから確認できます。
6. products のレスポンス
先のリクエストから、productsのレスポンスは以下になります。
7,8,9 同様にmanufacturerサーバーにもリクエストが投げられる
繰り返しになるのでmanufacturerサーバーへのリクエストの説明は割愛します。以下のとおりです。(全画面で見たい方は こちら )
10,11 すべての結果が然るべき型にマージされて返される
そして最終的にはすべての結果がマージされて、クライアントがリクエストした結果が返されるという流れです。
まとめ
公式ドキュメントよりもう少し詳細な動きを確かめてみたところ、だいぶType Mergingが理解できた気がします。特に selectionSet
がいまいちよくわからなかったものが、実際のリクエストを確かめることで暗黙的に注入されている動きを理解できました。
GraphQL Gatewayに関するほんの一部について紹介しただけですが、 Type Merging に関しては同じ様に理解に苦しむ方がいるかと思うので、本記事が誰かの助けになったら幸いです。
「SHElikes(シーライクス)」を運営するSHEの開発チームがお送りするテックブログです。私たちは社会的不均衡の解決を目指すインパクトスタートアップです。【エンジニア積極採用中】カジュアル面談、副業からのトライアル etc 承っております💪 採用情報 -> bit.ly/3XxywnD
Discussion