同じTODOを同時編集したら?share&doで学ぶ楽観ロックと更新競合
楽観ロックがあれば、同時編集はすべて安全なのでしょうか。share&doのローカルAPIで古い版の保存を試し、バージョン比較・画面の同期・競合後の操作を図と実行結果で整理します。
CONTENTSこの記事の目次7項目
share&doの同じTODOを、二つの画面で開いて編集したら、どちらの内容が残るのでしょうか。「15秒ごとに同期するから大丈夫」「楽観ロックがあるから大丈夫」と説明しても、保存までの流れを追わないと答えは出ません。今回は架空の買い物TODOを使い、手元のAPIで古い版の保存を確かめました。
結果は、古い版番号の更新は拒否され、先に保存した変更が残りました。ただし、最新の版番号を付けて再送すると保存されます。版番号の整合性と、利用者が変更内容に納得しているかは、別々に考える必要があります。
Aは牛乳2本、Bは豆乳。どちらが残る?
まずは仮想例です。AとBが「牛乳を買う」という同じTODOを読み込みます。Aは「牛乳を2本買う」、Bは「豆乳を買う」に書き換えました。元の状態を確認せず順番に保存する仕組みなら、Bの保存でAの変更が失われることがあります。これが更新の消失、いわゆるロストアップデートです。
楽観ロックは、編集を始めた人がずっとデータを独占する代わりに、保存するときに「読んだあとで変更されていないか」を確かめる考え方です。ここではデータの版を表すversionを使います。同時編集とは、保存ボタンを同じ瞬間に押す場合だけではなく、編集している時間が重なる場合も含みます。
share&doでは、どこで版を比べている?
画面はTODOの更新リクエストにversionを付けます。サーバーは保存済みのTODOを取得し、受け取った版と一致するか比較します。すでにAの保存が終わっていれば、Bの古い版はここで拒否されます。
// TodoService.kt:受け取った版を比較
if (item.version != request.version) {
throw TodoConflictException()
}
// TodoModels.kt:永続化時の版管理
@Version
@Column(nullable = false)
var version: Long = 0ただし、if文だけでは十分ではありません。二つの処理が同じ版を読み、両方が比較を通過してから保存する可能性があるためです。TODOにはJPAの@Versionもあり、HibernateがDBへの書き込み時に版を検証します。リクエストの古さを調べる比較と、トランザクション同士の競合を調べる仕組みは、守る時間の範囲が違います。
また、完了切り替えや並べ替えなどでは、リストにPESSIMISTIC_WRITEという悲観ロックも使っています。DBの処理中に対象をロックするもので、利用者が編集画面を開いている間ずっと押さえるわけではありません。タイトル・備考だけの更新もすべてこのロックを通る、と説明すると不正確です。
古い版の保存を、ローカルAPIで確かめる
2026年9月12日、ローカルのshare&do APIとPostgreSQLで検証しました。参照コードは2f75234、ビルド設定はSpring Boot 4.1.1、解決されたHibernateは7.4.5.Final、Java 21です。本番のデータは使わず、検証専用のゲストと架空のTODOを作成しました。
- 「牛乳を買う」というTODOを作り、A・Bに見立てて2回取得する。どちらもversion 0であることを確認する。
- Aとして「牛乳を2本買う」をversion 0で送信する。期待値はHTTP 200、保存後の版は1。
- Bとして「豆乳を買う」を古いversion 0のまま送信する。期待値はHTTP 409とTODO_CONFLICT。
- もう一度取得し、タイトルが「牛乳を2本買う」のままであることを確認する。
- 比較実験として、最新のversion 1を付けて「豆乳を買う」を送信する。これはHTTP 200で保存される。実運用で無条件に再送するための手順ではない。
結果はすべて期待どおりでした。APIの呼び出しは順番に行い、Bが持つ版を固定しています。ブラウザーを2枚開いての同時操作ではなく、15秒ごとの再取得やフォーカス復帰の影響を除いた検証です。
既存のTodoServiceTestsも実行し、26件すべて成功しました。そのうち「item update rejects a stale version before saving」は、保存済みの版が3、リクエストが2なら例外になり、保存処理が呼ばれないことを確認する単体テストです。リポジトリーをモックに置き換えているため、これだけでDB上の同時トランザクションまで検証したことにはなりません。
競合したら、入力と最新の内容を見比べる
今回のAPI検証では、409とともに「ほかの変更が反映されています。最新の内容を読み込み直しました。」というメッセージが返りました。ただし、サーバーが返すこの文言だけでは、画面側の再取得が成功したことまで保証しません。
画面側のコードでは、409を受けたらリストを再取得し、エラーメッセージを設定します。タイトル・備考の編集が失敗した場合、詳細画面を閉じる処理は走らず、入力値を消す処理もありません。一方、「編集をやめる」を押すと、その時点のリストの内容に戻す作りです。この通知の見え方や入力保持は、今回、実際の競合画面では再現確認していません。
- 保存に失敗したら、再送を繰り返す前に、残したい入力を手元へ控える。
- 「編集をやめる」で最新の内容を見直す。取得エラーも出ている場合は、通信が回復してから再読み込みする。
- 先に保存された変更と自分の意図を比較し、必要な内容を改めて入力する。「牛乳2本と豆乳の両方が必要か」など、内容の判断は版番号では決められない。
画面の同期と、保存の競合防止は別の仕事
share&doは、画面が表示されているときの15秒間隔と、ウィンドウへのフォーカス復帰時にリストを再取得します。これは画面を新しい状態へ近づける仕組みです。ポーリングをWebSocketなどの通知方式へ替えても、通知の前後に別の保存が入る可能性は残るため、保存時の確認が不要になるわけではありません。
ここで、編集フォームの基準となる版にも注意が必要です。share&doのコードでは、タイトル・備考の入力値を別に持つ一方、保存に付けるversionは再取得されたリスト内のTODOから取り出します。編集中に再取得が入ると、入力の基準は古いまま、版番号だけ新しくなる可能性があります。
API実験の最後に最新の版で再送できたのは、サーバーが「その内容を見て判断し直したか」までは判別できないからです。したがって、「二つの画面で編集すれば必ず409になる」とは言えません。実際の二画面での発生条件は未検証ですが、編集開始時の版を保持することや、変更の差分を見せて確認を求めることは、別途考えるべき設計です。
「同時編集にはどう対応していますか?」への答え方
share&doについて、今回の確認範囲で答えるなら次のようになります。できている対策と、別途検証が必要な部分を分けると、実態が伝わります。
次に自分のアプリを確かめるときは、「古い版を送ったらどうなるか」に加えて、「編集中に再取得が入ったら何を送るか」まで見てみてください。保存APIだけでも、画面の同期だけでも終わらないところに、わかったつもりの、その先があります。
READ MOREshare&doの使い方を確認するリストの作成・共有・参加から、基本操作を確認できます。→READ MOREshare&doの開発記録を読む登録なしの入口や共有方法など、アプリ全体の設計判断はこちらにまとめています。→確認した資料と、今回の検証範囲
- Jakarta Persistence 3.2仕様、3.5節「Locking and Concurrency」:楽観ロック、版の検証、悲観ロックの定義。依存APIは3.2.0。
- Hibernate ORM 7.4 User Guide、11章「Locking」:@Versionによる版管理。参照ページは7.4.7.Final表記、検証環境の依存版は7.4.5.Final。
- share&doのTodoService.kt、TodoModels.kt、TodoRepositories.kt、ApiExceptionHandler.kt、TodoListWorkspace.tsx:保存時の比較、例外処理、画面の再取得と入力管理。
- 2026年9月12日確認。APIの順次実行と26件のサービス単体テストを実施。二画面の競合通知・入力保持、DBで同時に書き込む競合、通信断や削除を伴うケースは未検証。