kintone × GAS kintoneアプリの情報をスプレッドシートに書き出す
自分のメモ、頭の整理のために書いています。クソ長くなってしまった。
自分がContent-Typeヘッダーを適当にしていたことがよくわかった(反省)。
思ったより色々めんどくさかった。
0.概要と余談
GitHub
概要
スプレッドシートがマスタであったものをkintoneアプリでDB化したは良いものの、その情報をスプレッドシートに落として参照したい時、あると思います。
ワンショットならCSVをエクスポートすれば良いですが、定常的にkintoneの情報をスプレッドシートに書き出したいニーズがあります。
そんな時のために、GASでkintoneアプリの情報を書き出す時のTips、定型的なものをメモしておきます。
- 対象読者:kintoneデータをスプレッドシートで参照・集計・レポート化したい人
- 前提知識:kintoneの閲覧用APIトークン取得、GASの基本操作ができること
概要、要点は下記のとおりです:
- セキュリティ: APIトークンはスクリプトプロパティで管理する
- 権限設計: 「レコード閲覧」のみで動作する設計
- パフォーマンス: ページネーションで大量データにも対応する
- データ形式: フィールドタイプに応じた適切な値変換
- エラーハンドリング: 本番運用を想定した例外処理
- クラス設計: 保守性と拡張性を重視した実装
余談
なお、kintoneには JavaScript API と REST API があります。
今回はGASから外部連携するため、REST APIを使用します。
JavaScript API と REST API のざっくり比較は下表のとおりです。
項目 | JavaScript API | REST API |
---|---|---|
実行場所 | kintoneアプリ内のブラウザ | 外部システム(GAS、サーバーなど) |
認証 | セッション認証(自動) | APIトークン、パスワード認証 |
用途 | アプリのカスタマイズ、UI操作 | 外部システムとの連携、バッチ処理 |
制限 | kintoneの画面上でのみ動作 | ネットワーク経由でどこからでもアクセス可能 |
コード例 | kintone.api()、kintone.events.on() | UrlFetchApp.fetch()、HTTP リクエスト |
余談ですが、JavaScriptカスタマイズは「作ったは良いが属人化して誰もメンテできない」「セキュリティホールが放置される」といった問題もあり、社内にkintone専門部隊がいるとか、コード管理、運用保守体制やセキュリティ管理体制がないうちは、慎重に検討した方が良いのでは、とも思っているところがあります。
REST APIは、まだkintone本体への影響が限定的なので、運用負荷が比較的軽いのではないかと思います。
さらに言うと、組織内でのGASコードの管理、運用はどないすんねやという話にもなってきますが、それはまた別のお話。
kintoneは全て小文字が正しいです。
個人的に、Kintoneと表記しているベンダー、コンサルには警戒心を高めています。
1.APIトークンの取得と設定
ポイント
- APIトークンはスクリプトプロパティに保存し、コード内に直接記載しない
- kintoneアプリの設定でAPIトークンに適切な権限(閲覧権限など)を設定する
kintoneアプリのAPIトークン生成方法はkintoneヘルプを参照してください。
今回のように、単にkintoneアプリの情報をスプレッドシートに書き出すだけであれば、アクセス権は「レコード閲覧」のみにしておきましょう。GETだけして、POST/DELETEなどはしない、という感じです。「レコード閲覧」だけであれば編集もできないので安心です。企業環境では「より安全な方」を選択することを強く推奨します。
GASのコード上に直接APIトークンを記述すること(ハードコーディング)は避け、スクリプトプロパティを利用します。
スクリプトプロパティは、GUIで設定でもコードで設定してもどちらでも良いです。
GASコード例
コードでスクリプトプロパティにトークンを設定
/**
* APIトークンをスクリプトプロパティに設定する関数
*/
function setApiToken() {
const token = 'your-api-token-here'; // 実際のAPIトークンに置き換え
PropertiesService.getScriptProperties().setProperty('KINTONE_API_TOKEN', token);
console.log('APIトークンを設定しました');
}
設定値の読み込み
/**
* kintone API設定
*/
const KINTONE_CONFIG = {
subdomain: 'your-subdomain', // kintoneのサブドメイン
appId: 123, // アプリID
apiToken: PropertiesService.getScriptProperties().getProperty('KINTONE_API_TOKEN')
};
より厳密に管理したい場合、全ての設定値をスクリプトプロパティで管理してもよいでしょう。
// より厳密な設定例
const KINTONE_CONFIG = {
subdomain: PropertiesService.getScriptProperties().getProperty('KINTONE_SUBDOMAIN'),
appId: PropertiesService.getScriptProperties().getProperty('KINTONE_APP_ID'),
apiToken: PropertiesService.getScriptProperties().getProperty('KINTONE_API_TOKEN')
};
スクリプトプロパティのリファレンス(Google Apps Scriptの公式リファレンス):
なぜスクリプトプロパティを使うのか?
セキュリティ上の理由:
- コードに直接記載すると、コード共有時にAPIトークンが漏洩するリスクがある
- GitHubなどにアップロードしてしまうと誰でも閲覧可能になる
- スクリプトプロパティは実行時にのみアクセスされ、コード上には表示されない
運用上の理由:
- 本番・テスト環境でトークンを使い分けられる
- トークン変更時にコード修正が不要
2.ページネーションを考慮したデータ取得
「kintoneからスプレッドシートに落とすだけでしょ?楽勝でしょ?」と思っていた私を戒める章です。
なぜページネーションが必要なのか
- kintoneの取得制限(500件/回)
- 大量データを効率的に処理するため
kintoneのREST APIには1回のリクエストで最大500件までしか取得できない制限があります。
つまり、1,000件のデータがあっても一度に全部は取れません。
そこで必要になるのが「ページネーション」です。
データを小分けにして複数回に分けて取得し、最終的に全件を揃える仕組みです。
大量データを効率的に処理するためには、適切なページネーション戦略が不可欠になります。
基本方針:データ量に応じた3つのアプローチ
データの件数によって最適な取得方法が変わります:
- 10,000件以下 → offset方式(シンプルで分かりやすい)
- 10,000件超 → レコードID方式(offset制限を回避)
- ソート条件が複雑 → カーソルAPI方式(今回は割愛、POST必要)
まずは件数を確認してから、適切な方法を選択しましょう。
API制限と注意点
実装時に知っておくべき制限事項:
- 1回の取得上限: 最大500件
- offset上限: 10,000件(10,001件目以降は取得不可)
- API実行回数制限: 1時間あたり10,000回
- 推奨待機時間: 100ms程度(API負荷軽減のため)
- キーワード検索の制限: 100,000件に達した時点で検索打ち切り
ページネーション実装のポイント
パフォーマンス最適化:
- API呼び出し間隔を調整(Utilities.sleep())
- 不要なフィールドは除外(fieldsパラメータを使用)
- バッチサイズの適切な設定(500件推奨)
エラーハンドリング:
- HTTPステータスコードのチェック
- 進行状況の詳細ログ出力
- 一時的なネットワークエラーに対するリトライ機能
仕様整理:
- limit<=500、offset<=10000。
推奨:
- 10,000件未満 → OffsetでOK。
- 10,000件以上 or 将来増加見込み → レコードIDを利用した取得(シーク法)(order by $id asc + id > lastId) か Cursor API に切り替え。
APIリクエストの落とし穴
実装中にちょいとハマった問題です。
うっかりGETリクエストにContent-Typeヘッダーを付けると400エラーが発生します。
HTTP仕様では「sending a payload body on a GET request might cause some existing
implementations to reject the request/既存の実装によってはリクエストが拒否される可能性がある」とされていますが、APIによってその解釈、対応が異なるようです。
寛容なAPI: 「不適切だけど害はないので無視しよう」→割と多い、GitHub/Google系
厳格なAPI: 「不適切なので拒否しよう」→kintoneはこちら
RFC 7231(HTTP/1.1 Semantics and Content)
A payload within a GET request message has no defined semantics;
sending a payload body on a GET request might cause some existing
implementations to reject the request.
(Google翻訳) GET リクエストメッセージ内のペイロードには定義されたセマンティクスがありません。GET リクエストでペイロード本体を送信すると、既存の実装によってはリクエストが拒否される可能性があります。
// ❌ 400エラーの原因
const options = {
method: 'GET',
headers: {
'X-Cybozu-API-Token': apiToken,
'Content-Type': 'application/json' // これが原因
}
};
// ✅ リクエストが通る実装
const options = {
method: 'GET',
headers: {
'X-Cybozu-API-Token': apiToken
// Content-Typeは不要
}
};
3. クラス設計による実装
設計思想
- 最小権限: レコード閲覧権限のみで動作
- 動的フィールド検出: レコードからフィールド情報を構築
- エラー耐性: 本番運用を想定した例外処理
- 拡張性: 増分取得などの機能追加が容易
クラス実装例
4. 使用方法と実際の動作結果
基本的な使用例
// 設定値をスクリプトプロパティから取得
const CONFIG = {
subdomain: PropertiesService.getScriptProperties().getProperty('KINTONE_SUBDOMAIN'),
appId: PropertiesService.getScriptProperties().getProperty('KINTONE_APP_ID'),
apiToken: PropertiesService.getScriptProperties().getProperty('KINTONE_API_TOKEN')
};
// 基本的な使用例
async function exportAllData() {
const exporter = new KintoneSpreadsheetExporter(CONFIG);
try {
const result = await exporter.exportToSheet();
console.log(`エクスポート完了: ${result.recordCount}件`);
} catch (error) {
console.error('エクスポートエラー:', error);
}
}
実際の動作結果
パフォーマンス(参考値):
- 約1,500件のレコードを約7秒で処理
- 自動的にoffset方式を選択(10,000件以下)
- 約40個のフィールドを動的に検出・書き出し
5. フィールドタイプの処理
フィールドタイプの処理 実装例
/**
* フィールドタイプに応じた値のフォーマット
* @private
*/
_formatFieldValue(field, fieldType) {
if (!field || field.value === null || field.value === undefined) {
return '';
}
switch (fieldType) {
case 'SINGLE_LINE_TEXT':
case 'MULTI_LINE_TEXT':
case 'RICH_TEXT':
case 'NUMBER':
case 'CALC':
case 'LINK':
case 'RECORD_NUMBER':
return field.value;
case 'DATE':
case 'DATETIME':
case 'CREATED_TIME':
case 'UPDATED_TIME':
return field.value ? new Date(field.value) : '';
case 'TIME':
return field.value;
case 'DROP_DOWN':
case 'RADIO_BUTTON':
return field.value;
case 'CHECK_BOX':
case 'MULTI_SELECT':
return Array.isArray(field.value) ? field.value.join(', ') : field.value;
case 'USER_SELECT':
case 'ORGANIZATION_SELECT':
case 'GROUP_SELECT':
case 'CREATOR':
case 'MODIFIER':
if (Array.isArray(field.value)) {
return field.value.map(item => item.name || item.code).join(', ');
}
return field.value?.name || field.value?.code || '';
case 'FILE':
if (Array.isArray(field.value)) {
return field.value.map(file => file.name).join(', ');
}
return '';
case 'SUBTABLE':
return `[サブテーブル: ${field.value?.length || 0}行]`;
case '__ID__':
case '__REVISION__':
return field.value;
default:
return field.value?.toString() || '';
}
}
なぜフィールド形式の処理が必要か
kintoneのフィールドは多様な形式でデータを保持しており、そのままスプレッドシートに出力すると以下の問題が発生します:
- 配列系フィールド:チェックボックスや複数選択は配列で格納されているため、[Object object] と表示される
- ユーザー選択系:ユーザー情報はオブジェクト形式で、名前やコードを適切に抽出する必要がある
- 日時系:文字列で格納されているため、スプレッドシートの日付として認識させるにはDate型への変換が必要
- ファイル系:ファイル情報は複雑なオブジェクト構造になっている
各フィールドタイプの特徴と処理方法
テキスト系フィールド
case 'SINGLE_LINE_TEXT': // 文字列(1行)
case 'MULTI_LINE_TEXT': // 文字列(複数行)
case 'RICH_TEXT': // リッチエディター
case 'NUMBER': // 数値
case 'CALC': // 計算
return field.value;
これらは単純な値なのでそのまま返します。ただし、RICH_TEXTはHTML形式で格納されているため、タグを除去したい場合は別途処理が必要です。
日時系フィールド
case 'DATE':
return field.value ? new Date(field.value) : '';
case 'DATETIME':
return field.value ? new Date(field.value) : '';
kintoneでは日時が文字列で格納されているため、スプレッドシートで日付として扱うにはDate型に変換します。
選択系フィールド
case 'DROP_DOWN': // ドロップダウン
case 'RADIO_BUTTON': // ラジオボタン
return field.value; // 単一値なのでそのまま
case 'CHECK_BOX': // チェックボックス
case 'MULTI_SELECT': // 複数選択
return Array.isArray(field.value) ? field.value.join(', ') : field.value;
複数選択系は配列で格納されているため、カンマ区切りの文字列に変換して見やすくします。
ユーザー・組織選択系
case 'USER_SELECT': // ユーザー選択
case 'ORGANIZATION_SELECT': // 組織選択
case 'GROUP_SELECT': // グループ選択
if (Array.isArray(field.value)) {
return field.value.map(item => item.name || item.code).join(', ');
}
return field.value?.name || field.value?.code || '';
これらのフィールドは以下のような構造で格納されています:
{
"value": [
{"code": "sato", "name": "佐藤太郎"},
{"code": "tanaka", "name": "田中花子"}
]
}
表示名(name)を優先し、なければコード(code)を使用します。
ファイル系フィールド
case 'FILE':
if (Array.isArray(field.value)) {
return field.value.map(file => file.name).join(', ');
}
return '';
ファイルフィールドの実際の構造:
{
"value": [
{
"fileKey": "20230101123456789",
"name": "sample.pdf",
"contentType": "application/pdf",
"size": "12345"
}
]
}
現在はファイル名のみを出力しています。
サブテーブル
case 'SUBTABLE':
return `[サブテーブル: ${field.value?.length || 0}行]`;
サブテーブルは行データの配列なので、行数のみを表示。詳細を展開したい場合は別途処理が必要になります。
実装上の注意点
null/undefined チェック
if (!field || field.value === null || field.value === undefined) {
return '';
}
フィールドが存在しない場合や値が空の場合の防御的プログラミングです。
型情報の取得
const fieldType = fieldMap[fieldCode].type;
フィールドの型情報は取得したレコードのfield.typeプロパティから動的に判定します。
デフォルト処理
default:
return field.value?.toString() || '';
予期しないフィールドタイプに対する汎用的な処理を用意しておきます。
この処理により、kintoneの多様なフィールドタイプをスプレッドシートで見やすい形式に統一できます。運用要件に応じて、さらに詳細な変換ロジックを追加することを検討してください。
6.スプレッドシートへの書き出し
処理の全体の流れ
- スプレッドシートの準備:シートの取得・作成・初期化
- データ取得:kintoneからレコードを取得
- ヘッダー行の構築:フィールド情報を基にした列名設定
- データ行の変換:各レコードをスプレッドシート用の配列に変換
- 一括書き込み:パフォーマンスを考慮した効率的な出力
- 装飾・調整:見やすさのための書式設定(なくてもいい)
実装例はGitHub参照してください。
7.エラーハンドリングとロギング
/**
* エラーハンドリング強化版
*/
async function exportWithErrorHandling() {
const exporter = new KintoneSpreadsheetExporter(CONFIG);
try {
exporter.showConfig();
const result = await exporter.exportToSheet();
const message = `kintone同期完了\n件数: ${result.recordCount}\n時間: ${result.duration}秒`;
console.log(message);
} catch (error) {
const errorMessage = `kintone同期エラー\n${error.message}\n\nスタック:\n${error.stack}`;
console.error(errorMessage);
sendErrorNotification(errorMessage);
}
}
// === 通知関数(実装例)===
function sendErrorNotification(message) {
// Gmail通知の例
try {
GmailApp.sendEmail(
'admin@yourcompany.com',
'kintone同期エラー',
message
);
} catch (e) {
console.error('メール送信失敗:', e);
}
}
function sendUrgentNotification(error) {
// Slack Webhook通知の例
try {
const payload = {
text: `🚨 kintone同期で緊急エラー\n${error.message}`,
channel: '#alerts'
};
// Slack Webhook URL(実際のURLに置き換え)
const webhookUrl = 'https://hooks.slack.com/services/YOUR/SLACK/WEBHOOK';
UrlFetchApp.fetch(webhookUrl, {
method: 'POST',
contentType: 'application/json',
payload: JSON.stringify(payload)
});
} catch (e) {
console.error('Slack通知失敗:', e);
}
8.定期実行
インストーラブルトリガーでもコード上でトリガー設定でもお好みでどうぞ。
/**
* 定期実行用(トリガーから呼ばれる想定)
*/
async function scheduledExport() {
const startTime = new Date();
const maxDuration = 5 * 60 * 1000;
try {
const exporter = new KintoneSpreadsheetExporter(CONFIG, {
sheetName: `データ_${Utilities.formatDate(new Date(), 'Asia/Tokyo', 'yyyyMMdd_HHmm')}`
});
const result = await exporter.exportToSheet();
const endTime = new Date();
const duration = endTime - startTime;
if (duration > maxDuration * 0.8) {
console.warn(`実行時間注意: ${duration / 1000}秒`);
}
console.log(`定期実行完了: ${result.recordCount}件`);
} catch (error) {
console.error('定期実行エラー:', error);
sendUrgentNotification(error);
}
}
/**
* 定期実行のトリガーを設定
*/
function setupScheduledTrigger() {
// 既存のトリガーを削除
const triggers = ScriptApp.getProjectTriggers();
triggers.forEach(trigger => {
if (trigger.getHandlerFunction() === 'scheduledExport') {
ScriptApp.deleteTrigger(trigger);
}
});
// 毎日午前9時に実行するトリガーを作成
ScriptApp.newTrigger('scheduledExport')
.timeBased()
.everyDays(1)
.atHour(9)
.create();
console.log('定期実行トリガーを設定しました(毎日9時)');
}
9.まとめ
ポイント:
- セキュリティ: APIトークンはスクリプトプロパティで管理する
- 権限設計: 「レコード閲覧」のみで動作する設計
- パフォーマンス: ページネーションで大量データにも対応する
- データ形式: フィールドタイプに応じた適切な値変換
- エラーハンドリング: 本番運用を想定した例外処理
- クラス設計: 保守性と拡張性を重視した実装
この実装により、kintoneアプリのデータを効率的にスプレッドシートに同期できます。
定期実行設定により、常に最新のデータを参照することが可能になります。
10.免責事項
本記事は技術情報の共有を目的としており、実装や運用については読者の責任と判断のもとで行っていただくことを前提としています。
重要な注意点:
- 本記事の内容を実環境で使用する際は、必ず十分なテストを実施してください
- セキュリティやパフォーマンスについては、各組織の要件に応じて適切に検証・調整してください
- システム障害やデータ損失等、いかなる損害が発生した場合も筆者は責任を負いかねます
記事について:
- 本記事は公開情報および一般的な技術知識に基づいて作成されています
- 内容は筆者の個人的見解であり、所属組織の公式見解ではありません
実装前には公式ドキュメント等を必ずご確認いただき、適切なテスト環境での検証を強く推奨いたします。
Discussion