開発現場で言われる『楽観ロック』とは? 同時編集でデータが壊れるのを防ぐ仕組みをコード付きで解説

目次

「さっき更新したはずのデータが消えてるんだけど…」

管理画面で商品情報を編集して保存した。5分後に確認したら、自分の変更が消えている。調べてみると、同じタイミングで別の担当者も同じ商品を編集していて、後から保存した方の内容で上書きされていた。いわゆる「後勝ち」問題です。

レビューで「ここ楽観ロック入れた方がいいよ」と指摘されて、「楽観ロック…?」となった経験はありませんか?本記事では、現場でよく使われる楽観ロックの仕組みを、悲観ロックとの違いからコード例まで解説します。

楽観ロックとは? ―「たぶん大丈夫」で進めて、ダメなら弾く

楽観ロック(Optimistic Locking)は、「同時に同じデータを編集する人は少ないだろう」と楽観的に考え、ロックせずに処理を進める。ただし、保存する瞬間に「自分が読み取った時点からデータが変わっていないか」をチェックし、変わっていたら保存を拒否する、という仕組みです。

【楽観ロックの流れ】

1. Aさんが商品を開く(バージョン: 1)
2. Bさんも同じ商品を開く(バージョン: 1)
3. Aさんが保存 → バージョン1のまま? → YES → 保存成功(バージョン: 2に更新)
4. Bさんが保存 → バージョン1のまま? → NO(もう2になってる)→ 保存拒否!
   → 「他の人が更新しています。最新データを確認してください」

「楽観」と呼ばれる理由は、競合が起きないことを前提にして、起きた時だけ対処するから。データの読み取り時にロックを取らないので、他のユーザーの操作をブロックしません。

悲観ロックとの違い

楽観ロックの対になるのが悲観ロック(Pessimistic Locking)です。「誰かが同じデータを触るかもしれない」と悲観的に考え、読み取りの時点でロックを取って、他の人が触れないようにする方式。

項目 楽観ロック 悲観ロック
考え方 競合は少ないだろう 競合するかもしれない
ロックのタイミング ロックしない(保存時にチェック) 読み取り時にロック
他のユーザーへの影響 ブロックしない ロック中は待たされる
競合時の動作 後から保存した側がエラー そもそも同時編集できない
実装方法 バージョン番号 or タイムスタンプ SELECT ... FOR UPDATE
向いている場面 Web管理画面、API、読み取りが多い処理 在庫引き当て、残高更新

現場で「楽観ロック入れて」と言われるのは、ほとんどがWeb管理画面やAPIのケース。複数ユーザーが同時にフォームを開いて編集・保存するシナリオで、後勝ちによるデータ消失を防ぐのが目的です。

実装パターン:バージョン番号方式

楽観ロックの最も一般的な実装は、テーブルにversionカラムを持たせる方式です。

テーブル定義

-- PostgreSQL
CREATE TABLE products (
  id         SERIAL PRIMARY KEY,
  name       VARCHAR(255) NOT NULL,
  price      INTEGER NOT NULL,
  version    INTEGER NOT NULL DEFAULT 1,  -- ← これが楽観ロック用
  updated_at TIMESTAMP DEFAULT NOW()
);

更新SQLのポイント

更新時にWHERE句にバージョン番号を含めるのがキモです。

-- 楽観ロック付きのUPDATE
UPDATE products
SET
  name = '新しい商品名',
  price = 2000,
  version = version + 1,      -- バージョンをインクリメント
  updated_at = NOW()
WHERE
  id = 1
  AND version = 3;             -- 読み取り時のバージョンと一致するか確認

-- 影響行数が0 → 誰かが先に更新した → 競合エラーとして扱う

version = 3の条件があるので、他の人が先に更新してversionが4になっていた場合、WHERE句にマッチせず影響行数が0になります。これを「競合が起きた」と判定します。

バックエンド実装(Node.js + PostgreSQL)

// 楽観ロック付きの更新処理
async function updateProduct(
  id: number,
  data: { name: string; price: number },
  currentVersion: number  // フロントから受け取った読み取り時のバージョン
) {
  const result = await db.query(
    `UPDATE products
     SET name = $1, price = $2, version = version + 1, updated_at = NOW()
     WHERE id = $3 AND version = $4`,
    [data.name, data.price, id, currentVersion]
  );

  // 影響行数が0 → 競合発生
  if (result.rowCount === 0) {
    throw new Error('他のユーザーがこのデータを更新しました。最新データを確認してください。');
  }

  return { success: true };
}

フロント側の対応

フロントは、データ取得時にversionを受け取り、更新リクエストに含めて送ります。

// フロントからのリクエスト例
const response = await fetch(`/api/products/${id}`, {
  method: 'PUT',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    name: '新しい商品名',
    price: 2000,
    version: 3,  // 取得時のバージョンをそのまま送る
  }),
});

if (!response.ok) {
  const error = await response.json();
  // 「他のユーザーが更新しました」のエラーメッセージを表示
  alert(error.message);
  // 最新データを再取得してフォームを更新
  await refetchProduct();
}

どっちを使う? 選び方のフローチャート

Q1. 同時に同じデータを編集する頻度は?
  → 低い(管理画面、CMS等)→ 楽観ロック
  → 高い ↓

Q2. 競合時に「やり直し」で許容できる?
  → YES → 楽観ロック
  → NO ↓

Q3. データの整合性が絶対に壊れてはいけない?
  → YES(在庫引き当て、残高更新)→ 悲観ロック
  → NO → 楽観ロック

迷ったらまず楽観ロックを選んでおけば大抵のケースでは問題ありません。悲観ロックは「ロック中に他のリクエストが待たされる」ためパフォーマンスに影響しやすく、本当に整合性が最優先の処理だけに使うのが定石です。

楽観ロックの理解を深める関連記事

楽観ロックの実装を理解したら、DB運用やAPI設計の知識もあわせて強化しましょう。

データベース・API

開発効率化

まとめ:「後勝ちで消える」を1カラムで防ぐ

本記事のポイントを整理します。

  • 楽観ロックとは、ロックを取らずに処理を進め、保存時にデータが変わっていないかチェックする仕組み
  • バージョン番号方式が定番。テーブルにversionカラムを追加し、UPDATE文のWHERE句に含める
  • 影響行数が0なら競合。「他のユーザーが更新しました」とエラーを返す
  • 悲観ロックとの違い:楽観ロックは他のユーザーをブロックしない。Webアプリの大半は楽観ロックで十分
  • フロントはversionを受け取って送り返すだけ。ロジックの本体はバックエンド

楽観ロックは、テーブルにversionカラムを1つ足して、UPDATE文のWHEREに条件を1つ加えるだけ。実装コストは最小限なのに、「さっき更新したのに消えた」というユーザーの絶望を防げます。管理画面やCMSを作っているなら、迷わず入れましょう。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次