kintoneで2,000レコードを一度に入れる!bulkRequestを使ってみました
今回はkintone REST APIを呼ぶ共通関数を作りました
今回は概念的な話ではなく、より具体的なコードについて説明します。
これからkintone REST APIを呼び出す共通関数の作成とコードの内容についてお話するつもりです。共通関数は一度作ってしまえば使い回しができますが、作るまでが結構手間です。この部分だけは「kintoneだから簡単」というわけにはいかず、普通のWebアプリケーションを作るようにJavaScriptで作り込む必要があります。
REST APIの使用は、kintoneの外部からデータを取り込むためのインターフェースなのですが、kintoneは標準機能としてExcelやCSVデータをローカルPCから直接取り込む方法を用意していますので、REST APIの使用は、あくまでCSV取り込みができない場合の「プランB」として考えておきましょう。
大量データをkintoneに入れたい!しかし…
既存の大容量データをkintoneに一度に読み込ませたい、もしくは別のWebシステムに保存しているデータをkintoneと同期させたいケースはよくあるのではないでしょうか。データをkintoneだけで持つ場合は、CSVやExcel形式にしてローカルPCに持たせ、それをアップロードするだけですから簡単です。しかし前述のような「システム間連携」となるとWebブラウザからのアップロードは不可能で、kintone REST APIを使う必要があります。
しかしこのREST API、非常に悩ましい「制限」があります。
kintone側の負荷低減のためなのか、1アプリにつきAPI呼び出し回数は10,000件/1日の制限があります。「1万もあれば十分じゃないの?」と思われますか?それでは、もしあなたがkintoneに保存・同期させたいレコード数が1万以上あったらどうでしょうか?すぐに制限に引っかかります!
コーディングの経験がない方は、まだピンと来ていないかもしれません。
つまりREST APIを呼ぶための実装としては「1レコード=1 API呼び出し」でコードを書いてはいけない、ということです。これは結構、困ったことです。なにか解決策はないのでしょうか?
bulkRequestでまとめて送ろう
kintone REST APIには「bulkRequest」というAPIがあります。これは複数のレコードを表現したJSONをまとめて送ることができます。これを使えば、大量のレコードを登録できそうです。
POSTリクエストで同時に送ることができるレコード数100 x bulkRequestの最大リクエスト数20 = 2,000件まで一度に送信できる見込みです。もし登録が途中で失敗しても、2,000件までならロールバックするようですので、これが最大値でしょう。
更に、bulkRequestはすべてのHTTPメソッドを使うことができます。例えば1回のbulkRequestの中にPOSTを100件、PUTを100件、DELETEを100件…と、それぞれのメソッドの上限値まで一度のbulkRequestに詰めこんで、それを20回まで一度に送ることができます。
実際にコードを書く
では、コードを書いていきましょう。REST APIを呼ぶ際に使用するのは、 サイボウズがMITライセンスで公開している js-sdk です。
複数アプリのレコード操作を一括処理する を参照すると、kintoneに渡す引数は以下のようにする、とありました。各レコードはpayload内に書き、それをメソッド別にまとめているようです。apiはどれでも構いません。
requests: [
{
method: 'POST',
api: '/k/v1/record.json',
payload: {/* ここにレコードの中身を */}
},
{
method: 'PUT',
...
]
この、引数を呼ぶ処理、そしてbulkRequestを呼ぶ処理は何度も利用するので、関数としてライブラリ化しておくと良さそうです。問題は、「登録するレコード数は毎回変わるが、最大2,000レコードの範囲内でまとめる」処理をどのように書くかがポイントになるでしょう。
まずは定数から書きましょうか。
const POST_PUT_DEL_LIMIT = 100;
const BULK_REQUEST_LIMIT = 20;
POST、PUT、DELETEの最大値は100です。
bulkRequestの最大呼び出し回数(一度に繰り返すことのできる回数)は20です。
次は、methodですね。これはPOTなのかPUTなのか、引数で指定できるようにします。
exports.makeRequestBase = (method) => {
return {
method: method,
api: '/k/v1/records.json',
};
}
exportsなど、見慣れない書き方になっているのは、これはNode.js向けのCommonJS仕様で書いているからです。
ペイロードはどのように書きましょう。引数として、レコードJSONとアプリIDを取ります。
exports.makePayload = (_records, appId) => {
let records = [..._records];
let payloads = [];
while (records.length) {
payloads.push({
app: appId,
records: records.slice(0, POST_PUT_DEL_LIMIT),
});
records.splice(0, POST_PUT_DEL_LIMIT);
}
return payloads;
}
この関数のポイントは splice() と slice() 関数です。スペルも似たようなものなので間違えやすいですが、spliceは引数として与えた配列から指定の長さぶんを本当に削除します。関数型プログラミング言語で言う「副作用がある」関数ですね。一方、sliceは、配列のうち、指定した長さ分の参照を返しているだけであり、元の関数に変更を加えません。「副作用なし」です。クリーンです。安全・安心です。
なぜこれら2つの関数を使い分けれなければならないのでしょうか?それは前述の「何行のレコードが来るかわからないが、最大100レコードx20にまとめて送信する」ためです。
let records = [..._records];
少しずつ見ていきましょう。まず引数の配列をスプレッド構文 "…" でディープコピーしています。これはES2018言語仕様で追加された「配列もスプレッド構文できる」を利用しており、標準的な処理方法がなく毎回困ってしまうディープコピーを1行で書けるようになった、と理解しましょう。
while (records.length) {
payloads.push({
app: appId,
records: records.slice(0, POST_PUT_DEL_LIMIT),
});
records.splice(0, POST_PUT_DEL_LIMIT);
}
このwhileの意味はわかるかと思います。先ほどディープコピーした配列が0になるまで繰り返すという意味です。つまり、これからの処理で配列の要素数は「減っていく」のです。これは配列操作の副作用によるものです。
whileの中身は、というと配列payloadsに追加し続けているのは、実際のペイロードになる予定のデータです。ここでslice と splice が出てきました。
まずはsliceの部分、ここで「配列の先頭から100までの参照を返す」として、ペイロードのrecords部分に設定しています。
その後、spliceを使って配列の一部を実際に削除しています。これはどのような言語で書いても似たような仕組みになるのですが、非ソフトウェア・エンジニアの方向けに、処理を詳しく説明します。
例えば201件のレコードがあったとします。ヤバいですね。既に100件というPOSTの制限値も超えていますし、100で割ったら余りが出てきます。この場合、[100][100][1]の配列にまとめたいのです。
最初のwhileループでは、sliceで配列の先頭から100件までをrecordsに指定します。その後、spliceで先頭100件を削除します。
すると、whileの2回目のループではsliceの先頭100件というのは、実際には100件目-200件目のことになりますね。そしてspliceで100-200件のデータも消す。
3回目のループです。ここでは「余りの1件」をsliceで設定します。spliceでそれを削除します。
4回目のループ。whileの条件として「recordsの配列の長さが0ではないこと」になっているため、4回目ではwhileの中に処理は入りません。おわり。
上記を理解すれば、実際にbulkRequestを読んでいる関数も、何をしているのか理解できるのではないかと思います。下記コードでは、bulkRequestを呼ぶkintoneのフロントエンドは引数として渡していますが、これを廃止してコード例のように直接呼び出し処理を書いてしまっても構いません。
exports.bulkRequest = async(kintoneRestAPIClient, _requests) => {
// deep copy
let requests = [..._requests];
let responses = [];
while (requests.length) {
responses.push(
await kintoneRestAPIClient.bulkRequest({
requests: requests.slice(0, BULK_REQUEST_LIMIT),
})
);
requests.splice(0, BULK_REQUEST_LIMIT);
}
return responses;
}
共通関数を呼び出す
あとは一直線に関数を呼び出すだけです。map関数の使い方についてはmozillaのドキュメントを参照してください。これは、配列をのすべての要素に対して与えられた処理(この場合はbaseとpayloadの連結)を行い、それを新しい配列として返しています。
const postsPayload = restutil.makePayload(postAry, 111);
const postBase = restutil.makeRequestBase("POST");
const postReqs = postsPayload.map((payload) => ({ ...postBase, payload }));
const res = await restutil.bulkRequest(kintoneRestAPIClient, postReqs);
さいごに
「kintoneはローコード」と聞いたのに、こんなにコードを書かせるのか!とお怒りの方もいらっしゃることでしょう。しかしこの辺りはkintoneのせいというよりも、「あるWebアプリを別のWebアプリと同期させるための仕組み」ですから、どのようなWebアプリを使っても似たようなコードを書くことになるかと思います。それよりも、もし大量のデータをkintoneに取り込まなければならない場合は、まずそれらのデータをCSV形式にして、ローカルPCからkintoneに取り込めないかを考えてみてください。
追伸・複数アプリの同時更新
Nさんに「kintoneRestAPIClientはbulkRequestじゃなくても、一括登録、一括更新は出来るよ!2000件でロールバックするよ!」と突っ込みが入りました。しかし当時、Nさんは2000件のデータをAアプリに一括更新→Bアプリに一括登録したデータの同期が取れない事に悩んでいました。つまりBアプリの一括登録で失敗するとAアプリの更新はロールバックされません。
そこで以下のようなコード例を思いつきました。
// アプリA にPUT
const putsPayload = restutil.makePayload(putAry, 112); // appIdは適当
const putBase = restutil.makeRequestBase("PUT");
const putReqs = putsPayload.map((payload) => ({ ...putBase, payload }));
// アプリB にPOST
const postsPayload = restutil.makePayload(postAry, 113); // appIdは適当
const postBase = restutil.makeRequestBase("POST");
const postReqs = postsPayload.map((payload) => ({ ...postBase, payload }));
const merged = _.concat(putReqs, postReqs);
const res = await restutil.bulkRequest(kintoneRestAPIClient, merged);
複数アプリに対してREST APIを呼び出す際、bulkRequestを使えば一度に送信できます。これは、アプリAへの更新をアプリBにログとして登録する場合などで使えるでしょう。
上記の例ではアプリBのPOSTが失敗した場合、アプリAのPUTも同時にロールバックされます。kintone内の複数アプリの同時更新を行った場合、このような実装ならエラーが起きても中途半端なデータが残ってしまう心配もありません。
お問い合わせ
Kintone導入検討時のご相談から、導入後の利活用・定着化に至るまで、私たちは、お客様と「伴走」しながら思いを込めてサポートいたします。ご相談無料ですので、ぜひお気軽にお問い合わせください。
参照記事
記事作成
kintone推進チーム
UnsplashのMarkus Winklerが撮影した写真