🛍️

Ruby:Shopify GraphQL Admin API の使い方

13 min read

Ruby で Shopify GraphQL Admin API を使う方法についてまとめます。

GraphQL API を使うべき理由

公式ドキュメントの Shopify Admin API のページに

The Admin API is available through GraphQL and REST.

と記載されている通り、

Shopify Admin API 自体は、GraphQL API・REST API のどちらでも利用できます。

しかし Shopify としては、GraphQL APIの利用が推奨されているので、今後のアップデートを考えると、GraphQL API の方を使っておくのがベターです。

gem shopify_api でもそのことが明記されています:

The best way to consume the Shopify API is through GraphQL, which enables high volume mutations, bulk operations, and access to all new features.

なので、今後は REST API でなく、GraphQL API を使っていきましょう。

REST API との違い

GraphQL と REST の違いをまとめると次の表の通りです。

項目 GraphQL REST
利用方法 HTTP HTTP
エンドポイント 1つ 複数
1リクエストで取得可能なリソース 複数 1つ
必要以上のデータ取得 起こらない 起こる

GraphQLはエンドポイントが単一で、Client 側のニーズに応じてデータを取得できるので、API リクエストの効率が良いです。

用語の整理

GraphQL で出てくる用語をざっくり整理します。これが分かっていると、後続の説明がすんなり入ってきます。

参考: In GraphQL what's the meaning of “edges” and “node”?

用語 説明
connection オブジェクトデータとメタデータがまとまったもの(edges や pageInfo などで構成される)
field オブジェクトが持っているデータ構造
pageInfo hasNextPage, hasPreviousPage, startCursor, endCursor などのページ情報
edge オブジェクトの配列データ(node、cursor で構成される)
node オブジェクトデータ(product、productVariant などのデータ)
cursor edge のポジションを表す Base64 エンコード文字列(ページネーションで利用される)

Query Root

すべてのGraphQLのクエリは、QueryRoot に記載の Connections もしくは Fields のいずれかの項目をトップレベルとして書き始めます。

この表、パッと見、分かりにくいですが(僕だけかもしれませんが)、各項目ごとに tr の色がストライプ装飾されています。それが分かるとそこそこ見やすいです。

サンプルクエリ

各項目の詳細ページ末尾にいくと、「GraphiQL(ツール)」でサンプルリクエストを呼び出せます。これで実際にどんな値が返ってくるか確認できます。

例:Product のサンプル

リクエスト制限

コスト

GraphQL API ではリクエストするクエリの複雑さにより「コスト」が計算されます。そのコストが制限を超える場合は、エラーが返されます。

コスト計算の詳細:こちら

制限

Shopify API のリクエスト制限には a leaky bucket algorithm が使われています。

そして、GraphQL API のバケツの大きさは「1000pt」、リークレートが「50pt/sec」です。

An app is given a bucket of 1,000 cost points, with a leak rate of 50 cost points per second.

制限の詳細:こちら

例えば、products 50件と、それに紐づく productVariants 最大100件 を同時に取得しようとすると、クエリコストが 5152 > 1000 となり、一発で制限超過のエラーが出てしまいます!

制限超過する場合は、取得フィールド数やオブジェクトのネストを減らしましょう。

Ruby での導入方法

それでは本題に入り、Ruby で Shopify GraphQL Admin API を導入する方法を整理します。

gem shopify_api

REST API と同じ gem shopify_api で利用できます。

Gemfile
gem 'shopify_api'
  • gem のバージョンは 9.0.2
  • Ruby のバージョンは 2.4.10
  • Shopify API のバージョンは 2020-10

認証方法

APIの認証方法も、REST API と同じです。session をアクティベートすれば、API を利用できます。

session = ShopifyAPI::Session.new(
  domain:      domain,
  token:       token,
  api_version: API_VERSION
)

ShopifyAPI::Base.activate_session(session)

# APIコール

ShopifyAPI::Base.clear_session

設定エラー InvalidClient が出る場合

ShopifyAPI::GraphQL のスキーマファイルが生成されていないと、API呼び出し時に次のエラーが出ます。

ShopifyAPI::GraphQL::InvalidClient

以下のコマンドを叩くことでスキーマファイルが生成され、エラーが解消します。このスキーマファイルは「開発環境でのクエリの有効性チェックのため」に必要らしく、任意のストア情報で、1度生成すればOKです。

rake shopify_api:graphql:dump SHOP_DOMAIN="SHOP_NAME.myshopify.com" ACCESS_TOKEN="SHOP_TOKEN" API_VERSION=yyyy-mm

参考:How to use GraphQL schema for public apps?

It appears the dump is to "ensure your queries are valid in development", so you'll only need to generate it once - perhaps with test store credentials?

Ruby での使い方

gem shopify_api での GraphQL API の呼び出し方法を整理します。

基本

基本的な呼び出し方は次の通りです。

Call

例:Shop 名を取得

client = ShopifyAPI::GraphQL.client

SHOP_NAME_QUERY = client.parse <<-'GRAPHQL'
  {
    shop {
      name
    }
  }
GRAPHQL

result = client.query(SHOP_NAME_QUERY)

ポイント

  • client.parse で文字列クエリをGraphQLクエリにパース

Result

pry(main)> result
=> #<GraphQL::Client::Response:0x00007fa9e0ccfb00
 @data=#< shop=...>,
 @errors=#<GraphQL::Client::Errors @messages={} @details={}>,
 @extensions={"cost"=>{"requestedQueryCost"=>1, "actualQueryCost"=>1, "throttleStatus"=>{"maximumAvailable"=>1000.0, "currentlyAvailable"=>999, "restoreRate"=>50.0}}},
 @original_hash=
  {"data"=>{"shop"=>{"name"=>"MY SHOP NAME"}},
   "extensions"=>{"cost"=>{"requestedQueryCost"=>1, "actualQueryCost"=>1, "throttleStatus"=>{"maximumAvailable"=>1000.0, "currentlyAvailable"=>999, "restoreRate"=>50.0}}}}>
pry(main)> result.data.shop
=> #< name="MY SHOP NAME">

変数あり

GRAPHQL クエリの値を動的に定義したい場合は、変数 variables を持たせます。

Call

例:Products を50件取得

client = ShopifyAPI::GraphQL.client

PRODUCTS_QUERY = client.parse <<-'GRAPHQL'
  query($per_page: Int){
    products(first: $per_page) {
      edges {
        cursor
        node {
          id
        }
      }
    }
  }
GRAPHQL

variables = {
  per_page: 50
}

result = client.query(PRODUCTS_QUERY, variables: variables)

ポイント

  • 変数を query($[name]: [type]){} で定義
  • 定義した変数は、文字列クエリ内で $[name] で参照できる
  • client.query の第二引数 variables に動的な値を渡す

Result

pry(main)> result
=> #<GraphQL::Client::Response:0x00007fa9df661350
 @data=#< products=...>,
 @errors=#<GraphQL::Client::Errors @messages={} @details={}>,
 @extensions={"cost"=>{"requestedQueryCost"=>52, "actualQueryCost"=>52, "throttleStatus"=>{"maximumAvailable"=>1000.0, "currentlyAvailable"=>948, "restoreRate"=>50.0}}},
 @original_hash=
  {"data"=>
    {"products"=>
      {"edges"=>
        [{"cursor"=>"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx0", "node"=>{"id"=>"gid://shopify/Product/1000000000000"}},
         {"cursor"=>"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx1", "node"=>{"id"=>"gid://shopify/Product/1000000000001"}},

...
pry(main)> result.data.products.edges.first.node
=> #< id="gid://shopify/Product/1000000000000">

ページネーション

Shopify GraphQL API のベージネーションは、cursor-based pagination です。

Paginating results with GraphQL

When you use a connection to retrieve a list of resources, you use arguments to specify the number of results to retrieve. You can select which set of results to retrieve from a connection by using cursor-based pagination.

connections (products など)のクエリパラメータに after: [cursor] を渡すことで、指定した cursor 以降の edges データを取得することができます。

次ページの有無は、connections.pageInfo.hasNextPage で把握できます。

Call

例:指定した任意の cursor 以降の Products を2件取得

client = ShopifyAPI::GraphQL.client

PRODUCTS_QUERY = client.parse <<-'GRAPHQL'
  query($per_page: Int, $cursor: String){
    products(first: $per_page, after: $cursor) {
      pageInfo {
        hasNextPage
      }
      edges {
        cursor
        node {
          id
        }
      }
    }
  }
GRAPHQL

variables = {
  per_page: 2,
  cursor:   'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx1'
}

result = client.query(PRODUCTS_QUERY, variables: variables)

Result

pry(main)> result
=> #<GraphQL::Client::Response:0x00007fa9ddfe9b68
 @data=#< products=...>,
 @errors=#<GraphQL::Client::Errors @messages={} @details={}>,
 @extensions={"cost"=>{"requestedQueryCost"=>4, "actualQueryCost"=>4, "throttleStatus"=>{"maximumAvailable"=>1000.0, "currentlyAvailable"=>996, "restoreRate"=>50.0}}},
 @original_hash=
  {"data"=>
    {"products"=>
      {"pageInfo"=>{"hasNextPage"=>true},
       "edges"=>
        [{"cursor"=>"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx2", "node"=>{"id"=>"gid://shopify/Product/1000000000002"}},
         {"cursor"=>"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx3", "node"=>{"id"=>"gid://shopify/Product/1000000000003"}}]}},
   "extensions"=>{"cost"=>{"requestedQueryCost"=>4, "actualQueryCost"=>4, "throttleStatus"=>{"maximumAvailable"=>1000.0, "currentlyAvailable"=>996, "restoreRate"=>50.0}}}}>
pry(main)> result.data.products.page_info.has_next_page?
=> true

connections での検索(query

connections の結果を絞りたい時は、クエリパラメータに query: String を渡します。

query のシンタックス: こちら

Call

例:指定した product_id に該当する Products を1件取得

client = ShopifyAPI::GraphQL.client

PRODUCTS_QUERY = client.parse <<-'GRAPHQL'
  query($query: String){
    products(first: 1, query: $query) {
      edges {
        cursor
        node {
          id
        }
      }
    }
  }
GRAPHQL

variables = {
  query: 'id:1000000000000'
}

result = client.query(PRODUCTS_QUERY, variables: variables)

ポイント

  • レスポンスの node.id は gid (gid://shopify/[object]/[id) というフォーマットだが、検索クエリで id を指定する場合は [id] の部分のみを用いる(ID 型が要求される時は gid を使う)

Result

pry(main)> result
=> #<GraphQL::Client::Response:0x00007fa9e25f3ca8
 @data=#< products=...>,
 @errors=#<GraphQL::Client::Errors @messages={} @details={}>,
 @extensions={"cost"=>{"requestedQueryCost"=>3, "actualQueryCost"=>3, "throttleStatus"=>{"maximumAvailable"=>1000.0, "currentlyAvailable"=>997, "restoreRate"=>50.0}}},
 @original_hash=
  {"data"=>{"products"=>{"edges"=>[{"cursor"=>"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx0", "node"=>{"id"=>"gid://shopify/Product/1000000000000"}}]}},
   "extensions"=>{"cost"=>{"requestedQueryCost"=>3, "actualQueryCost"=>3, "throttleStatus"=>{"maximumAvailable"=>1000.0, "currentlyAvailable"=>997, "restoreRate"=>50.0}}}}>
pry(main)> result.data.products.edges.first.node
=> #< id="gid://shopify/Product/1000000000000">

nodes での検索(複数ID)

複数IDなど、複数値で検索する時は、nodes オブジェクトを使います。

フォーラムでの関連回答: こちら

Another option is the nodes field on QueryRoot, which takes an array of ids.

Call

例:指定した複数の ids に該当する Products を取得

client = ShopifyAPI::GraphQL.client

PRODUCTS_QUERY = client.parse <<-'GRAPHQL'
  query ($ids: [ID!]!){
    nodes(ids: $ids) {
      ... on Product {
        id
      }
    }
  }
GRAPHQL

variables = {
  ids: ["gid://shopify/Product/1000000000000", "gid://shopify/Product/1000000000001"]
}

result = client.query(PRODUCTS_QUERY, variables: variables)

ポイント

  • nodes の ID には gid (gid://shopify/[object]/[id) のフォーマットで指定する
  • ... on [Object] で取得したいオブジェクトを指定する
  • 返ってくる値に edges は含まれない

Result

pry(main)> result
=> #<GraphQL::Client::Response:0x00007f9560413690
 @data=#< nodes=...>,
 @errors=#<GraphQL::Client::Errors @messages={} @details={}>,
 @extensions=
  {"cost"=>
    {"requestedQueryCost"=>3,
     "actualQueryCost"=>3,
     "throttleStatus"=>
      {"maximumAvailable"=>1000.0, "currentlyAvailable"=>997, "restoreRate"=>50.0}}},
 @original_hash=
  {"data"=>
    {"nodes"=>
      [{"__typename"=>"Product",
        "id"=>"gid://shopify/Product/1000000000000"},
       {"__typename"=>"Product",
        "id"=>"gid://shopify/Product/1000000000001"}]
pry(main)> result.data.nodes
=> [#< __typename="Product" id="gid://shopify/Product/1000000000000" >,
 #< __typename="Product" id="gid://shopify/Product/1000000000001" >]

まとめ

  • 今後のアップデートを考えると、REST API ではなく GraphQL API の方を使っておくのがベター。
  • GraphQLのクエリは QueryRoot に記載の Connections もしくは Fields のいずれかの項目からスタートさせる。
  • リクエスト制限に引っかかる場合は、クエリフィールドやオブジェクトのネストを絞る。
  • Ruby では REST API と同じ gem shopify_api で利用できる。
  • ベージネーションの仕様は cursor-based pagination
  • 複数IDの検索は connections(query; String) ではなく nodes(ids: [ID]) を使う。

Discussion

ログインするとコメントできます