カーソルAPIを使ってレコードを一括取得したい（シンプル風味のソースを添えて）

こんにちは、[tarimo](https://twitter.com/tarimo34)です。
[kintone Advent Calendar 2020](https://qiita.com/advent-calendar/2020/kintone)本日12/8の担当です！
久しぶりにQiitaに投稿するので、Markdownのやり方を一部忘れてしまい想定以上に時間がかかっております・・・😅

#そもそもカーソルAPIってなんだ？
さて、皆さんはカーソルAPIお使いでしょうか？
実はリリースされたのは2019年7月。大量なデータを一括で取得するためのAPIですが、私の周りでは当時もあまり注目度は高くなかったように思えます。

ですが、予期せずして、これまでの通常のREST APIでのレコード一括取得について制限がかかるアナウンスがありました。
[kintone API レコード一括取得APIのoffsetの上限値制限について](https://cs.cybozu.co.jp/2019/006924.html)

>レコード一括取得APIでoffsetに大きな値を指定した場合、サーバーに非常に高い負荷がかかることが確認されています。
これを回避するため、レコード一括取得APIのoffsetの上限値を10,000までに制限するとともに、大量データの一括取得を行っても負荷の低い（現行のレコード一括取得APIと比較して）API「cursor.json」を新しく提供することにしました。

要は「データを10,000件超える一括取得」は負荷が高いので控えてくださいね。というお話です。至ってごもっともな話です。

#「警告が出るだけでしょ？ウチは大丈夫（意訳：API書き換えめんどくさい）」

そして再度2020年4月に記事に追記されます。

[kintone API レコード一括取得APIのoffsetの上限値制限について](https://cs.cybozu.co.jp/2019/006924.html)

>2020年7月の定期メンテナンス以降、レコード一括取得APIでoffset上限値10,000を超える処理を行った場合、そのリクエストは処理されなくなります。
ただし、2020年7月の定期メンテナンス以前からご利用いただいているお客様については、kintone管理者および該当のアプリ管理者の画面上に「上限値を超過した旨」が警告表示され、リクエストは処理される仕様となっています。
※警告が表示されたお客様は上記「3. 本件仕様変更にかかる対応方法」をご確認の上、対応のご検討をお願いします。

このあたりから 
<strong>「どうしようか」「書き換えめんどくさいな」「誰かやってくんない？」 </strong>
など私の周りがざわつき始めました。。😅　何なら、7月以降に<strong>「なんかエラー出たよ？」</strong>など連絡をいただく始末。。。OH...😓

#そもそもカーソルAPIの仕組みってどんなの？

わかりやすいように説明用の図解してみました。↓

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/76660/019cf8a3-3085-6b64-04ea-d5a49ff5b5ed.png" width ="80%">

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/76660/01a8e2f2-321a-5cd7-427e-ea5f08fa3d6c.png" width ="80%">

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/76660/2f72140b-8d8f-bd73-c9ce-fb09b25e611c.png" width ="80%">

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/76660/5c5e9da3-cba8-6f86-0a43-23db6a9ccc9c.png" width ="80%">

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/76660/43bbe934-1edf-1ddb-4db4-5f027f572725.png" width ="80%">


#カーソルAPIに載せ替えも結構だけどちょっと待って！！
より安全に大量なデータを取得できるのがカーソルAPIですが、<strong>一寸お待ちを。</strong>
カーソルAPIで取得しようとしているそのデータ、<strong>「ホントに必要でしょうか？？」</strong>載せ替えの前に今一度考えてみたほうがいいかもしれません。

また、[レコードIDを利用して取得する方法](https://developer.cybozu.io/hc/ja/articles/360031191791#use_id)もあります。

#　仕組みが理解できた上で、カーソルAPIの載せ替えを試みます。ただし・・・。

そこで<strong>「可読性の高いソースの提供をお願いしたい！」</strong>　というオーダーを頂きました。
申し遅れていましたが、ワタクシ社内のkintone管理以外にも、結構な数お客様のkintoneのカスタマイズや管理者の方へのサポートをしております。（むしろ近年はそちらがメイン業務）
<strong>「まあ自分はわかるからちゃちゃっと・・・」</strong>ではなく、弊社お客様向けにはなぜ動くかの動作原理の説明込みでわかりやすいコーディングサンプル提示をする必要があります。

#公式のコーディング例はアカンのか？（そんなことはない）

[レコード一括取得の JavaScript コーディング例：カーソル API を利用する方法](https://developer.cybozu.io/hc/ja/articles/360031191791#use_cursor)

```javascript

/*
 * get all records function by cursor id sample program
 * Copyright (c) 2019 Cybozu
 *
 * Licensed under the MIT License
 */

// カーソルを作成する
var postCursor = function(_params) {
  var MAX_READ_LIMIT = 500;

  var params = _params || {};
  var app = params.app || kintone.app.getId();
  var filterCond = params.filterCond;
  var sortConds = params.sortConds;
  var fields = params.fields;

  var conditions = [];
  if (filterCond) {
    conditions.push(filterCond);
  }

  var sortCondsAndLimit =
    (sortConds && sortConds.length > 0 ? ' order by ' + sortConds.join(', ') : '');
  var query = conditions.join(' and ') + sortCondsAndLimit;
  var body = {
    app: app,
    query: query,
    size: MAX_READ_LIMIT
  };
  if (fields && fields.length > 0) {
    body.fields = fields;
  }

  return kintone.api(kintone.api.url('/k/v1/records/cursor', true), 'POST', body).then(function(r) {
    return r.id;
  });
};

// 作成したカーソルからレコードを取得する
var getRecordsByCursorId = function(_params) {
  var params = _params || {};
  var id = params.id;

  var data = params.data;

  if (!data) {
    data = {
      records: []
    };
  }

  var body = {
    id: id
  };
  return kintone.api(kintone.api.url('/k/v1/records/cursor', true), 'GET', body).then(function(r) {
    data.records = data.records.concat(r.records);
    if (r.next) {
      return getRecordsByCursorId({ id: id, data: data });
    }
    return data;
  });
};

/*
 * @param {Object} params
 *   - app {String}: アプリID（省略時は表示中アプリ）
 *   - filterCond {String}: 絞り込み条件
 *   - sortConds {Array}: ソート条件の配列
 *   - fields {Array}: 取得対象フィールドの配列
 * @return {Object} response
 *   - records {Array}: 取得レコードの配列
 */
var getRecords = function(_params) {
  return postCursor(_params).then(function(id) {
    return getRecordsByCursorId({ id: id });
  });
};
```

リンク先からベタっと引用してみましたが、わかりにくいことはないソースだと思います。
ただ、お客様先のコーディングスタイルとはちょっと違いました。

- 関数の再帰呼び出しはあまり使われていない

なるほど、取ってつけたように一部分が違うコーディングスタイルになると気持ち悪いですもんね、そこは合わせましょう。

- カーソルを開いて、データ取得のあたりがネストが深くなりそう

こともないかと思われますが、ソースの可読性を増すため、await/async でネストが深くならないようフラットに実装してみましょう。

#早速サンプルアプリ作成開始！

こんな感じで30,000件ほどのデータが格納されているサンプルアプリを作成。
app.record.index.showの一覧表示あたりで30,000件のデータをカーソルAPIから取得するサンプルを作ってみました。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/76660/da0c7459-518e-cb79-c663-4fa3e5937290.png" width ="60%">

app.record.index.showでカーソルAPIからデータ取得してコンソールに表示します。

```javascript

(function() {

    "use strict";

    kintone.events.on('app.record.index.show', async function(event) {

      try{//カーソルAPIを使用してデータを全件取得する。

        let readCount = 0;//カーソルAPIから何度呼び出したか
        let cursorId = await createCursor();//新規カーソルを作成＆得られたカーソルID
        let totalData = {records: []};//カーソルAPIで取得される全件レコード  
        let isReading = true;//カーソルAPIからのデータ取得中であるかを示す真偽値
        console.log("■■■■■■　処理開始　■■■■■■");
        while (isReading == true){//作成したカーソルより、500件づつデータを取得＆結合
          let retValue = await getRecordsByCursorId(cursorId);
          totalData.records = totalData.records.concat(retValue.records);//取得したデータを結合
          readCount=readCount+1;//カーソルAPIからデータを取得したのでカウントアップ
          console.log("データ取得【"+readCount+"】回目");
          if(retValue.next==false){
            isReading = false;
          }
          if(readCount>=10000){/*念の為の防波堤。。デバック中に暴発（無限ループ）しないように！*/
            console.log("■■　処理中断　■■");
            isReading = false;
            break;
          }
        }
        console.log("■■■■■■　処理完了　■■■■■■");
        console.log("取得したデータは？？");
        console.dir(totalData.records);

      }
      catch{

        /**
        * データ取得中にエラーが発生し取り込みが中断された場合、明示的にカーソルを削除する必要がある。
        * ※カーソル経由で全てのレコードを取得すると、当該カーソルは自動的に削除される。
        */
        await deleteCursor(cursorId);

      }
      finally{

        /**
        * ここにカーソルAPIからのデータ取得後処理が入る。
        */
       console.log("処理おわり！！");
        return event;      

      }
    });

    /***************************************
    * 概要：カーソルからデータを取得する
    * cursorId {String}: カーソル
    * return {Allay}:
    *          - records : カーソルから取得した一部レコード
    *          - next    : 次のカーソルで取得するデータが存在するか
    ***************************************/
    async function getRecordsByCursorId(cursorId) {

      let body = {id: cursorId};
      return kintone.api(kintone.api.url('/k/v1/records/cursor', true), 'GET', body).then(async function(resp) {
        return {records:resp.records,next:resp.next}//取得したデータおよび、次のカーソル取得データ(next)が存在するか？を返却
      });

    };

    /***************************************
    * 概要：カーソルを作成する
    * return cursorId{String}: 新たに取得されたカーソル。取得失敗の場合は値はセットされない。
    ***************************************/
    async function createCursor() {

      const getPerSize = 500;/* 500件づつ取得する */
      let body = {
        'app': kintone.app.getId(),
        'fields': ['レコード番号', '作成者', '作成日時'],
        'query': 'order by レコード番号 asc',
        'size': getPerSize
      };

      return kintone.api(kintone.api.url('/k/v1/records/cursor', true), 'POST', body).then(async function(resp) {
        // success
        console.log("■■　カーソル作成　■■");
        console.log("cursorIdは"+resp.id);
        return resp.id;
      }, function(error) {
        // error
        console.log(error);
        return;
      });

    }

    /***************************************
    * 概要：カーソルを削除する
    * cursorId {String}: カーソル
    * return {boolean}: true 成功 / false失敗
    ***************************************/
    async function deleteCursor(cursorId){

      let body = {'id': cursorId};

      return kintone.api(kintone.api.url('/k/v1/records/cursor', true), 'DELETE', body).then(async function(resp) {
        // success
        console.log(resp);
        return true;
      }, function(error) {
        // error
        console.log(error.message);
        return false;
      });
    }

  })();
```

ある程度の実装ができました。動作させてみましょう！

#動作結果

カーソルを開いてから、500件づつ取得している感じ！
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/76660/1b8679d5-0951-2639-7512-a328e08718d8.png" width ="60%">

データも格納されてました！
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/76660/ccc6bc8b-89e9-f736-1c6d-f49c6761927e.png" width ="60%">

#まとめ

- 「そのJavascriptカスタマイズ、ホントに必要？」を挟んでから実装しよう！


- コピペする前にサンプルアプリで検証して自分の血肉にしてから実装しよう！

 
- 制限事項もよく読もう！→[カーソルAPIの制限事項](https://developer.cybozu.io/hc/ja/articles/360029152012#step4)


今日のところはいじょうです。