【神アプデ】kintone REST APIでレコードのUPSERTが超簡単にできるようになったので試してみた【最新機能】

遅ればせながら、明けましておめでとうございます!

2025年もよろしくお願いいたします。

さて、今月12日にkintoneの月例アップデートがありましたが、
その中に1つ興味深いものがありました。

『レコードをUPSERT(対象レコードがあれば更新なければ登録)する操作をREST APIで行えるように』

これ、かなりのコード短縮が見込める神アプデだな~と思いました!

APIの詳細な仕様についてはこちらをご覧ください。

今回は、このUPSERTを実際に試してみたいと思います!

HTTP Client Tool for kintoneで試してみた

最初に、kintone REST APIを気軽に試すことができる「HTTP Client Tool for kintone」でやってみます。

今回のUPSERTは従来のレコード一括更新APIの機能追加という形で実装されているので、
リクエストURLは「/k/v1/records.json」、HTTPメソッドは「PUT」です(従来のものと同じ)。

認証は、今回はAPIトークンを使用しましたが、
従来のレコード一括更新APIはレコード閲覧・編集権限があれば使えましたが、
今回のUPSERTは「対象レコードがなければ登録」するので、レコード追加権限も必要になります。

APIトークンへの権限追加を忘れないように注意してくださいね。

そして、今回UPSERTを試すリクエストボディがこちら↓

{
	"app": 2,
	"upsert": true,
	"records": [
		{
			"id": 6,
			"record": {
				"日付": {
					"value": "2025-01-01"
				}
			}
		},
		{
			"id": 999999999999,
			"record": {
				"日付": {
					"value": "2025-01-22"
				}
			}
		}
	]
}

upsert」が今回追加されたパラメーターで、
UPSERTモードでレコード一括更新APIを使う場合はtrueを、
従来の更新のみの利用の場合はfalseを設定します。
※指定しなかった場合はfalseとして扱われます

ここからが問題なのですが、
レコード一括更新APIは、更新の際にキーとする「id」「updateKey」の何れかが必須です。

今回はテストなので「id」の方で指定をしてみましたが、「id」はレコード番号なので、
nullにしたり、空文字にしたり、通常あり得ない0未満の数値にしたりするとエラーになりました。

今回はテストで追加したいだけなので、
現時点でアプリに存在しない大きい数値(999999999999)を入れることにしましたが、
現状kintoneにはレコード数の最大値が定められていないので、
この数値でも更新されてしまう可能性は0ではありません。

まあそもそも最初からレコード追加だってプログラムで判別できているなら、
素直にレコード一括登録APIの方を使ってくれってことなのでしょうね。

リクエストが成功すると、以下のレスポンスが返ってきました。

{
    "records": [
        {
            "id": "6",
            "revision": "4",
            "operation": "UPDATE"
        },
        {
            "id": "7",
            "revision": "1",
            "operation": "INSERT"
        }
    ]
}

どうやらどんなに大きい数値を入れようが、
存在しないレコード番号であれば従来通り通し番号で無事に採番されるみたいですね。

従来のコードを書き換えてみた

さて、ここからは実際に従来のコードを書き換えて、どれぐらい短縮されるのかを見てみましょう。

今回はkintoneアプリから別のkintoneアプリを更新するJavaScript関数を例にします。

従来のコードはChatGPT君に書いてもらいました。
※少し手を加えていますが動作テストはしていないので動作を保証するものではありません

(() => {
	"use strict";

	/**
	 * kintoneのカーソルを使って全てのレコードを取得する関数
	 * @param {number} appId アプリID
	 * @returns {Array} 取得した全レコード
	 */
	async function fetchAllRecordsWithCursor(appId) {
		let allRecords = [];
		try {
			// 1. カーソルを作成
			const cursorId = (await kintone.api(
				kintone.api.url('/k/v1/records/cursor.json', true), 'POST', {
					app: appId,
					size: 500
				}
			)).id;

			// 2. カーソルを使って全てのレコードを取得
			while (true) {
				const getResponse = await kintone.api(
					kintone.api.url('/k/v1/records/cursor.json', true), 'GET', {
						id: cursorId
					}
				);
				allRecords = allRecords.concat(getResponse.records);

				if (!getResponse.next) break;
			}
		} catch (error) {
			console.error('Error fetching records with cursor:', error);
		}

		return allRecords;
	}

	/**
	 * メールアドレスをキーに一括UPSERTする関数
	 * @param {number} appId アプリID
	 * @param {Array} recordsData UPSERT対象のレコードデータの配列
	 */
	async function bulkUpsertRecords(appId, recordsData) {
		try {
			const updates = [];
			const creations = [];

			// 3. レコードを更新・新規作成に振り分け
			recordsData.forEach(async record => {
				if ((await fetchAllRecordsWithCursor(appId)).map(record =>
					record['メール'].value).includes(record['メール'].value)) {
					updates.push({
						updateKey: record['メール'].value,
						record: record
					});
				} else {
					creations.push(record);
				}
			});

			// 4. 更新処理
			if (updates.length > 0) {
				for (const chunk of chunkArray(updates, 100)) {
					await kintone.api(kintone.api.url('/k/v1/records.json', true), 'PUT', {
						app: appId,
						records: chunk
					});
					console.log('Updated records:', chunk);
				}
			}

			// 5. 新規作成処理
			if (creations.length > 0) {
				for (const chunk of chunkArray(creations, 100)) {
					await kintone.api(kintone.api.url('/k/v1/records.json', true), 'POST', {
						app: appId,
						records: chunk
					});
					console.log('Created records:', chunk);
				}
			}
		} catch (error) {
			console.error('Error in bulk UPSERT operation:', error);
		}
	}

	/**
	 * 配列を指定したサイズに分割するユーティリティ関数
	 * @param {Array} array 分割対象の配列
	 * @param {number} size 分割サイズ
	 * @returns {Array} 分割後の配列
	 */
	function chunkArray(array, size) {
		const chunks = [];
		for (let i = 0; i < array.length; i += size) {
			chunks.push(array.slice(i, i + size));
		}
		return chunks;
	}

	// 使用例
	kintone.events.on('app.record.index.show', async function (event) {
		if (event.records.length > 0) await bulkUpsertRecords(2, event.records);
		return event;
	});
})();

…な、長すぎ…笑

bulkRequest等もっとやれることはありますが、
今回は使用例でも一覧画面表示分から更新元レコード取得しているだけですし、今回は省きました。
※特に指定していないのにChatGPT君が100件毎の分割まで考慮してくれました

これをUPSERTでの書き方に変えてみます。

(() => {
	"use strict";

	/**
	 * メールアドレスをキーに一括UPSERTする関数
	 * @param {number} appId アプリID
	 * @param {Array} recordsData UPSERT対象のレコードデータの配列
	 */
	async function bulkUpsertRecords(appId, recordsData) {
		try {
			// 1. UPSERT
			for (const chunk of chunkArray(recordsData.map(record => ({
				updateKey: record['メール'].value,
				record: record
			})), 100)) {
				await kintone.api(kintone.api.url('/k/v1/records.json', true), 'PUT', {
					app: appId,
					upsert: true,
					records: chunk
				});
				console.log('UPSERT records:', chunk);
			}
		} catch (error) {
			console.error('Error in bulk UPSERT operation:', error);
		}
	}

	/**
	 * 配列を指定したサイズに分割するユーティリティ関数
	 * @param {Array} array 分割対象の配列
	 * @param {number} size 分割サイズ
	 * @returns {Array} 分割後の配列
	 */
	function chunkArray(array, size) {
		const chunks = [];
		for (let i = 0; i < array.length; i += size) {
			chunks.push(array.slice(i, i + size));
		}
		return chunks;
	}

	// 使用例
	kintone.events.on('app.record.index.show', async function (event) {
		if (event.records.length > 0) await bulkUpsertRecords(2, event.records);
		return event;
	});
})();

なんと、半分以下に短縮しました!!!

コードのコメントにもある通り、
既存レコード取得(カーソル)→対象の振り分け→更新&登録の流れで5ステップもあった処理が、
短縮後はたった1ステップだけになりました!

今回の例は先程の「id」ではなく、「updateKey」にメールアドレスを指定しているので、
メールアドレスが既存レコードに存在しない場合は新規登録、存在する場合は更新という例ですが、
既存レコードの取得も不要、対象の振り分けも不要、更新と登録も同じ処理で済むという楽さです。

更に、kintoneには1日に実行できるAPIリクエスト数に上限があり、
スタンダードコースでは1アプリにつき1万回です。

短縮後はREST APIを呼び出す回数も減っているので、リクエスト数の節約にもなっていますね!

終わりに(宣伝:kintoneフェス2025について)

最後までお読みいただきありがとうございました!

最後に宣伝なのですが、
3月7日(金)岡山市にてネットリンクス社主催の「kintoneフェス2025」というイベントがあります。

私はCybozu Days 2024に引き続き「k-Report」の販促として参加する予定ですので、
お近くにお住まいの方は是非お越しください。

今後もkintoneの更なるアップデートや関連製品の充実に期待したいですね!

追記:お願い

マシュマロの方でご質問をいただくことがありますが、こちらでは個別のご質問に回答できないため、
個別で回答が必要なご質問は該当記事のお問い合わせ欄か、
お仕事関連であればお問い合わせフォームの方へお願いいたします。