Webエンジニア向けプログラミング解説動画をYouTubeで配信中!
▶ チャンネル登録はこちら

【ITニュース解説】Why Two Tabs Silently Overwrote Each Other: Field Notes on Lost Updates, Version Columns, and ETags

2026年10月31日に「Dev.to」が公開したITニュース「Why Two Tabs Silently Overwrote Each Other: Field Notes on Lost Updates, Version Columns, and ETags」について初心者にもわかりやすく解説しています。

作成日: 更新日:

ITニュース概要

同じデータを複数人で編集すると、前の変更が後から上書きされる「失われた更新」が起こる。Webアプリでは、これはデータベースのトランザクションでは防げない。データのバージョン番号を使い、更新時にバージョンが一致するか確認する「オプティミスティック並行制御」で解決できる。これにより、サイレントなデータ損失を防ぎ、ユーザーに衝突を伝えられる。

ITニュース解説

システム開発において、複数のユーザーが同じデータにアクセスし、同時に変更しようとすると、思いがけない問題が発生することがある。その典型的な例が「ロストアップデート」と呼ばれる現象だ。これは、二人のユーザーが同じデータ(例えば商品情報)を読み込み、それぞれが内容を変更して保存した際に、後から保存したユーザーの変更が、先に保存したユーザーの変更を気づかないうちに上書きしてしまうことを指す。結果として、先に変更した内容が「失われる(ロストする)」ことになるため、この名前が付いている。

この問題は、特にウェブアプリケーションで頻繁に発生する。ある管理画面で商品の価格をAさんが変更し、同じく商品の説明をBさんが変更したとしよう。Aさんが価格を変更して保存し、その直後にBさんが説明を変更して保存したとする。もし適切な対策が施されていない場合、Bさんの保存処理がAさんの価格変更を消し去ってしまう可能性があるのだ。なぜなら、Bさんが読み込んだデータは、Aさんが価格を変更する前の古いデータだからだ。Bさんが保存する際には、自分が読み込んだ古い価格情報も含めて全てのフィールドをサーバーに送ってしまうため、結果としてAさんの価格変更が失われてしまう。このとき、システム側ではエラーも警告も表示されず、データが静かに失われるため、発見が非常に困難になることがある。

このようなデータ損失は、一般的なデータベースのトランザクション機能だけでは防ぐことができない。データベーストランザクションは、複数の操作を一つのまとまりとして扱い、その全体が成功するか、あるいは全体が失敗するかを保証する仕組みだ。しかし、ウェブアプリケーションの場合、ユーザーがウェブページを開いてデータを読み込む操作と、フォームに情報を入力して保存する操作は、通常、別々のHTTPリクエストとしてサーバーに送られる。つまり、データを読み込むときに一度データベースにアクセスし、データを保存するときに再度データベースにアクセスする、という流れになる。この二つの操作は、データベースにとっては全く独立した二つのトランザクションとして扱われる。たとえデータベースの隔離レベルを最高レベルに設定しても、この二つのトランザクションは時間的に重なっていないため、ロストアップデートを防ぐことはできないのだ。

では、どのようにしてこのロストアップデートを防ぐのだろうか。その解決策の一つが「楽観的並行性制御(Optimistic Concurrency Control, OCC)」と呼ばれる手法である。これは、複数のユーザーが同時にデータを更新しようとしても、お互いをブロックすることなく処理を進め、書き込み時にデータの競合を検出して対処するという考え方だ。

具体的には、データベースのテーブルに「バージョンカラム」という新しい列を追加することから始める。このカラムは通常、整数型で、データのバージョンを表すために使われる。例えば、新しく作成されたデータのバージョンを「1」とし、データが更新されるたびにこのバージョン番号を「1」ずつ増やしていく。

データの更新処理は次のように実装する。まず、ユーザーがウェブページを開いてデータを読み込む際、そのデータの現在のバージョン番号も一緒に読み込んでおく。そして、ユーザーがデータを編集し、保存ボタンを押したとき、更新リクエストには「最初に読み込んだときのバージョン番号」を含めてサーバーに送信する。サーバー側では、データベースのUPDATE文を発行する際に、WHERE句にデータのIDだけでなく、「クライアントから送られてきたバージョン番号」も指定する。さらに、更新が成功した場合には、データベース内のバージョンカラムの値を一つ増やすように指定する。

もし、他のユーザーが先に同じデータを更新しており、データベースのバージョン番号が、クライアントから送られてきたバージョン番号よりも進んでいる場合、WHERE句の条件を満たす行は一つもなくなる。この結果、UPDATE文はどの行も更新しない。サーバーは、更新された行数がゼロだったことを検出し、これを「競合が発生した」と判断する。この場合、サーバーはエラーをクライアントに返し、クライアントはユーザーに競合が発生したことを伝えることができる。

この楽観的並行性制御は、HTTPの仕組みとも相性が良い。HTTPには、リソースの特定のバージョンを識別するための「ETag(Entity Tag)」というレスポンスヘッダと、特定のETagを持つリソースの場合のみリクエストを処理する「If-Match」というリクエストヘッダが標準で定義されている。サーバーは、データのGETリクエストに応答する際に、バージョンカラムの値をETagヘッダとしてクライアントに送る。クライアントは、そのETagを保存しておき、後でデータを更新するPUTやPATCHリクエストを送る際に、そのETagをIf-Matchヘッダに含めて送り返す。

サーバーは、受け取ったIf-Matchヘッダの値と、現在のデータベースのバージョン番号を比較する。もし両者が一致しない場合、つまり他の誰かが先にデータを更新してバージョンが進んでいる場合、サーバーは「412 Precondition Failed(前提条件失敗)」というHTTPステータスコードを返して、リクエストを拒否する。また、悪意のあるクライアントや古いクライアントが誤ってIf-Matchヘッダを送らずに更新しようとした場合を防ぐため、「428 Precondition Required(前提条件が必要)」というステータスコードを返すことで、必ずIf-Matchヘッダを要求するように強制することもできる。もし、バージョン情報がHTTPヘッダではなくリクエストボディに含まれて送られてきた場合は、「409 Conflict(競合)」というステータスコードを返すのが適切だ。成功した場合は、サーバーは更新された新しいバージョン番号をETagヘッダとしてクライアントに返し、クライアントはそれを保持して次の更新に備える。

ウェブフォームの場合、カスタムHTTPヘッダを設定できないことがあるため、バージョン情報は隠しフォームフィールドとして渡すのが一般的だ。サーバー側で競合を検出した場合、ユーザーインターフェースは単にエラーメッセージを表示するのではなく、「他のユーザーがこの商品を更新しました。変更点を確認してください。」といった具体的な情報を提示し、ユーザーに現在の最新データの内容と自分の変更内容を比較させて、どちらを採用するか(自分の変更を破棄して最新データを再読み込みするか、最新の変更を上書きして自分の変更を優先するか)を明示的に選択させるべきである。システムが自動でリトライしたり、強制的に上書きしたりする挙動は、結局はデータ損失を招く「ラストライトウィンズ」と同じ結果になりかねないため、避けるべきだ。

楽観的並行性制御以外にも、データの同時更新を扱う戦略はいくつか存在する。例えば、「ラストライトウィンズ」は、先ほど説明したように、ただ単に最後に保存された変更を適用するもので、データ損失のリスクが高い。これは個人の設定データなど、同時に編集される可能性が低い場面で使われることがある。また、「アトミックな更新」は、データベースの機能を使って「在庫を1減らす」といった特定の操作を一つのSQL文で安全に行うもので、カウンターや在庫管理に適している。一方、「悲観的ロック」は、データを読み込む際に他のユーザーがそのデータを変更できないようにロックをかける方法だが、これはロックが解除されるまで他の操作をブロックするため、ユーザーがフォーム入力に時間をかけるようなウェブアプリケーションでは、パフォーマンスの低下やデッドロックの原因となるため、短いサーバーサイドの処理に限られる。

ほとんどの複雑なウェブアプリケーションでは、これらの並行性制御戦略を組み合わせて使用することになる。カウンターのような単純な値にはアトミックな更新を、ユーザーが編集するほとんどのフォームには楽観的並行性制御を、そしてごく一部の短時間で完結するサーバー側の処理には悲観的ロックを適用するなど、データの特性やユースケースに応じて最適な方法を選択することが重要である。楽観的並行性制御は、ユーザーの思考時間とデータ保存の間に生じる「読み込み→変更→書き込み」という一連の処理の中で、データ損失を防ぐための非常に強力な手段となる。これは、単にデータを更新するだけでなく、データが安全に、そして意図通りに変更されることを保証するための基盤となる技術の一つだ。

この仕組みは、データの更新だけでなく、削除操作にも適用できる。あるユーザーが古い情報を基にデータを削除しようとした際、そのデータがすでに別のユーザーによって更新されていた場合、削除を拒否することで、古い情報に基づいた意図しない削除を防ぐことができる。また、複数のユーザーがリアルタイムで同じドキュメントを同時に編集するような高度なコラボレーション機能が必要な場合は、CRDT(Conflict-free Replicated Data Types)のような、競合を自動的に解決してデータをマージする技術が必要になるが、一般的なフォームベースのアプリケーションであれば、楽観的並行性制御が実用的な解決策となるだろう。

関連コンテンツ

関連IT用語

関連ITニュース