事象
コードは何も変えていないのに
hibernate3 で insert してから update すると突然下記のエラーが発生するようになりました。
org.hibernate.StaleObjectStateException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect)
環境は下記のいずれの組み合わせでも発生。
Java6, Java8
MySQL 5.5, MySQL 5.6, MySQL 5.7
そんな古い Version を使うなよ。という突っ込みはさておき原因を調べてみました。
原因
数年ぶりに MySQL Connector/J を最新に上げたことにより発生するようになっていました。
エラーになる流れとしては
-
hibernate3 で insert をする。
このとき、Java ではミリ秒を保持し、DB ではミリ秒は四捨五入される。 -
その後、同じ entity で update する。
Java ではミリ秒を保持しており、update 時にミリ秒も評価するので楽観ロックエラーになる。
どの version からエラーになるのか調べると
MySQL Connector/J 5.1.23 からエラーになるようです。
Release Notes を見てみました。
この変更が影響してそうな気がします
The nativeSQL() method would always truncate fractional seconds rather than preserving the fractional part in the output string. Now Connector/J checks the server version: it preserves the fractional part for MySQL 5.6.4 and greater, and truncates the fractional part for older versions. (Bug #14748459, Bug #60598)
日本語訳
nativeSQL() メソッドは、出力文字列の端数部分を保存するのではなく、常に端数秒を切り捨てていました。現在 Connector/J はサーバ バージョンをチェックし、MySQL 5.6.4 以降では端数部分を保存し、それ以前のバージョンでは端数部分を切り捨てます。
MySQL Connector/J 5.1.22 以前だと update 時にはミリ秒以下を評価の対象にしていなかったためエラーにならなかったと思われます。(nativeSQL()が何かはよくわかってません・・)
ただ、謎なのは動作環境によってエラーになったりならなかったりしていて、 MySQL やサーバの version や設定の違いが関係しているのかもしれませんが、時間と気力の制約上これ以上の深追いはやめておきます。。