1. Top
  2. ブログ一覧
  3. GraphQL設計の魅力を解き明かす。Shopifyに見る使いやすさと効率性の両立

Eコマース

GraphQL設計の魅力を解き明かす。Shopifyに見る使いやすさと効率性の両立

公開日

2024.11.20

GraphQL設計の魅力を解き明かす。Shopifyに見る使いやすさと効率性の両立のサムネイル

Shopifyは、膨大な店舗数と多様な顧客ニーズに応えるため、洗練されたGraphQL APIを構築しています。そのAPI設計は、柔軟性とスケーラビリティを同時に実現するものであり、システム全体の品質向上に大きく寄与しています。本記事では、ShopifyがどのようにGraphQL設計を実現し、その成功を支える設計原則をどのように適用しているのかを掘り下げます。

本記事のコードは、GraphQL API設計の概念を説明するためのサンプルであり、Shopifyの実際のコードを引用したものではありません。Shopifyの具体的な実装については、公式ドキュメントや参考文献をご参照ください。

また、この記事はShopifyの公式設計方針を完全に反映したものではなく、GraphQL設計の一般的なパターンや、公開情報を元にした解説です。

リソースモデルを中心とした設計

GraphQLスキーマ設計の基本は、リソースモデルにあります。Shopifyでは、「Product(製品)」「Customer(顧客)」「Order(注文)」などの主要なリソースを中心に据えたスキーマを構築し、それらが直感的に操作できるように設計されています。

ShopifyのProductモデルの設計

たとえば、ShopifyのProductリソースは、以下のような設計で提供されています。

製品には複数の画像やバリエーション(例: サイズや色)が関連付けられており、それを階層化したスキーマで表現しています。以下のクエリは、製品の基本情報と関連するデータを取得するものです

query {
  product(id: "12345") {
    title
    images(first: 5) {
      edges {
        node {
          url
          altText
        }
      }
    }
    variants(first: 5) {
      edges {
        node {
          title
          price
        }
      }
    }
  }
}

このスキーマ構造により、開発者は関連データを効率的に取得でき、必要に応じて取得するフィールドの深さを制御することが可能です。

また、製品はタグやコレクションといった他のリソースと関連付けられており、これらもスキーマ内で自然に操作できる形で提供されています。開発者はリレーションシップを意識せずに、必要なデータに直接アクセスできます。

スキーマの再利用性とモジュール性

Shopifyは、スキーマのモジュール化と再利用性を促進することで、拡張時の影響を最小限に抑えています。たとえば、タグ(Tags)やメタフィールド(Metafields)など、複数のリソースで共通して使用される型は、独立したモジュールとして設計され、必要に応じて各リソースに再利用可能な形で統合されています。

type Tag {
  id: ID!
  name: String!
}

type Product {
  id: ID!
  tags: [Tag]
}

タグ関連のスキーマ変更が他のリソースに波及するリスクが減少し、保守性が向上します。

グローバルなオブジェクト識別子(Global Object Identification)の導入

GraphQLのスキーマ設計において、グローバルなオブジェクト識別子(Global Object Identification、GOI)は、各リソースを一意に識別し、システム全体の効率性と一貫性を向上させる重要な役割を果たします。Shopifyでは、リソースの種類(例: ProductやCustomer)とそのID(例: 12345)を含むグローバル ID(GID、例: gid://shopify/Product/12345)としてリソース管理を行っています。

GOIの利点の一つは、クライアント側のキャッシュ管理が効率化されることです。この識別子をキャッシュキーとして使用することで、取得済みのデータを効率的に再利用でき、他のリソースと衝突するリスクを回避できます。同じデータを別のクエリで要求した場合でも、キャッシュ済みの情報を即座に活用できるため、通信の効率が向上します。

また、サーバー側ではGOIがリソース解決(リゾルバ)の迅速化に寄与します。識別子にリソースの種類とインスタンス情報が含まれているため、サーバーは追加の処理をせずにデータを特定できます。これにより、データベースクエリの効率が上がり、大量のリクエストを効率的に処理できる設計が実現されています。

さらに、GOIはAPI全体の一貫性を保つ点でも重要です。すべてのリソースを統一された形式で識別できるため、クライアントは一貫した方法でAPIを利用できます。これにより、APIの学習コストが低下し、利用者が誤った実装をするリスクも減少します。また、エラーの発生時にはGOIを参照するだけで問題のリソースを特定できるため、デバッグやデータ追跡も容易です。

最後に、GOIは異なるシステム間の統合にも役立ちます。ShopifyのAPIを外部アプリケーションと連携する際、この識別子を利用することで、データの整合性が保たれます。GOIがあることでリソースが一意に特定され、サードパーティツールがShopifyのデータを正確に分類し、処理できるようになります。

ConnectionとEdgeパターンの活用

ShopifyのGraphQL APIでは、ConnectionとEdgeというデザインパターンを活用して、大規模データの効率的な取り扱いを実現しています。この設計は、ページネーションや関連データの取得をスムーズに行うための手法です。

Connectionの活用例

Connectionはリソースの集合を効率的に管理し、必要なデータだけを取得する柔軟性を提供します。たとえば、Shopifyでは製品に関連付けられた画像をConnectionで扱い、ページネーションを可能にしています。以下のクエリでは、製品の最初の5件の画像データを取得しています。

query {
  product(id: "12345") {
    images(first: 5) {
      edges {
        node {
          url
          altText
        }
      }
    }
  }
}

Connection構造では、edgesはリソース本体であるnodeと、それに関連するメタデータを含む形で構成されています。edges内に格納されたnodeがリソースの具体的なデータ(例: URLや説明)を提供し、edgesそのものがカーソルなどのメタ情報を保持します。この設計は、大量のリソースを扱う際に、クライアントが必要な範囲のデータのみを逐次的に取得できるようにする点で効果的です。

クライアントは必要なデータを小分けに取得できるため、一度に大量のリクエストを処理するサーバー負荷を軽減し、ネットワーク通信の効率も上がります。

Connectionはデータセットが増大しても処理を分割しながらパフォーマンスを維持できます。この仕組みが、Shopifyが膨大なデータ量を抱えつつも、高速かつ安定したデータアクセスを実現している理由の一つです。次の「Edgeの役割」では、この構造内でedgesが果たす具体的な役割について解説します。

Edgeの役割

Edgeは、先述した通り、Connection内でリソースデータ(ノード)とそのメタ情報を管理します。 Edgeに含まれる主なメタデータの例としては、ページネーション用のcursor、リスト内の順序を示すpositionやrank、関連リソースを識別するlinkedResourceIdなどがあります。また、更新履歴(例: updatedAt)、リソースの状態(例: isActiveやstatus)、アクセス制限情報(例: accessLevel)など、クライアントがリソースを操作するための追加情報も含むことができます。

メタデータをリソース本体から分離することで、リソースデータそのものを変更することなく、ページネーションや並び順などの操作を容易に実現できます。また、レスポンスサイズを抑えつつ必要な情報だけを取得できるため、クライアントとサーバー間の通信が効率化されます。

スキーマ設計における拡張性の確保

Shopifyでは、スキーマの拡張性を保ちながら、新しい機能やフィールドを追加する際の影響を最小限に抑えることを重視しています。そのため、スキーマ設計ではnullability(nullableか非nullか)の判断が重要な役割を果たします。この判断は、APIが提供するデータの本質やビジネスロジックに基づいて行われます。

たとえば、製品(Product)に「環境負荷評価(Environmental Impact)」という新しい属性を追加するケースを考えてみます。このフィールドは以下のようにnullableとして設計されます。

type Product {
  id: ID!
  title: String!
  environmentalImpact: EnvironmentalImpact
}

type EnvironmentalImpact {
  score: Int
  description: String
}

ここで重要なのは、なぜenvironmentalImpactがnullableとして設計されるかという点です。製品の種類によっては環境負荷評価が存在しない場合があります。このようなデータが存在し得ない状態を表すためには、空のオブジェクトやデフォルト値ではなくnullが適切です。ビジネスロジック的にデータが存在しない状態を明確に示すことで、API利用者に対して直感的で意味のあるデータ提供が可能になります。

Shopifyが開発者向けに公開しているTutorial: Designing a GraphQL APIでは、rules(あるECサイトのコレクション(商品グループ)に関連するルールを表現するリスト型のフィールド)のようにリスト型のフィールドでは、空リストとnullが異なる意味を持つ場合が多いため、リスト型を非nullで設計する手法が紹介されています。リストが空の場合でも、リスト自体は存在するという意味を明確にすることで、利用者は余計なnullチェックを行う必要がなくなります。たとえば、製品のタグリストが空であれば「タグがない」という状況を表し、nullの場合には「データが不完全または取得できない」という意味を持たせることができます。

さらに、Shopifyでは、関連性の高い複数のフィールドを1つのサブオブジェクトにまとめることで、スキーマの明確さと拡張性を高めています。たとえば、Tutorial: Designing a GraphQL APIの例だと、特定の条件に従ってデータを分類するためのルールを記述するフィールドと、そのルールの適用方法を制御するフィールドを1つのオブジェクトに統合しています。この設計により、データ分類の条件が不要なケースではそのサブオブジェクト全体を無効化でき、余計なチェックやエラーの発生を防ぎます。一方、条件が必要な場合にはそのサブオブジェクトが有効になり、データ分類のルールと適用方法を一貫した形で提供できます。さらに、この設計により、新しい条件や設定項目を追加してもスキーマ全体の整合性を保つことが可能です。この手法は、複雑な条件を扱う場面でもAPI利用者にとって直感的で、柔軟性のある構造を提供します。

拡張性を維持するために、Shopifyの設計エンジニアはnullを単なる「値がない」状態として扱うのではなく、ビジネスロジックを反映する重要なツールと見なしていると言えます。これにより、API利用者がスキーマの構造を直感的に理解できるようにすると同時に、将来のスキーマ進化においても後方互換性を維持しやすい設計を実現しています。結果として、nullabilityに基づく慎重な設計が、ShopifyのAPIが柔軟で信頼性の高いものとなる鍵となっています。

まとめ: Shopifyのアプローチがもたらすシステム品質への影響

ShopifyのGraphQL設計は、具体的な設計思想に基づいてシステム全体の品質を向上させています。例えば、ConnectionとEdgeパターンの活用により、大量のデータを効率的に取り扱う方針を採用しています。また、nullabilityやサブオブジェクトの適切な利用によって、スキーマの拡張性を保ちながら、後方互換性を維持していることも紹介しました。

これらの設計は、クライアントが必要なデータだけを取得しやすくする柔軟なクエリ設計や、サーバー負荷を軽減する効率的な通信にもつながっています。結果として、Shopifyは開発者体験を向上させつつ、堅牢で継続的に拡張可能なAPIを提供しています。

GraphQLの特性を活用しながら、Shopifyの設計エンジニアは現実のビジネス要件に即した実用的なAPIを構築しています。このアプローチから学ぶことで、他のシステムでも品質の高いAPI設計が実現できるかもしれません。

参考情報

著者:ROUTE06 Research Team
#Shopify