「さっき更新したはずのデータが消えてるんだけど…」
管理画面で商品情報を編集して保存した。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
- psqlとは?PostgreSQLをコマンドラインで操作する基本コマンドと実践的な使い方 – 楽観ロックのSQLをpsqlで試す際に必要なコマンド操作
- 無料で使えるPostgreSQLサービス8選を比較!2025年版おすすめ – 楽観ロックの検証環境を無料で用意したい場合の選択肢
- 「全部POSTでよくない?」がモヤる人へ|HTTPメソッド使い分けの正論と現場のリアル – 更新API(PUT/PATCH)の設計と楽観ロックの組み合わせ方
開発効率化
- Claude Codeとは?AI搭載のコーディングアシスタントを徹底解説 – 楽観ロックの実装コードをAIに生成してもらう活用法
- リリース前に必ず確認!バイブコーディング&非エンジニア向けWebアプリ安全チェックリスト – 同時編集テストを含むリリース前の品質チェック
まとめ:「後勝ちで消える」を1カラムで防ぐ
本記事のポイントを整理します。
- 楽観ロックとは、ロックを取らずに処理を進め、保存時にデータが変わっていないかチェックする仕組み
- バージョン番号方式が定番。テーブルに
versionカラムを追加し、UPDATE文のWHERE句に含める - 影響行数が0なら競合。「他のユーザーが更新しました」とエラーを返す
- 悲観ロックとの違い:楽観ロックは他のユーザーをブロックしない。Webアプリの大半は楽観ロックで十分
- フロントは
versionを受け取って送り返すだけ。ロジックの本体はバックエンド
楽観ロックは、テーブルにversionカラムを1つ足して、UPDATE文のWHEREに条件を1つ加えるだけ。実装コストは最小限なのに、「さっき更新したのに消えた」というユーザーの絶望を防げます。管理画面やCMSを作っているなら、迷わず入れましょう。
