🕸️

GraphQL StitchingのType Mergingを深掘りする

2023/09/29に公開

最近GraphQL Stitchingを勉強しているのですが、Type Mergingが何度読んでもしっくりこなかったので、深掘りしようと頑張ってみました。

Type Mergingに関しては公式ドキュメントでは以下の箇所となります。

https://the-guild.dev/graphql/stitching/docs/approaches/type-merging

本記事の前提

本記事ではGraphQL GatewayやGraphQL Stitchingがどういうものなのか、ということに関しては説明しません🙏そのあたりを説明している良き記事はいろいろとありますので、ぜひそちらをまず参照ください。

参考までに公式ドキュメントは以下となります。

https://the-guild.dev/graphql/stitching

GraphQL Stitching における Type Merging

Type Merging とは

Type Mergingは端的に言うと複数のスキーマで同じ型を定義しているときに、使う側はあたかも一つの型であるかのようにマージして扱えるようにする仕組みです。

例えば、以下のような「storefronts」、「products」、そして「manufacturers」3つの異なるサービスがあったとします。

別サービスではあるものの、お互いに密接に関連しています。

  1. storefrontsサービス

    • お店そのものの情報を管理
    • さらに、そのお店が持っている商品のリストや情報もここで管理
  2. productsサービス

    • 商品そのものの詳細(商品名や価格)を管理
    • どのメーカーがどの商品を提供しているのかという情報もここで管理
  3. 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]!
}

ここまでは感覚的にもわかりやすいと思うのですが、では「どうやってマージするのか?」という部分がドキュメントを読んだだけでは微妙にわかりづらく、読んでは忘れ、読んでは忘れを繰り返していました。

ということで、公式のサンプルを基にもう少し詳しく中の動きを見てみることにしました。

https://github.com/ardatan/schema-stitching/tree/master/examples/type-merging-single-records

Merging Flow を実際に確認する

公式ドキュメントの MergingFlow を確認すると、以下の図とそれぞれのステップに関する記述があり、どのようにマージが行われているのか説明されています。

この図が微妙に省略している説明があったりするので、個人的にはパッと見では理解するのが難しいです。

これを上でも述べた Storefront, Product, Manufacturer の例でもう少し詳しく見てみたフローを図にしてみました。(全画面でみたい場合は こちら )

順番に見ていきましょう。

1. クライアントのリクエスト

まずはクライアントが投げるリクエストです。

Gatewayのスキーマは上に記載したものになっているので、これは通常のGraphQL同様で、欲しい物をリクエストしている普通のクエリです。

2. オリジナルのリクエストがstorefrontサーバーへ

次に、このリクエストがまずはstorefront向けになっているので、gatewayを経由してstorefrontサーバーに投げられます。このときにgatewayは、元々のクエリからstorefrontに関係している部分にフィルタして投げます。storefrontが理解している Product 型は、あくまで upc のみなので、 元々のクエリの namemanufacturer は省かれます。

ただし、これだけだと 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 設定です。注目するのは fieldNameargs です。これは Product 型をマージするときには自分自身(productsサーバー)の product クエリを使い、引数には 元となるオブジェクトの upc プロパティを使う、という意味です。

ちょっと分かりづらいですが、以下のような関係性です。

この情報を使って手順5のクエリが生成されます

5. リクエストがproductsサーバーへ

  1. からの情報を基に、 productsサーバーへのクエリが生成されます。具体的には以下のようになります。

args 定義のおかげで upc6 で注入されているのがわかります。

また、ここからは繰り返しになるのですが、productsサーバーがハンドリングできるクエリにのみフィルタが成されます。オリジナルのクエリは以下のように manufacturernameproducts をリクエストしていますが、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 に関しては同じ様に理解に苦しむ方がいるかと思うので、本記事が誰かの助けになったら幸いです。

SHE Tech Blog

Discussion