[Shopify] API (REST) ページネーション

今日はShopifyのページネーションに関してまとめます。　コードのは解説は`ASP.Net Core 3.1` `C#`になります。

# ページネーション？
ページネーションとは訳の通りページ送りです。例えば、APIを介してなにかをリクエストした際に100万行のデータが返ってきても処理しきれない、レスポンスを返すShopifyのサーバーからしても負荷が大きすぎるため、分散（ページにわけて）レスポンス（依頼に対しての返信）をします。

たとえば、Shopifyにある`customersavedsearch`内にいる顧客一覧を取得する際に、10万人の顧客情報がある、なんてこともあります。

`customersavedsearch`の詳細はこちら
https://shopify.dev/docs/admin-api/rest/reference/customers/customersavedsearch#other-2020-07

そして、10万人いるのですが、デフォルトで50人、最大で250人づつしか返してくれません。なので、ページ送りをする必要があります。

# ページ送りの仕組み（ざっくり）

Shopifyにリクエスト（依頼）を送ると、レスポンス（返信）のヘッダー部分に次のページを示す値が付帯されています。その次ページの値を読み取って、次のリクエストを送るという仕組みです。　


じゃ、`loop`すればいいな、と思うかもしれませんが、このリクエストをループで処理すると、ShopifyのApi上限を超えることもありますので注意が必要です。RESTAPIの場合1秒間に2リクエストを上限としていますので、数秒間隔でリクエストを送るように制御することをお勧めします。

上限に関しては下記を参照ください。
https://shopify.dev/concepts/about-apis/rate-limits

#リクエストプロパティ (Shopify Api Serviceにリクエスト)

Shopify APIにリクエストを送信する際に指定できるパラメターは下記です。

例: 1ページに3レコード返してほしい時

```
https://{yourshop}.myshopify.com/admin/api/2020-07/customer_saved_searches/{customer_saved_search_id}/customers.json?limit=3
```

上記は初期リクエスト時に1ページにいくつのレコードを返してほしいか指定するときに使います。

次のページを参照したいというときは`page_info`も指定する必要があります。`page_info`の値はAPIレスポンスに含まれるヘッダー情報を読み取る必要があります。（それは後ほど）

リクエスト時に指定できるページネーションに関するプロパティは下記です。

|プロパティ|説明|
|:--|:--|
|`page_info`|ページを指定するID。APIレスポンスに含まれるページIDをここで指定します。|
|`limit`|返してほしいレコード数を指定|
|`fields`|返してほしいプロパティを指定（サポートされているAPIは限定されています）|

# レスポンスプロパティ (Shopify Api Serviceからのレスポンス)

postmanのリクエストとレスポンスは下記です
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241909/b489566f-4a4a-54d7-826c-19707e881287.png)

下記のヘッダープロパティーを解読します。ヘッダー内のプロパティは`Link`です。次のページがない時は`Link`プロパティー自体がありません。

```
<https://{yourshop}.myshopify.com/admin/api/2020-07/customer_saved_searches/{customer_saved_search_id}/customers.json?limit=3&page_info=eyJxdWVyeSI6ImNvdW50cnk6XCJKYXBhblwiIiwibGFzdF9pZCI6MzcwODgxOTg5ODUyNywibGFzdF92YWx1ZSI6MTU5NjUwNDY1NzAwMCwiZGlyZWN0aW9uIjoibmV4dCJ9>; rel="next"
```

上記の値から page_info= 部分を読み取ります。

# page_infoの抽出

下記のコードで抽出しています。

```csharp

  public string ExtractPageInfo(string url)
        {
            // return string that is page_info=
            var pageInfo = "";

            // strip down <>
            url = url.Replace("<", "");
            url = url.Replace(">", "");

            var key = "page_info=";

            Match match = Regex.Match(url, key, RegexOptions.IgnoreCase);

            if (match.Success)
            {
              
                var length = key.Length;

                pageInfo = url.Substring(match.Index + length, url.Length - (match.Index + length));
            }

            return pageInfo;
        }
```

# 解説

実際のコードで解説したいと思います。　下記の環境はASP.NET Core 3.1 C#です。

## 全体（とりあえず）
まず全体のコードは下記です。

```csharp

 public async Task<ShopifyCustomerSavedSearcheCustomers> GetListOfUsersEmailInShopifyCustomerSavedSearcheAsync(string storeUrl, 
            string nameOfCustomerSavedSearches, string pageInfo)
        {
            // read 250 at time

            var endPoint = apiVersion + "/customer_saved_searches/" + nameOfCustomerSavedSearches + "/customers.json";

            var store = _organizationHandlers.GetOrganization(storeUrl);

            var rawStoreLog = new Dictionary<string, string>();
            rawStoreLog.Add("Store", JsonConvert.SerializeObject(store));

            _telemetry.TrackEvent("customer_saved_searches.json - store", rawStoreLog);

            var ShopifyCustomerSavedSearcheCustomers = new ShopifyCustomerSavedSearcheCustomers();
          
            try
            {
                using (var httpClient = new HttpClient())
                {
                    httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
                    httpClient.DefaultRequestHeaders.Add("X-Shopify-Access-Token", store.AccessToken);

                    var requestUrl = "https://" + storeUrl + endPoint + "?limit=250&fields=email,first_name,last_name";

                    if(!string.IsNullOrEmpty(pageInfo))
                    {
                        requestUrl += "&page_info=" + pageInfo;
                    }

                    using (var response = await httpClient.GetAsync(requestUrl))
                    {
                        string apiResponse = await response.Content.ReadAsStringAsync();
                        
                        // Try and get link property of the header
                        var apiHeaderCollectionLinkKeyValuePairs = from l in response.Headers where l.Key == "Link" select l;
                        var apiHeaderCollectionLinkKeyValuePairString = "";

                        if (apiHeaderCollectionLinkKeyValuePairs.Any())
                        {
                            // Insert page_info (if any)
                            apiHeaderCollectionLinkKeyValuePairString = apiHeaderCollectionLinkKeyValuePairs.FirstOrDefault().Value.First();
                        }

                        var rawLog = new Dictionary<string, string>();
                        rawLog.Add("ApiResponse", apiResponse);

                        _telemetry.TrackEvent("customer_saved_searches.json", rawLog);

                        ShopifyCustomerSavedSearcheCustomers = JsonConvert.DeserializeObject<ShopifyCustomerSavedSearcheCustomers>(apiResponse);

                        // Append page_info
                        ShopifyCustomerSavedSearcheCustomers.link = apiHeaderCollectionLinkKeyValuePairString;
                    }
                }

                return ShopifyCustomerSavedSearcheCustomers;
            }
            catch (Exception e)
            {
                var log = new Dictionary<string, string>();
                log.Add("Message", e.Message);

                _telemetry.TrackEvent("GetShopifyCustomerSavedSearchesAsync", log);
            }

            return ShopifyCustomerSavedSearcheCustomers;

        }

      
    }
```

## メソッドの定義

まずはこちらから

```csharp

public async Task<ShopifyCustomerSavedSearcheCustomers> GetListOfUsersEmailInShopifyCustomerSavedSearcheAsync(string storeUrl, 
            string nameOfCustomerSavedSearches, string pageInfo)
```

こちらのメセッドの入力値に pageInfoを設けています。ここでの想定はpageInfo自体をこのメソッドに渡しています。注意点としては、このメソッド自体でページネーションの処理は行っていません。あくまでの独立性を担保するためにページネーションを実際に行うメソッドはもうひとつ上のレイヤーにおいています。

## オブジェクトの準備
次の部分でこれから行う処理に使われるオブジェクトを作っておきます。

```csharp

var endPoint = apiVersion + "/customer_saved_searches/" + nameOfCustomerSavedSearches + "/customers.json";

            var store = _organizationHandlers.GetOrganization(storeUrl);

            var rawStoreLog = new Dictionary<string, string>();
            rawStoreLog.Add("Store", JsonConvert.SerializeObject(store));

            _telemetry.TrackEvent("customer_saved_searches.json - store", rawStoreLog);

            var ShopifyCustomerSavedSearcheCustomers = new ShopifyCustomerSavedSearcheCustomers();
```

ここでは、DBに保存しているマーチャント情報の取得と、エラー検知・デバグ用のApplication Insightへログを記載しています。

## レスポンス受ける用のクラスを準備

ShopifyCustomerSavedSearcheCustomersは受用のオブジェクトで下記のように定義しています。

```csharp

  public class ShopifyCustomerSavedSearcheCustomers
    {
        public List<ShopifyCustomerSavedSearcheCustomer> customers { get; set; }
        public string link { get; set; }
    }

    public class ShopifyCustomerSavedSearcheCustomer
    {
        public string email { get; set; }
        public string first_name { get; set; }
        public string last_name { get; set; }
    }
```

## Shoify API Request

次から実際にShopifyへリクエストしている部分です。

```csharp

  try
            {
                using (var httpClient = new HttpClient())
                {
                    httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
                    httpClient.DefaultRequestHeaders.Add("X-Shopify-Access-Token", store.AccessToken);

                    var requestUrl = "https://" + storeUrl + endPoint + "?limit=250&fields=email,first_name,last_name";

                    if(!string.IsNullOrEmpty(pageInfo))
                    {
                        requestUrl += "&page_info=" + pageInfo;
                    }

                    using (var response = await httpClient.GetAsync(requestUrl))
                    {
                        string apiResponse = await response.Content.ReadAsStringAsync();
                        
                        // Try and get link property of the header
                        var apiHeaderCollectionLinkKeyValuePairs = from l in response.Headers where l.Key == "Link" select l;
                        var apiHeaderCollectionLinkKeyValuePairString = "";

                        if (apiHeaderCollectionLinkKeyValuePairs.Any())
                        {
                            // Insert page_info (if any)
                            apiHeaderCollectionLinkKeyValuePairString = apiHeaderCollectionLinkKeyValuePairs.FirstOrDefault().Value.First();
                        }

                        var rawLog = new Dictionary<string, string>();
                        rawLog.Add("ApiResponse", apiResponse);

                        _telemetry.TrackEvent("customer_saved_searches.json", rawLog);

                        ShopifyCustomerSavedSearcheCustomers = JsonConvert.DeserializeObject<ShopifyCustomerSavedSearcheCustomers>(apiResponse);

                        // Append page_info
                        ShopifyCustomerSavedSearcheCustomers.link = apiHeaderCollectionLinkKeyValuePairString;
                    }
                }

                return ShopifyCustomerSavedSearcheCustomers;
            }
            catch (Exception e)
            {
                var log = new Dictionary<string, string>();
                log.Add("Message", e.Message);

                _telemetry.TrackEvent("GetShopifyCustomerSavedSearchesAsync", log);
            }
```

## Shoify API Request (ヘッダー）

上記のコードをさらに詳しく見てみます。

```csharp

 using (var httpClient = new HttpClient())
                {
                    httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
                    httpClient.DefaultRequestHeaders.Add("X-Shopify-Access-Token", store.AccessToken);

                    var requestUrl = "https://" + storeUrl + endPoint + "?limit=250&fields=email,first_name,last_name";

                    if(!string.IsNullOrEmpty(pageInfo))
                    {
                        requestUrl += "&page_info=" + pageInfo;
                    }

                    using (var response = await httpClient.GetAsync(requestUrl))
                    {
                        string apiResponse = await response.Content.ReadAsStringAsync();
                        
                        // Try and get link property of the header
                        var apiHeaderCollectionLinkKeyValuePairs = from l in response.Headers where l.Key == "Link" select l;
                        var apiHeaderCollectionLinkKeyValuePairString = "";

                        if (apiHeaderCollectionLinkKeyValuePairs.Any())
                        {
                            // Insert page_info (if any)
                            apiHeaderCollectionLinkKeyValuePairString = apiHeaderCollectionLinkKeyValuePairs.FirstOrDefault().Value.First();
                        }

                        var rawLog = new Dictionary<string, string>();
                        rawLog.Add("ApiResponse", apiResponse);

                        _telemetry.TrackEvent("customer_saved_searches.json", rawLog);

                        ShopifyCustomerSavedSearcheCustomers = JsonConvert.DeserializeObject<ShopifyCustomerSavedSearcheCustomers>(apiResponse);

                        // Append page_info
                        ShopifyCustomerSavedSearcheCustomers.link = apiHeaderCollectionLinkKeyValuePairString;
                    }
                }

                return ShopifyCustomerSavedSearcheCustomers;
            }
```


ここではヘッダー部分にAccessTokenを指定し、リクエスト用のUrl文字列を生成しています。もしpageInfoが空白またはnullでなければ、リクエスト用URLの文字列最後に追加します。

```csharp

httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
                    httpClient.DefaultRequestHeaders.Add("X-Shopify-Access-Token", store.AccessToken);

                    var requestUrl = "https://" + storeUrl + endPoint + "?limit=250&fields=email,first_name,last_name";

 if(!string.IsNullOrEmpty(pageInfo))
                    {
                        requestUrl += "&page_info=" + pageInfo;
                    }

```

## レスポンスの処理

次は実際のリクエスト行い、帰ってきた値に対して処理を行います。今回はヘッダー部分にあるLinkというプロパティーも読み込んでいます。このLinkは次のページがなければレスポンスの値に含まれませんので `linq`構文で検索を行い結果にあるか確認をします `.Any()`部分です。

そのあとはデバグ用にレスポンスの文字列そのものをapplication insightに記録し、事前に定義したオンジェクトに対してjsonをマッピングしています。そして最後のlinkの値を付随しています。

```csharp



 using (var response = await httpClient.GetAsync(requestUrl))
                    {
                        string apiResponse = await response.Content.ReadAsStringAsync();
                        
                        // Try and get link property of the header
                        var apiHeaderCollectionLinkKeyValuePairs = from l in response.Headers where l.Key == "Link" select l;
                        var apiHeaderCollectionLinkKeyValuePairString = "";

                        if (apiHeaderCollectionLinkKeyValuePairs.Any())
                        {
                            // Insert page_info (if any)
                            apiHeaderCollectionLinkKeyValuePairString = apiHeaderCollectionLinkKeyValuePairs.FirstOrDefault().Value.First();
                        }

                        var rawLog = new Dictionary<string, string>();
                        rawLog.Add("ApiResponse", apiResponse);

                        _telemetry.TrackEvent("customer_saved_searches.json", rawLog);

                        ShopifyCustomerSavedSearcheCustomers = JsonConvert.DeserializeObject<ShopifyCustomerSavedSearcheCustomers>(apiResponse);

                        // Append page_info
                        ShopifyCustomerSavedSearcheCustomers.link = apiHeaderCollectionLinkKeyValuePairString;
```

このようにレスポンスのヘッダー値を確認してページネーションを行います。

本日は以上です。


# 募集

トランスコスモス技術研究所では引き続きShopifyに関わる開発人員やプロジェクトマネージャーを募集しています。こちらをご参照ください。
http://t-rnd.com/Jobs/