hatenob

プログラムって分からないことだらけ

JPAでHibernateの更新時の楽観ロックエラーの実装を確認した

ちょっと調べものをしていたのでメモ。

調べたかったこと

JPAのEntityで楽観ロック(@Version)を使った場合、更新時にjavax.persistence.OptimisticLockExceptionになる条件。

調べかった理由

VersionチェックのためにSQL投げたりしてないよね?というのを確認したかった。
で、tcpdumpを見ると投げてないことは分かった。
となると更新件数で見てるんだろうなというのは容易に予想できるのだけれど、一応、どうやってるのか実装を見ておこう、と。

実装

意図的にエラーを起こしてスタックトレースから例外発生個所を特定。

Caused by: org.hibernate.StaleObjectStateException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect) : [com.github.chanko.jpa.Emp#100]
	at org.hibernate.persister.entity.AbstractEntityPersister.check(AbstractEntityPersister.java:2541) [hibernate-core-4.3.7.Final.jar:4.3.7.Final]
	at org.hibernate.persister.entity.AbstractEntityPersister.update(AbstractEntityPersister.java:3285) [hibernate-core-4.3.7.Final.jar:4.3.7.Final]
	at org.hibernate.persister.entity.AbstractEntityPersister.updateOrInsert(AbstractEntityPersister.java:3183) [hibernate-core-4.3.7.Final.jar:4.3.7.Final]
...

AbstractEntityPersister.checkで何かをやってるっぽいので見てみる。ちなみにcheckはその下のupdateの中で呼ばれているので一緒に呼出しタイミングも見てみる。

3285行目:update
    return check( session.getTransactionCoordinator().getJdbcCoordinator().getResultSetReturn().executeUpdate( update ), id, j, expectation, update );

2541行目:check
    try {
        expectation.verifyOutcome( rows, statement, -1 );
    }
    catch( StaleStateException e ) {

updateで、executeUpdateでUPDATE文を実行してその結果(更新件数)をcheckに渡しているのが分かる。
次のcheckでは、その件数をもとにverifyOutcomeというメソッドでチェックをかけていて、そこでエラーだったらStaleStateExcetptionが返ってくる模様。このcatchの中でStaleObjectStateExceptionが投げられているので、このメソッドの中で何かやってるっぽい。
というか、引数見たらだいたい「やっぱそうだよね」という感じですが一応続きをみてみます。

expectationの実装はExpectations$BasicExpectationのようです。

67行目:
    public final void verifyOutcome(int rowCount, PreparedStatement statement, int batchPosition) {
        rowCount = determineRowCount( rowCount, statement );
        if ( batchPosition < 0 ) {
            checkNonBatched( rowCount );
        }
        ...

94行目:
    private void checkNonBatched(int rowCount) {
        if ( expectedRowCount > rowCount ) {
            throw new StaleStateException(
                "Unexpected row count: " + rowCount + "; expected: " + expectedRowCount
            );
        }
        if ( expectedRowCount < rowCount ) {
            String msg = "Unexpected row count: " + rowCount + "; expected: " + expectedRowCount;
            throw new TooManyRowsAffectedException( msg, expectedRowCount, rowCount );
        }
    }

batchPositionは-1なので、checkNonBatchedに更新件数を渡しています。
checkNonBatchedでは、件数がexpectedRowCount以下であればStaleStateExceptionを投げています。
詳細は省きますが、このexpectedRowCountは単一テーブルを表すEntityの場合は1です。
ちなみに1より大きかったらTooManyRowsAffectedExceptionという別の例外が出るようです。

まとめ

予想通りというかやっぱそうだよねというか、更新件数が0件だったときに出る。
EntityManagerを使ってEntityクラスを介した更新なので、当然主キー(@Id)を持ってます。
更新はキー+Version項目で更新にいくので必ず1件更新されるはずで、更新結果が0件だったということは別の誰かが更新した(Versionが変わっていた)か、削除した(キーにヒットするレコードがなかった)か、というわけで例外が返されるということでした。