メールや外部サービスからスプレッドシートへ転記する処理で、同じ注文が2行できてしまう。処理をやり直しただけなのに、受付数が増えてしまう。自動化では、1回成功させることに加え、同じ入力が再び来たときの扱いを決める必要があります。
処理の実行回数と、業務データの件数を分けて考えるのが出発点です。この記事では、受注元と注文IDを組み合わせたキーを使い、同じ注文を再登録しないGASの小さな実装例を紹介します。メール取得や定期実行は組み込みません。
件名や実行時刻を、注文の識別子にしない
同じ件名でも別の注文の場合があります。逆に、同じ注文のメールが再送されることもあります。「この処理を実行した時刻」ではなく、「業務上のどの1件か」を表す識別子が必要です。
| 候補 | 気を付けたいこと |
|---|---|
| メールの件名 | 別の注文でも同じ件名になり得る |
| 実行日時 | 再実行のたびに変わるため重複防止に使えない |
| メールのメッセージID | 同じメールの再処理は識別できるが、別メールでの同一注文までは識別できない |
| 受注元+注文ID | 元システムで同一注文を表すIDが安定しているなら業務の単位に合わせられる |
以下では「受注元」と「注文ID」の組を使います。注文訂正や取消は別の業務イベントとして設計が必要です。この例は初回受付だけを対象とし、既存行の更新や取消を行いません。
同時実行への対策と、重複チェックをセットにする
「まだ登録されていない」と2つの処理が同時に判定すると、両方が追記してしまいます。Google公式のLockServiceは、同時に動く処理が同じ箇所へ入るのを制限するための仕組みです。サンプルでは同一スクリプト内のロックを取ってから、キー確認と追記を行います。
ロックだけでは、前日に登録した注文の再処理は防げません。逆に、キー照合だけでは同時実行の競合が残ります。この2つは役割が異なります。
空の検証用シートで試すコード
新しい検証用スプレッドシートに「orders」というシートを作り、1行目のA〜D列へ key、source、order_id、received_at を順に入力します。そのスプレッドシートに紐づくApps Scriptで、次のコードを使います。既存の業務シートへそのまま貼らず、コピーで確認してください。
function appendOrderOnce(order) {
const source = checkedId(order.source, 'source');
const orderId = checkedId(order.orderId, 'orderId');
const key = JSON.stringify([source, orderId]);
const lock = LockService.getScriptLock();
if (!lock.tryLock(5000)) throw new Error('busy: retry later');
try {
const sheet = SpreadsheetApp.getActiveSpreadsheet()
.getSheetByName('orders');
if (!sheet) throw new Error('orders sheet not found');
const headers = ['key', 'source', 'order_id', 'received_at'];
const actual = sheet.getRange(1, 1, 1, 4).getValues()[0];
if (JSON.stringify(actual) !== JSON.stringify(headers)) {
throw new Error('unexpected headers');
}
const count = sheet.getLastRow() - 1;
const keys = count > 0
? sheet.getRange(2, 1, count, 1).getValues().map(r => r[0])
: [];
if (keys.includes(key)) return {status: 'already_recorded', key};
sheet.appendRow([key, source, orderId, new Date()]);
SpreadsheetApp.flush();
return {status: 'recorded', key};
} finally {
lock.releaseLock();
}
}
function checkedId(value, field) {
if (typeof value !== 'string') throw new Error(field + ': string required');
const text = value.trim();
if (!/^[A-Za-z0-9_-]{1,100}$/.test(text)) {
throw new Error(field + ': invalid ID');
}
return text;
}
手動確認用の呼び出し例です。orderId は数値でなく文字列にし、先頭の0を維持します。受注元・注文IDは半角英数字、ハイフン、アンダースコアの1〜100文字に限る例です。実際のID形式が違う場合は、その仕様を確認して検証条件を変更します。
function manualExample() {
console.log(appendOrderOnce({source: 'shop_a', orderId: '000123'}));
}
1回目は recorded、同じ値の2回目は already_recorded を返す設計です。キーが既存行に残るため、追記後に後続の処理が失敗しても、再実行では既存キーを確認できます。ロック取得に失敗した場合はエラーにし、成功として進めません。
今回確認したことと、まだ確認していないこと
掲載コードをNode.jsの模擬シート・模擬ロックで実行し、重複、別受注元、先頭0、入力不備、ロック失敗、ヘッダー不一致、追記後の応答失敗からの再実行を確認しました。これは業務条件と分岐のローカル検証です。実際のGoogleスプレッドシートでのAPI動作、同時実行、メール連携、処理件数の上限を検証した結果ではありません。
| 検証用データ・状態 | 期待結果 |
|---|---|
| shop_a / 000123 を2回 | 同じキーの行は1件 |
| shop_b / 000123 を追加 | 受注元が違うため別の行 |
| shop_a / 123 を追加 | 000123とは別のIDとして扱う |
| IDが空、数値、許可外文字 | 登録前にエラー |
| ロックが取れない | 登録せずエラー |
| 列名が想定と違う | 登録せずエラー |
| 追記後に応答が失敗し、同じ入力を再実行 | 残ったキーを見て再追記しない |
このコードで保証できる範囲を決める
スクリプトのロックは、別プロジェクトのGASや人の直接編集、他のAPI経由の書き込みまでは止めません。複数の入口から書く場合は、共通のデータベースの一意制約など、全ての入口を含む設計を検討します。また、この例は毎回キー列を読むため、大量データ向けの実装ではありません。
スプレッドシートと外部通知を一括で確定する仕組みもありません。行を保存した後にメール通知が失敗したなら、登録済みかどうかと通知済みかどうかを別に記録する必要があります。「同じキーの再追記を防ぐ」と「全処理が必ず1回だけ完了する」は同じ保証ではありません。
自動化の企画では、成功例に再実行の列を足す
n8nやGASの実践記事では、連携後に何ができるかが分かる画面や構成図が参考になります。ただ、作る際には、正常な1件に加えて「同じ入力がもう一度来たら」「保存は成功したが通知が失敗したら」を書き出してみてください。実行回数が増えても業務データが壊れないか、担当者が確認できるようになります。
参考資料・確認範囲
Google:LockService — スクリプト単位などの排他制御の公式資料。2026年11月2日確認。
Google:SpreadsheetApp.flush — 保留中のスプレッドシート変更を適用するAPI。複数サービス間のトランザクションを保証するものではありません。
Qiita:GASによる受注転記の設計記事 — 企画時の関連資料。本文で訂正された未検証の顧客実績は本稿の根拠に使用していません。
Zenn:n8nを用いたワークフローの実践 — 連携の構成・運用を示す記事の比較対象。本稿のコードは独自の小さな検証例です。


コメント