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
のような指定)やcursor
、pageInfo
は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回なり、status
がCOMPLETED
になるまでするということです。
あり得るステータスは下記の通りです。
ステータス名 | 説明 |
---|---|
CANCELED | オペレーションがキャンセルされた |
CANCELING | オペレーションをキャンセル中 |
COMPLETED | オペレーションが完了した |
CREATED | オペレーションが作成された(ばかり) |
EXPIRED | オペレーションの有効期限が切れた |
FAILED | オペレーションが失敗した |
RUNNING | オペレーションは実行中(データ取得中) |
よりシンプルに、IDの指定もいらないcurrentBulkOperation
というポーリング手段もありますが、こちらはクエリしているストア・クエリ元アプリで作られた最新のBulk Operation情報を返すため、複数のBulk Operationが走りうる状況である場合、結果が整合しない可能性があるため注意してください。
また、上のクエリの中にあるobjectCount
は、ポーリングした時点で取得できているレコード件数がわかるため、進捗の確認に使えます。
データの取得
クエリのstatus
がCOMPLETED
になると、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で試せます。
以上、ありがとうございました!