【GAS】スクリプトの同時実行エラーを防ぐ『排他制御』とは?LockServiceで安全なデータ管理を実現

基本解説

Google Apps Script(GAS)は、Googleスプレッドシートやドキュメントなど、Google Workspaceの様々なアプリケーションを連携・自動化できる強力なツールです。しかし、複数人が同時にGASのスクリプトを実行した際に、「変更が競合してデータが正しく保存されなかった」「処理がエラーになってしまった」という経験はありませんか?

特に、勤怠管理や在庫管理、案件管理など、重要なデータを扱うスプレッドシートでは、このようなデータの不整合は大きな問題につながりかねません。

この記事では、Google Apps Script(GAS)を使って、このようなスクリプトの同時実行によるトラブルを防ぐための「排他制御(はいたせいぎょ)」という仕組みと、その具体的な実装方法について、実用的なサンプルコードを交えながら解説します。

閲覧の前に

GASの基本的な使い方やスクリプトエディタの開き方については、別の記事で詳しく解説しています。もしGAS自体が初めてという方は、まずはこちらの記事からご覧いただくことをお勧めします。

データ破損の原因?「競合」と「排他制御」の基本

まずは、なぜスクリプトの同時実行で問題が起きるのか、そしてそれを防ぐ「排他制御」とは何かを理解しましょう。

「競合」が引き起こす問題

複数のユーザーがほぼ同時に同じデータを更新するスクリプトを実行すると、「競合」と呼ばれる状態が発生します。

例えば、スプレッドシートで管理している商品の在庫数を更新するシナリオを考えてみましょう。

  1. 現在の在庫は「10個」。
  2. Aさんが「3個購入」のボタンを押し、在庫を「7個」に更新する処理を開始。
  3. ほぼ同時に、Bさんが「5個購入」のボタンを押し、在庫を「5個」に更新する処理を開始。
  4. Aさんの処理がシートから「10」という数値を読み取る。
  5. Bさんの処理もシートから「10」という数値を読み取る。
  6. Aさんの処理が「10 – 3 = 7」を計算し、セルに「7」を書き込む。
  7. Bさんの処理が「10 – 5 = 5」を計算し、セルに「5」を書き込む。

この結果、最終的な在庫数は「5」になってしまいます。本来であれば、合計8個売れているので在庫は「2」になるべきですが、Bさんの処理がAさんの更新を上書きしてしまったため、データに不整合が起きてしまいました。これが「競合」によるデータ破損の一例です。

データを守る「排他制御」という考え方

このような事態を防ぐのが「排他制御(はいたせいぎょ)」です。

排他制御とは、「ある処理がデータにアクセスしている間、他の処理が同じデータにアクセスできないようにロック(鍵をかける)する仕組み」のことです。

先ほどの在庫管理の例に排他制御を導入すると、処理の流れは以下のようになります。

  1. Aさんがボタンを押し、処理を開始。まず在庫データにロックをかけます
  2. ほぼ同時にBさんがボタンを押しますが、Aさんが既にロックをかけているため、待機状態になります。
  3. Aさんの処理が完了し、在庫を「7」に更新。その後、ロックを解除します
  4. ロックが解除されたので、待機していたBさんの処理が開始されます。在庫データに再びロックをかけます
  5. Bさんの処理は、Aさんによって更新された後の「7」という数値を読み込み、「7 – 5 = 2」を計算してセルに書き込みます。
  6. Bさんの処理が完了し、ロックを解除します

これにより、最終的な在庫数は正しく「2」となり、データの整合性が保たれます。

GASで排他制御を実現する「LockService」

GASには、この排他制御を簡単に実装するためのLockServiceという便利な機能が用意されています。LockServiceを使うことで、スクリプトの実行中に特定の範囲でロックをかけることができます。

LockServiceには、ロックをかける範囲に応じて3種類のロックがあります。

種類ロックを取得するメソッドロックの範囲主な用途
スクリプトロックgetScriptLock()同一スクリプト内最も一般的。同じスクリプトの同時実行を防ぐ場合に利用します。
ユーザロックgetUserLock()同一ユーザー・同一スクリプト内同じユーザーが同じスクリプトを複数回(例:ボタン連打)実行するのを防ぎます。
ドキュメントロックgetDocumentLock()同一ドキュメント(ファイル)内スプレッドシートやドキュメントなど、ファイル単位でロックをかけます。

ほとんどの場合、getScriptLock() を使えば問題ありません。この記事でもgetScriptLock()を使用した方法を中心に解説します。

実践!勤怠打刻システムに排他制御を実装する

それでは、具体的な例として「スプレッドシートを使った勤怠打刻システム」に排他制御を実装してみましょう。

複数人が同じスプレッドシートを使って出退勤時刻を記録します。「打刻」ボタンを押すと、GASが実行され、打刻ログシートの最終行に「社員番号」「氏名」「打刻時刻」を追記します。

排他制御がない場合のコード(問題点あり)

まずは、排他制御を考慮していないコードを見てみましょう。

function recordTimestamp() {
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('打刻ログ');

  // 最終行を取得して、その次の行にデータを追記する
  const lastRow = sheet.getLastRow();
  const newRow = lastRow + 1;

  // 記録するデータ(ここでは例として固定値)
  const employeeId = '1001';
  const employeeName = '山田 太郎';
  const timestamp = new Date();

  sheet.getRange(newRow, 1).setValue(employeeId);
  sheet.getRange(newRow, 2).setValue(employeeName);
  sheet.getRange(newRow, 3).setValue(timestamp);
}

このコードは、複数人が同時に「打刻」ボタンを押すと、getLastRow()で同じ行番号を取得してしまい、片方のデータが上書きされてしまう可能性があります。

LockServiceで改善したコード

次に、LockServiceを使ってこの問題を解決したコードを紹介します。

function recordTimestampWithLock() {
  // スクリプト全体をロックする
  const lock = LockService.getScriptLock();

  try {
    // ロックの取得を試みる(最大待機時間:30秒)
    // 30秒以内にロックが取得できない場合はエラーを発生させる
    lock.waitLock(30000);

    const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('打刻ログ');

    // 最終行を取得して、その次の行にデータを追記する
    const lastRow = sheet.getLastRow();
    const newRow = lastRow + 1;

    // 記録するデータ(ここでは例として固定値)
    const employeeId = '1001';
    const employeeName = '山田 太郎';
    const timestamp = new Date();

    // データを書き込む
    sheet.getRange(newRow, 1).setValue(employeeId);
    sheet.getRange(newRow, 2).setValue(employeeName);
    sheet.getRange(newRow, 3).setValue(timestamp);

    // 変更を即時適用して、他のスクリプトが最新のデータを読み込めるようにする
    SpreadsheetApp.flush();

    // 処理が成功したことをユーザーに知らせる(任意)
    SpreadsheetApp.getUi().alert('打刻が完了しました。');

  } catch (e) {
    // ロックの取得に失敗した場合や、その他のエラーが発生した場合
    SpreadsheetApp.getUi().alert('処理が混み合っています。少し時間をおいてから再度お試しください。');

  } finally {
    // 処理が成功しても失敗しても、必ず最後にロックを解放する
    lock.releaseLock();
  }
}

コードのポイント解説

1. ロックの取得:LockService.getScriptLock()

const lock = LockService.getScriptLock();

まず、スクリプトロックを取得するためのオブジェクトを用意します。
「ロックを取得する」とは、言い換えると「これから処理を始めます」という宣言をして、他の人が同じ処理を始められないように”順番待ちの札”を取るようなイメージです。この宣言(ロック)がされている間、後から実行された処理は待機することになります。

2. ロックの待機:lock.waitLock(30000)

lock.waitLock(30000);

waitLock(ミリ秒)は、ロックが取得できるまで指定した時間待機するメソッドです。この例では30000ミリ秒(=30秒)に設定しています。

もし他のユーザーが処理中でロックをかけていた場合、この行で処理が一時停止し、ロックが解放されるのを待ちます。30秒待ってもロックが取得できない場合は、エラーを発生させて処理を中断します。これにより、無限に待ち続けることを防ぎます。

3. 確実な処理の実行:try…catch…finally

try {
  // ロックが必要な処理
} catch (e) {
  // エラーが発生した時の処理
} finally {
  // 必ず実行される処理
}

LockServiceを使う上で最も重要なのが、このtry…catch…finally構文です。

  • tryブロック: ロックをかけた後に行いたいメインの処理(データの読み書きなど)を記述します。
  • catchブロック: tryブロック内でエラーが発生した場合(waitLockのタイムアウトなど)の処理を記述します。エラーメッセージをユーザーに表示するなどの対応が考えられます。
  • finallyブロック: tryブロックの処理が成功しようと、失敗(catch)しようと、必ず最後に実行されるブロックです。

4. ロックの解放:lock.releaseLock()

finally {
  lock.releaseLock();
}

finallyブロックでロックを解放するのは、他のスクリプトの待ち時間を最小限に抑え、システムの処理効率を高めるための最も推奨される方法です。

ロックが必要な処理(クリティカルセクション)が終わり次第すぐにロックを解放すれば、たとえ後続の処理が残っていても、他のスクリプトが無駄に待機することを防げます。
finallyブロックは処理の成功・失敗にかかわらず必ず実行されるため、ここに解放処理を記述することで、最も早いタイミングで確実にロックを解放し、システム全体のパフォーマンスを向上させることができます。

5. 変更の即時反映:SpreadsheetApp.flush()

SpreadsheetApp.flush();

LockServiceを使用する際は、ロックを解放する直前にflush()メソッドを呼び出すことが推奨されます。
これは、setValue()などで行った変更をすぐにスプレッドシートに書き込むための命令です。これを実行することで、ロックを解放した直後に次のスクリプトが実行されても、確実に最新の状態のデータを読み込むことができ、よりデータの整合性を高めることができます。

まとめ

今回は、GASのLockServiceを使って、スクリプトの同時実行によるデータ破損を防ぐ「排他制御」について解説しました。

  • 排他制御は、複数人での同時編集によるデータの競合を防ぐための重要な仕組みです。
  • GASではLockServiceを使うことで、簡単に排他制御を実装できます。
  • 安全かつ効率的に利用するためには、try…catch…finally構文を使い、finallyブロックで確実にロックを早期解放することが不可欠です。

データの整合性を保ち、より安全で信頼性の高いシステムを構築することができます。
勤怠管理や在庫管理など、複数人で一つのスプレッドシートを更新する仕組みをGASで構築する際には、ぜひこの「排他制御」を導入してみてください。

※Googleサービスは、Google LLC の商標であり、この記事はGoogleによって承認されたり、Google と提携したりするものではありません。