1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-08-12

今日は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

下記のヘッダープロパティーを解読します。ヘッダー内のプロパティは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の抽出

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


  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#です。

全体(とりあえず)

まず全体のコードは下記です。


 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;

        }

      
    }

メソッドの定義

まずはこちらから


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

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

オブジェクトの準備

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


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は受用のオブジェクトで下記のように定義しています。


  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へリクエストしている部分です。


  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 (ヘッダー)

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


 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の文字列最後に追加します。


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の値を付随しています。




 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/

1
2
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
1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?