Skip to content

ハウツー: 楽観的並行制御(バージョンフィールド)

FT リファレンス: FT323 (NENE2-FT/optimisticlog) — PUT ボディにバージョンフィールドを持つドキュメント API、古いバージョンに 409、ロストアップデート防止、18 テスト / 34 アサーション PASS。

このガイドでは、HTTP ETag/If-Match ヘッダーの代替として、リクエストボディに version フィールドを渡す楽観的並行制御の実装方法を解説します。

スキーマ

sql
CREATE TABLE documents (
    id         INTEGER PRIMARY KEY AUTOINCREMENT,
    title      TEXT    NOT NULL,
    body       TEXT    NOT NULL DEFAULT '',
    version    INTEGER NOT NULL DEFAULT 1,
    updated_at TEXT    NOT NULL
);

エンドポイント

メソッドパス説明
POST/documents作成する(バージョンは 1 から始まる)
GET/documents一覧表示する
GET/documents/{id}バージョン付きで取得する
PUT/documents/{id}更新する(ボディにバージョン必須)

作成

php
POST /documents  {"title": "Hello", "body": "World"}
 201  {"id": 1, "title": "Hello", "version": 1}

バージョン付き更新

クライアントは現在の version を読み取り、PUT ボディに含めます:

php
// 読み取り
GET /documents/1
 200  {"id": 1, "title": "Hello", "version": 1}

// 正しいバージョンで更新
PUT /documents/1
{"title": "Updated", "body": "new body", "version": 1}
 200  {"id": 1, "title": "Updated", "version": 2}

バージョンは更新が成功するたびにインクリメントされます。

古いバージョン — 409 Conflict

php
// Alice と Bob の両方がバージョン 1 を読み取る
// Alice が先に更新 → バージョンが 2 になる
// Bob がバージョン 1 で更新しようとする → 拒否される
PUT /documents/1
{"title": "Bob's edit", "version": 1}
 409 Conflict  {"current_version": 2, "submitted_version": 1}

// Bob は再読み取りしてバージョン 2 を取得し、再試行する
PUT /documents/1
{"title": "Bob's edit", "version": 2}
 200  {"version": 3}

実装

php
private function update(ServerRequestInterface $request): ResponseInterface
{
    $body    = $this->parseBody($request);
    $version = $body['version'] ?? null;

    if (!is_int($version) || $version < 1) {
        return $this->json->create(['error' => 'version is required'], 422);
    }

    $doc = $this->repo->findById($id);
    if ($doc === null) {
        return $this->json->create(['error' => 'Not found'], 404);
    }

    if ($doc['version'] !== $version) {
        return $this->problems->create('conflict', 'Stale version', 409, [
            'current_version'   => $doc['version'],
            'submitted_version' => $version,
        ]);
    }

    $newVersion = $version + 1;
    // UPDATE documents SET ... WHERE id = ? AND version = ?
    $this->repo->update($id, $title, $newVersion, $now);

    return $this->json->create($updated);
}

UPDATE クエリの WHERE version = ? 句が並行書き込みに対するアトミックなガードです。

バージョン vs ETag

観点バージョンフィールド(このガイド)ETag / If-Match(optimistic-locking-etag.md 参照)
プロトコルボディフィールドHTTP ヘッダー
クライアント UXJSON の明示的な "version": NIf-Match: "vN" ヘッダー
409 ペイロードcurrent_version を返せる412 — ボディの標準なし
チェック欠落422(version なし)428(If-Match なし)

してはいけないこと

アンチパターンリスク
version フィールドなしの PUT を受け付けるロストアップデート: 最後の書き込みがサイレントに勝つ
古いバージョンに 200 を返す並行変更のサイレントな上書き
アプリケーションコードのみでバージョンをチェック(WHERE 句なし)読み取りと書き込みの間の競合状態
409 レスポンスに current_version を含めないクライアントは回復するために再 GET が必要; 高速な再試行のために含める

MIT ライセンスの下で公開されています。