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

## 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）形式で返され、１行に1JSONオブジェクトが入っている形になっています。
あとはこれをパースして、活用しましょう！


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

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

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


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