画面で同じデータを開いた二人が、それぞれ変更を保存する場面を考えてみましょう。後から保存した内容で先の変更を気付かず上書きすると、更新が失われることがあります。楽観ロックは、読み込んだ時点の版番号を保存時に照合し、古い状態からの更新を検知する方法です。
この記事では、MySQLのテーブルにversion列を持たせる例で、条件付きUPDATE、更新件数の判定、MyBatisからの呼び出し、二つのセッションでの確認方法を説明します。実際のスキーマやトランザクション管理は利用環境に合わせてください。
楽観ロックで防ぐこと
読み込み時のデータが保存時にも有効かを確認し、古い状態からの更新を検知するのが楽観ロックです。対象行にversionを持たせ、読み込み時の値を更新要求まで保持します。保存時は主キーとそのversionをWHERE条件に含め、成功時にversionを進めます。
競合自体をなくす仕組みではありません。また、versionを条件に含めない書き込み経路があれば、その経路からの上書きは検知できません。対象データを更新するすべての処理で同じルールを守ります。
version列と条件付きUPDATEを用意する
次はInnoDBを使うテーブルの説明用例です。既存テーブルへ列を追加するときは、既存行の初期値やNULL可否を含むスキーマ変更手順を別途検討してください。
CREATE TABLE inventory (
id BIGINT PRIMARY KEY,
stock INT NOT NULL,
version BIGINT NOT NULL DEFAULT 0
) ENGINE=InnoDB;読み込み時のidとversionを保存処理まで渡し、両方を条件に指定します。成功時にはversionを増やします。
UPDATE inventory
SET stock = ?, version = version + 1
WHERE id = ? AND version = ?;主キーで1行を対象にする例では、更新件数が1なら更新成功、0なら条件に一致する行がなかったことを表します。version不一致だけでなく、対象行が削除されている可能性もあるため、必要なら再取得して状態を判定します。成功件数が1を超える場合は、主キーやSQL条件など想定外の状態を調べます。
更新件数の数え方は、MySQLサーバーだけでなく接続ドライバの仕様と設定にも左右されます。たとえばMySQL C APIでは通常、実際に変更した行数を返し、CLIENT_FOUND_ROWSを指定するとWHERE条件に一致した行数を返します。JavaのConnector/JにもuseAffectedRowsプロパティがあるため、利用中のドライバと設定に対応する公式資料を確認してください。この例ではversionを増やすため、一致した行の値が変化します。
MyBatis Mapperで更新件数を確認する
Mapperへ渡す値には、画面から受け取った新しい値と、読み込み時のversionを含めます。XML Mapperの最小例を示します。クラス名や名前空間はプロジェクトに合わせてください。
<update id="updateStock" parameterType="StockUpdate">
UPDATE inventory
SET stock = #{stock}, version = version + 1
WHERE id = #{id} AND version = #{version}
</update>MyBatisのupdateメソッドは影響行数を返すため、Mapperの戻り値をintにして呼び出し側で判定できます。
int updateStock(StockUpdate command);
int updated = inventoryMapper.updateStock(command);
if (updated == 0) {
// 対象なし、またはversion不一致。最新の状態を再確認する
throw new OptimisticLockingFailureException("データが他の処理で更新されています");
}
if (updated != 1) {
throw new IllegalStateException("更新件数を確認してください");
}例外の型と利用者への案内はアプリケーションに合わせてください。競合時に古い入力値をそのまま自動再送すると、利用者の意図しない変更を適用するおそれがあります。最新値を表示し、入力内容をどう扱うか選べる導線を検討します。
2つのセッションで競合を確かめる
テスト用データで二つの画面またはセッションが同じ行を読み込み、同じversionを持った状態を作ります。その後、次の順で更新します。
- AとBが、同じ行とversionを読み込む。
- Aがそのversionを条件に更新し、更新件数1を確認する。
- Bが最初に読み込んだ古いversionで更新し、更新件数0となることを確認する。
- B側で最新値を再取得し、入力内容の維持・破棄を利用者が判断できることを確認する。
トランザクションを開いたままにする場合、Aのコミット前にBの更新が待機するなど、実行順によって観察結果が変わることがあります。初期データ、SQLの実行順、コミットのタイミングをテスト記録に含めます。
トランザクションとは役割が異なる
version比較は読み込み時の版と現在の版を照合する処理です。トランザクションは、複数のデータベース操作をどの範囲で一体として扱うかを決めます。複数テーブルの更新などではトランザクション境界も設計しますが、トランザクションを使うだけで別リクエストが保持する古い入力値を検知できるとは限りません。
InnoDBのUPDATEはロック機構とともに動作します。楽観ロックのversion条件は、そのロックを置き換えるものではありません。ロックの範囲や待機の挙動は条件、インデックス、分離レベルなどに依存するため、利用環境の仕様も確認してください。

実装時のチェックポイント
- 読み込み時のversionを保存要求まで正しく渡しているか。
- すべての更新経路でversionを条件に含め、成功時に増やしているか。
- 対象なしとversion不一致を、要件上区別する必要があるか。
- MySQLドライバの設定と更新件数の意味を確認したか。
- 競合時に最新値を示し、再編集方法を利用者が選べるか。
- 複数更新のトランザクション範囲と、タイムアウト等の扱いを決めたか。
- 実際のMySQL・MyBatis構成でSQLと画面の挙動をテストしたか。
公式資料で仕様を確認する
ロックと分離レベルはInnoDBの検索条件などに依存します。MySQL公式資料ではUPDATEのaffected rowsの数え方に接続オプションが影響すると説明されています。MyBatis公式Java APIでは、updateがステートメントの影響行数を返すと説明されています。利用するサーバー・ドライバ・MyBatisの版に対応した資料を確認してください。
MySQL Reference Manual: InnoDB Locking / Transaction Isolation Levels / mysql_affected_rows() / Connector/J connection properties / MyBatis Java API
まとめ
楽観ロックでは、読み込み時のversionをUPDATE条件に含め、成功時にversionを進めます。MyBatisの更新件数を確認し、0件の場合の再取得・再編集方法を決めます。二つのセッションで競合を試し、すべての更新経路、トランザクション、接続設定が実装と合っているかを確認してください。


コメント