10
3

More than 3 years have passed since last update.

大容量データを高速取得する Bulk Operations(バルク・オペレーション)

Last updated at Posted at 2020-12-05

Shopifyで大容量データを取るのは、かつて悩みの種だった

Shopifyで大容量のデータ(数万件の注文、数十万の顧客)を取得したいと思い、その方法や要する時間の長さに悩んだことはないでしょうか?
ERP連携、物流システム連携(出荷指示など)、DMP/CDP連携などで考えうるシナリオです。

ShopifyはAPI中心主義のマルチテナントSaaSですで、SQLでDBを叩くことはできません。
いままでSQLでECシステムのデータベースをクエリしてきた方は煩わしいと思うこともあるでしょう。

2019年10月に「Bulk Operations」が新登場

そこで、2019年10月バージョン(2019-10)よりGraphQL Admin APIにBulk Operations(バルク・オペレーション)という新しい仕組みが搭載されました。

この新しい仕組みを使うことで、1回にリクエストできるAPI制限(GraphQLでは1,000ポイント、REST APIでは250オブジェクトまでなど)を考慮しながら、APIで示される次のページを順に読み込んでいく方法に比べ圧倒的に早くデータが取得できます。

Bulk Operationsの仕組み

bulkOperationRunQuery というGraphQL mutationを実行したら(=APIリクエストをしたら)ポーリングをしておき、結果が出次第そのデータ(JSONL形式)をダウンロードできるURLが返されます。
あとはそのURLから得たデータをパースし、必要な形状にデータを変換して、データの渡し先に渡すだけです。

制限

現在このmutationに科されている制限は1Shopifyストアあたり、1つのBulk Operationしか走らせられないということです。したがって、必要以上にいろいろなアプリで使っていると、お互いの処理を保留させてしまう可能性があるため、本当に必要な大容量データ出力が想定されるシナリオだけに実装すると良いでしょう。

実際の使い方

リクエスト

具体的なbulkOperationRunQueryのクエリ部分は下記の通りです。見ていただくと、クエリ部分は本当に普通のGraphQLクエリであることが分かります。
数点仕様が違うところとしては、first引数(first:50のような指定)やcursorpageInfoはBulk Operationsでは無視されるということです。後でBulk Operationsじゃなくする予定もある場合はそのまま残しておいて良いでしょう。
そうでない場合は無駄な文字列になるので消しておいた方が良いですね。

{
    orders(reverse: #{reverse_order}, query: "#{query}") {
    edges {
        node {
        id
        name
        email
        phone
        displayFinancialStatus
        processedAt
        customerAcceptsMarketing
        currencyCode
        tags
        fulfillable
        closed
        totalRefundedSet{
            presentmentMoney{
            amount
            }
        }
        subtotalPriceSet{
            presentmentMoney{
            amount
            }
        }
        cancelledAt
        paymentGatewayNames
        riskLevel
        totalDiscountsSet{
            presentmentMoney{
            amount
            }
        }
        totalTaxSet{
            presentmentMoney{
            amount
            }
            shopMoney {
            amount
            }
        }
        note
        totalPriceSet{
            presentmentMoney{
            amount
            }
            shopMoney {
            amount
            }
        }
        discountCode
        createdAt
        publication {
            name
        }
        lineItems{
            edges {
            node {
                title
                variantTitle
                fulfillableQuantity
                quantity
                originalTotalSet{
                presentmentMoney{
                    amount
                }
                }
                originalUnitPriceSet{
                presentmentMoney{
                    amount
                }
                }
                discountedUnitPriceSet {
                presentmentMoney{
                    amount
                }
                }
                sku
                requiresShipping
                taxable
                fulfillmentStatus
                vendor
                totalDiscountSet{
                presentmentMoney{
                    amount
                }
                }
                customAttributes{
                key
                value
                }
                variant {
                compareAtPrice
                barcode
                }
                taxLines {
                priceSet {
                    presentmentMoney{
                    amount
                    }
                }
                }
            }
            }
        }
        }
    }
    }
}

あとは、上記クエリを bulkOperationRunQuery mutationに入れてあげるだけですね。
トリプルクオート(""")はGraphQLでの複数行文字列を指しており、その中にクエリを入れる形となります。

mutation {
  bulkOperationRunQuery(
    query:"""
    {
      orders(reverse: #{reverse_order}, query: "#{query}") {
        edges {
          node {
            省略
          }
        }
      }
    }
    """
  ) {
    bulkOperation {
      id
      status
    }
    userErrors {
      field
      message
    }
  }
}

Bulk Operationsが普通のクエリと大きく異なるのが、結果がすぐに返ってくるわけではなく、同じリクエストセッションでずっと待っているわけでもなく、上記のmutationの結果返されたoperation IDをゲットして一旦そのリクエストは終了させ、あとはそのIDでポーリングをする必要があるということです。

mutationのレスポンスは、このような形状のJSONです。

{
  "data": {
    "bulkOperationRunQuery": {
      "bulkOperation": {
        "id": "gid:\/\/shopify\/BulkOperation\/1",
        "status": "CREATED"
      },
      "userErrors": []
    }
  },
  ...
}

ポーリング

ポーリングには、先ほど取得したoperation IDを使って、 currentBulkOperation を使いましょう。
補足ですが、今回使うIDは上のレスポンスのid項目の中にある1だけではなく、gid:\/\/shopify\/BulkOperation\/1自体がIDです。GraphQLの世界ではIDがこういう形式になっています。

{
  node(id: "gid://shopify/BulkOperation/1") {
    ... on BulkOperation {
      id
      status
      errorCode
      createdAt
      completedAt
      objectCount
      fileSize
      url
      partialDataUrl
    }
  }
}

このポーリングを10秒に1回なり、30秒に1回なり、statusCOMPLETEDになるまでするということです。
あり得るステータスは下記の通りです。

ステータス名 説明
CANCELED オペレーションがキャンセルされた
CANCELING オペレーションをキャンセル中
COMPLETED オペレーションが完了した
CREATED オペレーションが作成された(ばかり)
EXPIRED オペレーションの有効期限が切れた
FAILED オペレーションが失敗した
RUNNING オペレーションは実行中(データ取得中)

よりシンプルに、IDの指定もいらないcurrentBulkOperation というポーリング手段もありますが、こちらはクエリしているストア・クエリ元アプリで作られた最新のBulk Operation情報を返すため、複数のBulk Operationが走りうる状況である場合、結果が整合しない可能性があるため注意してください。

また、上のクエリの中にあるobjectCountは、ポーリングした時点で取得できているレコード件数がわかるため、進捗の確認に使えます。

データの取得

クエリのstatusCOMPLETEDになると、urlのなかにデータをダウンロードできるURLが含まれます。
このURLには認証のレイヤーがあり、また1週間後には完全に有効期限が切れます。

このデータはJSONL(JSON Lines)形式で返され、1行に1JSONオブジェクトが入っている形になっています。
あとはこれをパースして、活用しましょう!

Bulk Operationsが実際に使われているアプリ

出荷指示用ShopifyアプリJapan Order CSVでは実際にBulk Operationsを使って、大容量データの出力に対応しています。
COMPLETE PLAN(月額$29.99、30日間無料トライアル)で、2,500レコードまでの出力ができます。5,000件を出力しても問題ないことは検証済で、今後上限値をあげる計画もあります。

Bulk Operationsの動きを見たい場合は、データを用意して、Japan Order CSVで試せます。

以上、ありがとうございました!

10
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
3