今回は、Springが提供しているDataSourceTransactionManager
をデフォルトの状態で使用していると、Commitフェーズで何かしらのエラーが発生するとCommitされる可能性がある・・・という話を紹介します。
Commitフェーズでエラーが起こる可能性はかなり少ないとは思いますが、発生してしまった時の衝撃を考えるとけっして見逃せません・・・
Note:
ちなみに・・・JTA(
JtaTransactionManager
)使用時の動作は本エントリーでは検証できていません。2016/12/10 12:20 追記
Spring BootがサポートしているOSSのJTA実装(Atomikos, Bitronix, Narayana)+PostgreSQLで試してみたところ・・・・
Commitはされませんでしたが・・・コネプションプールを使っていると、プールに戻ったコネクション(エラーが発生したコネクション)を別の処理で使った時に逆にCommitできませんでした(ある意味想定内の動きかも!?)・・・
この事象も、本エントリーで紹介している方法を適用することで解消することができました
なお、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
(自動コミットする)になっている orfalse
(自動コミットしない)になっていてもコネクションプールを使っている
Note:
DBにOracleを使用している場合は、自動コミットフラグの値に関係なくCommitされちゃいます。
これは、OracleのJDBCドライバが、自動コミットフラグがfalse
の時にコネクションのcommit
またはrollback
メソッドを明示的に呼び出さずにコネクションのclose
メソッドを呼び出すと、コネクションを暗黙Commitする仕様 になっているためです。
なぜCommitされるのか?
DataSourceTransactionManager
のデフォルト実装だと・・・Commitフェーズでエラーが発生した場合に、コネクションのrollback
メソッドを呼び出さない仕様になっているため、未確定の操作がコネクション(セッション上)に残ることになります。
この状態で・・・
自動コミットフラグが変更されるとCommitされます。これはJDBCの仕様として明示されており、DataSourceTransactionManager
のdoCleanupAfterCompletion
メソッドの中で自動コミットフラグを変更する処理があります。この処理は、コネクションの自動コミットフラグのデフォルト値がtrue
(自動コミットする)になっていると実行されます。
// 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されてしまうのです・・・
Note:
エラー理由によっては未確定状態の操作を保持したコネクションがコネクションプールに戻らない可能性もありますし、利用するJDBCドライバやコネクションプールの実装にも依存する部分なので、絶対にCommitされるというわけではないことを補足しておきます。(念のため・・・)
じゃ〜どうすればいいの?
DataSourceTransactionManager
(正確にはAbstractPlatformTransactionManager
)に実装されているsetRollbackOnCommitFailure
メソッドを呼び出して、コミットフェーズでエラーが発生した時にロールバック処理を呼び出すオプションを有効化しましょう。
こうすることで、コミットフェーズでエラーが発生した際にコネクションのrollback
メソッドが呼び出されるため、未確定の操作がCommitされることを防ぐことができます
(コネクションのrollback
メソッドでエラーが発生した場合はどうなるんだろ・・・エラー理由によって動作がかわると思うので本エントリーではいったん無視しよう)
@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で取り込んでくれそうです
まとめ
「DBとしてOracleを使う」 or 「コネクションプールを使う」アプリケーションでは、setRollbackOnCommitFailure(true)
の指定は必須といっていいでしょう。
Oracle以外のDB+コネクションプールを使わないアプリケーション(単発のバッチアプリケーションとか?)の場合は、自動コミットフラグのデフォルト値をfalse
(自動コミットしない)にしておけばCommitされることはありませんが、一律指定しちゃってよい気がしています。
参考サイト
-
「Spring Bootとmybatisでトランザクションタイムアウトが効かない」の「暗黙コミットについて」
-
https://docs.oracle.com/cd/E49329_01/java.121/b71308/getsta.htm#i1019153
-
https://docs.oracle.com/javase/jp/8/docs/api/java/sql/Connection.html#setAutoCommit-boolean-