【kintone】kintone REST API でレコードをたくさん(100件以上)登録する方法

こんにちは！
今回は、他のアプリにたくさん準備したレコードを、1つのアプリに全部登録するということをやってみたいと思います。
kintone REST API では一度に一括登録できるレコード数の上限が100件です。
なので、何回も100件登録を繰り返すことになるかなぁ・・・。

今回作ってみるのは

アプリA：レコードたくさん
アプリB：レコードたくさん
↓
アプリCに登録（たくさんのレコードを100件ずつ切り取りながら・・・）

という感じ。

ちょっと説明足らずになるかも知れませんが、やっていきたいと思います！

# アプリの準備

(1)エクセルなどで2000件くらいレコードを作ります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/536148/7b994e06-189f-01d9-cf2b-314c9faa5c15.png)
こんなデータを作りまして、
アップロードしてアプリA,アプリBを作成します。

(2)アプリAを再利用してレコードが空っぽのアプリCを作成します。
↓こんな感じ

| アプリA | アプリB |アプリC|
|:-:|:-:|:-:|
| ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/536148/6de21128-34ef-ed15-d282-b56d5f68ac89.png)  | ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/536148/2dc6e2a7-4ff5-0aa0-48cc-2ef845b74dd0.png)  |![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/536148/2df6fd2b-8a88-1813-2471-9a36f87ea124.png)|

ちなみにどのアプリもフィールドはこんなかんじ

| フィールド名 | フィールドコード | フィールド種類 |
|:-:|:-:|:-:|
|  数字 | 数値  | 数値  |
|  名前 |  文字列__1行_ | 文字列(1行)  |


# JavaScriptを書く

## kintone UI Componentを使う

こちらから、
https://github.com/kintone/kintone-ui-component/tree/master/dist

kintone-ui-component.min.css
kintone-ui-component.min.js

をダウンロードしてアプリCにアップロードしておきます。
（spinnerという機能を使いたくて）

## カーソルAPIを使って取ってくる
アプリA,アプリBからは公式のコードを使って取ってくるようにしましょう。
[レコード一括取得の JavaScript コーディング例 - カーソル API を利用する方法](https://developer.cybozu.io/hc/ja/articles/360031191791#use_cursor)

※イマドキなlet、constで書くES6なコードは、 
[kintone Advent Calendar 2020](https://qiita.com/advent-calendar/2020/kintone) 8日目
@tarimo34 さんの記事[カーソルAPIを使ってレコードを一括取得したい（シンプル風味のソースを添えて）](https://qiita.com/tarimo34/items/8169dcdcccc1b23d2cfb)
を是非ご覧くださいっ！

第1段階の準備としてはこんな感じ。
`//ココに一覧表示後イベントのコードを書いていく`と
`//ココにレコード登録用コードを書いていく`のところにコードを書いていきます。

```js
(()=>{

    'use strict';

    kintone.events.on(['app.record.index.show'],  event => {
    //ココに一覧表示後イベントのコードを書いていく
    });

    const putRecords = async (_params) =>{
    //ココにレコード登録用コードを書いていく
    }

//以下、公式サイト通りのソース
    // カーソルを作成する
    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 });
        });
    };

})();
```

## 一覧表示イベント

一覧表示はこんな感じにします。
↓spinner(待ってる間にグルグル表示するやつ)についてはこちらをご参考に
[「kintone UI Component」を使って簡単にkintoneライクなUIを設置する](https://developer.cybozu.io/hc/ja/articles/360000511023--kintone-UI-Component-%E3%82%92%E4%BD%BF%E3%81%A3%E3%81%A6%E7%B0%A1%E5%8D%98%E3%81%ABkintone%E3%83%A9%E3%82%A4%E3%82%AF%E3%81%AAUI%E3%82%92%E8%A8%AD%E7%BD%AE%E3%81%99%E3%82%8B)

```js
//一覧表示後イベント
kintone.events.on(['app.record.index.show'],  event => {
    //メニュー横の
    const sp = kintone.app.getHeaderMenuSpaceElement()
    //ボタン追加
    const btn = document.createElement('button');
    btn.textContent='ボタン';
    sp.appendChild(btn);

    //sppiner追加
    const spinner = new kintoneUIComponent.Spinner();
    const spn = spinner.render();
    sp.appendChild(spn);

    //ボタンクリック
    btn.onclick = async () => {
        //sppiner（待ってる間グルグル表示するやつ）表示する
        spinner.show();

        //アプリA、アプリBからレコード全件取ってくる（公式のコード内の関数を使用）
        const appA = await getRecords({app:アプリAのID, fields:['文字列__1行_','数値']});
        const appB = await getRecords({app:アプリBのID, fields:['文字列__1行_','数値']});

        //アプリAとアプリBのレコードをつなげる
        const appAB = appA.records.concat(appB.records);
        //アプリCにレコードを登録する（次の節でつくる関数）
        await postRecords({app:アプリCのID, records:appAB, next:appAB.length>0}); 
        
        //sppiner隠す（表示更新するから要らないと思うけど）
        spinner.hide();

        //表示更新
        location.reload();
    }

});
```


アプリAとアプリBからレコードを全件取ってきて、くっつけて、アプリCに登録する・・・という流れです。
`fields:['文字列__1行_','数値']`とすることで、余計な「レコード番号」等を取ってこなくて済みます。

```js

//アプリA、アプリBからレコード全件取ってくる（公式のコード内の関数を使用）
const appA = await getRecords({app:アプリAのID, fields:['文字列__1行_','数値']});
const appB = await getRecords({app:アプリBのID, fields:['文字列__1行_','数値']});

//アプリAとアプリBのレコードをつなげる
const appAB = appA.records.concat(appB.records);
//アプリCにレコードを登録する（次の節でつくる関数）
await postRecords({app:アプリCのID, records:appAB, next:appAB.length>0});
```


## レコード登録（今回のメイン）

たくさんのレコードをどうやって登録しましょうかね？
というわけで、

`postRecords`関数として↓を考えてみました。


```js

const postRecords = async (_params) =>{
        
    //_params.nextがfalseだったら終了
    if (!_params.next ){ return; }
    
    //今回POSTする100件のbody
    const body = {
        app: _params.app,
        records: _params.records.splice(0,100), //POSTする最大100件分だけ取り出す。
    };

    //次回以降分のパラメータ
    const next_params = {
        app: _params.app,
        records: _params.records, //残りのレコード
        next:(_params.records.length == 0) ? false: true　//残りのレコードが0件ならfalse
    }

    return kintone.api(kintone.api.url('/k/v1/records', true), 'POST', body).then(function(r) {
        //next_params.nextがfalseだったらreturn;でいいやんと、投稿後に気づいた。
        return postRecords(next_params);
    });
}
```

引数の`_params`ですが、
ドコから何を渡してやってきたかと言うと、この部分です。

```js

//アプリCにレコードを登録する（次の節でつくる関数）
await postRecords({app:アプリCのID, records:appAB, next:appAB.length>0}); 
```

引数の中身はこんな感じ。
**_params.app**：アプリCのID
**_params.records**：登録するレコード（AとBあわせた4000件くらいのレコード）
**_params.next**：true or false（続きのレコードがあればtrue, 全部登録し終わったらfalse)


↓つづいてこちら`body`はレコードPOSTのためのパラメータ。

```js
//今回POSTする100件のbody
const body = {
    app: _params.app,
    records: _params.records.splice(0,100), //POSTする最大100件分だけ取り出す。
};

```

**kintone REST APIのPOSTではレコードは100件ずつまでしか一括登録できない**ので、
100件ずつ登録して、続きがあれば繰り返すようにしています。

`_params.records.splice(0,100)`

ここアプリABのレコードである`_params.records`の4000件の100件を取り出して`body`の`records`に渡しています。
※`_params.records`自体は3900件に減ります。


続きまして、次回用のパラメータ（postRecords関数を再帰的に呼び出す際のパラメータ）です。

```js

//次回以降分のパラメータ
const next_params = {
    app: _params.app,
    records: _params.records, //残りのレコード
    next:(_params.records.length == 0) ? false: true　//残りのレコードが0件ならfalse、0件じゃなければtrue
}
```

そして最後の「登録！」の部分

```js
return kintone.api(kintone.api.url('/k/v1/records', true), 'POST', body).then(function(r) {
    return postRecords(next_params);
});
```

１行目で100件に切り取ったレコード分のPOSTをして、メソッドチェーン。
もう一度自分(postRecords)を呼びます。
何だか不思議ですが、自分(postRecords)を次々に呼ぶことにより、
最初4000件ほどあったレコードが100件ずつ切り取られていって、最後0件になったときにはもうPOSTせずに終わる・・・。
というような処理を実現できます。（再帰的な処理といいます）

※処理を終わりたいときは自分を呼ぶ時のパラメータで、`next_params.nextをfalse`にしておけば、次の`postRecords`関数内の最初で`return;`して、処理を終了！とすることができるのです。

```js
//_params.nextがfalseだったら終了
if (!_params.next ){ return; }
```

# 動作確認
アプリAに2201件、アプリBにも2201件レコードがある状態で、アプリCで動かしてみます。
ノーカット版なのでちょっとspinner表示（グルグル）が長いよ！

![231.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/536148/ea37d210-f696-694e-82bd-98bf11544c8b.gif)

# まとめ
たくさんのレコードから、どうやって100件ずつ登録したら良いかなぁと思いましたが、なんとなくうまくいきました！splice(0,100)でいい感じに取ってこれそうです。

※kintone JavaScript ClientのaddAllRecordsというメソッドを使うという方法もありますが、別の機会に書けたら良いなぁと思います。


また、いつものことながら、行数節約のため、エラー処理などは入れていません（＞＜）
もっと良い方法があったり、この書き方だとこんなデメリットがあるよ～というのがあったら教えて下さいm(_ _)m

というわけで、今日はこのへんで～(´・ω・`)ﾉｼﾉｼ

# 追記

冷静にコードを読んでいたら・・・、nextなフラグなんていらなかったので、修正したものを貼り付けて終わりますm(_ _)m

```js
kintone.events.on(['app.record.index.show'],  event => {
    const sp = kintone.app.getHeaderMenuSpaceElement()
    //ボタン追加
    const btn = document.createElement('button');
    btn.textContent='ボタン';
    sp.appendChild(btn);

    //sppiner追加
    const spinner = new kintoneUIComponent.Spinner();
    const spn = spinner.render();
    sp.appendChild(spn);

    btn.onclick = async () => {
        //sppiner表示
        spinner.show();
        const appA = await getRecords({app:アプリAのID, fields:['文字列__1行_','数値']});
        const appB = await getRecords({app:アプリBのID, fields:['文字列__1行_','数値']});
        const appAB = appA.records.concat(appB.records);

        //登録！
        await postRecords({app:アプリCのID, records:appAB});
        
        //sppiner隠す（表示更新するから要らないと思う）
        spinner.hide();

        //表示更新
        location.reload();
    }

});

const postRecords = async (_params) =>{
        
    //今回POSTする100件のbody
    const body = {
        app: _params.app,
        records: _params.records.splice(0,100), //POSTする最大100件分だけ取り出す。
    };

    //次回以降分
    const next_params = {
        app: _params.app,
        records: _params.records, //残りのレコード
    }

    return kintone.api(kintone.api.url('/k/v1/records', true), 'POST', body).then(function(r) {
        return _params.records.length > 0 && postRecords(next_params);
    });
}
```