Edited at

SpringのDataSourceTransactionManagerを使うとエラー時にCommitされる可能性あり!?

More than 1 year has passed since last update.

今回は、Springが提供しているDataSourceTransactionManagerをデフォルトの状態で使用していると、Commitフェーズで何かしらのエラーが発生するとCommitされる可能性がある・・・という話を紹介します。

Commitフェーズでエラーが起こる可能性はかなり少ないとは思いますが、発生してしまった時の衝撃を考えるとけっして見逃せません・・・:persevere:


Note:

ちなみに・・・JTA(JtaTransactionManager)使用時の動作は本エントリーでは検証できていません。

2016/12/10 12:20 追記

Spring BootがサポートしているOSSのJTA実装(Atomikos, Bitronix, Narayana)+PostgreSQLで試してみたところ・・・・

Commitはされませんでしたが・・・コネプションプールを使っていると、プールに戻ったコネクション(エラーが発生したコネクション)を別の処理で使った時に逆にCommitできませんでした(ある意味想定内の動きかも!?)・・・:fearful:

この事象も、本エントリーで紹介している方法を適用することで解消することができました :v:

なお、Java EEサーバが提供しているJTA実装を使った検証はできていません。(あしからず・・・)



動作検証に使用したDB/version


  • PostgreSQL 9.6

  • MySQL 8.0

  • Oracle 12c R1 Standard Edition

  • DB2 Express-C 10.5


Commitされる条件は?

以下の条件をみたすと、使用するDBの種類に関係なくCommitされる可能性があります。



  • DataSourceTransactionManagerをデフォルトの状態で使用している(Spring Bootが自動でコンフィギュレーションするDataSourceTransactionManagerを使うと、この条件に一致します)


    条件をみたしてしまうBean定義例

    @Bean
    
    DataSourceTransactionManager transactionManager(DataSource dataSource) {
    return new DataSourceTransactionManager(dataSource);
    }



  • コネクションの自動コミットフラグのデフォルト値がtrue(自動コミットする)になっている or false(自動コミットしない)になっていてもコネクションプールを使っている



Note:

DBにOracleを使用している場合は、自動コミットフラグの値に関係なくCommitされちゃいます。

これは、OracleのJDBCドライバが、自動コミットフラグがfalseの時にコネクションのcommitまたはrollbackメソッドを明示的に呼び出さずにコネクションのcloseメソッドを呼び出すと、コネクションを暗黙Commitする仕様 になっているためです。



なぜCommitされるのか?

DataSourceTransactionManagerのデフォルト実装だと・・・Commitフェーズでエラーが発生した場合に、コネクションのrollbackメソッドを呼び出さない仕様になっているため、未確定の操作がコネクション(セッション上)に残ることになります。

この状態で・・・

自動コミットフラグが変更されるとCommitされます。これはJDBCの仕様として明示されており、DataSourceTransactionManagerdoCleanupAfterCompletionメソッドの中で自動コミットフラグを変更する処理があります。この処理は、コネクションの自動コミットフラグのデフォルト値がtrue(自動コミットする)になっていると実行されます。


DataSourceTransactionManager#doCleanupAfterCompletion内の処理の抜粋

// Reset connection.

Connection con = txObject.getConnectionHolder().getConnection();
try {
if (txObject.isMustRestoreAutoCommit()) {
con.setAutoCommit(true); // このタイミングでCommitされる
}
DataSourceUtils.resetConnectionAfterTransaction(con, txObject.getPreviousIsolationLevel());
}
catch (Throwable ex) {
logger.debug("Could not reset JDBC Connection after transaction", ex);
}

では、コネクションの自動コミットフラグのデフォルト値がfalse(自動コミットしない)の時の動作はどうなるのでしょうか?

とりあえず、↑のメソッドでCommitされることはないのですが・・・コネクションプールを使っていると残念ながらCommitされてしまう可能性があります。(コネプションプールを使っていない場合は、Oracle以外はCommitされることはありません)

なぜか?というと・・・

コネクションプールを使っていると、未確定状態の操作を保持したコネクションがコネクションプールに戻る可能性があり、そのコネクションを全く別の処理が使ってCommitすることで、エラー発生時に行った未確定の操作(コミットしたくない操作)も一緒にCommitされてしまうのです・・・:scream:


Note:

エラー理由によっては未確定状態の操作を保持したコネクションがコネクションプールに戻らない可能性もありますし、利用するJDBCドライバやコネクションプールの実装にも依存する部分なので、絶対にCommitされるというわけではないことを補足しておきます。(念のため・・・:wink:



じゃ〜どうすればいいの?

DataSourceTransactionManager(正確にはAbstractPlatformTransactionManager)に実装されているsetRollbackOnCommitFailureメソッドを呼び出して、コミットフェーズでエラーが発生した時にロールバック処理を呼び出すオプションを有効化しましょう。

こうすることで、コミットフェーズでエラーが発生した際にコネクションのrollbackメソッドが呼び出されるため、未確定の操作がCommitされることを防ぐことができます :v:

(コネクションのrollbackメソッドでエラーが発生した場合はどうなるんだろ・・・エラー理由によって動作がかわると思うので本エントリーではいったん無視しよう:sweat_smile:


Commitフェーズでエラーが発生した際にrollbackを行うためのBean定義例

@Bean

DataSourceTransactionManager transactionManager(DataSource dataSource) {
DataSourceTransactionManager transactionManager = new DataSourceTransactionManager(dataSource);
transactionManager.setRollbackOnCommitFailure(true); // これを指定!!
return transactionManager;
}


Note:

まだマージされていませんが、プロパティファイル(or YAMLファイル)で指定できるようするためのPRをSpring Bootに送ってみました。今のところSpring Boot 1.5で取り込んでくれそうです :v:



まとめ

「DBとしてOracleを使う」 or 「コネクションプールを使う」アプリケーションでは、setRollbackOnCommitFailure(true)の指定は必須といっていいでしょう。

Oracle以外のDB+コネクションプールを使わないアプリケーション(単発のバッチアプリケーションとか?)の場合は、自動コミットフラグのデフォルト値をfalse(自動コミットしない)にしておけばCommitされることはありませんが、一律指定しちゃってよい気がしています。


参考サイト