305.8K Views
March 17, 23
スライド概要
Springでトランザクションを管理する際は @Transactional を使うことが多いと思います。しかし、このアノテーションによって何が起こっているのかはご存知でしょうか?このセッションでは、初級者向けのトランザクションとは何か・ @Transactional の基本的な使い方から、中上級者向けのSpringによるトランザクション管理の仕組みまで、徹底的に解説します。
Java、Spring、IntelliJ IDEA
質問は #jsug へ 詳解Springトランザクション 〜初級から上級まで〜 (株)クレディセゾン 多田真敏 2023年3月17日 Spring Fest 2023 1
質問は #jsug へ このセッションについて ▸ Springでのトランザクション管理の使い方、 およびその仕組みを解説します ▸ 対象者 ▸ Springを学習し始めて間もない方 ▸ 数年程度のSpringの経験があり、より深い知識を得たい方 ▸ 必要な前提知識 ▸ この資料レベルのSpringの基礎知識(一部内容が重複) ▸ この資料レベルのJDBCの基礎知識 2
質問は #jsug へ 自己紹介 ▸ 多田真敏 (@suke_masa) ▸ 社内システムの内製化+AWS化 ▸ JSUG&JJUGスタッフ ▸ Thymeleaf・Resilience4jの ドキュメント和訳 3
質問は #jsug へ 環境 ▸ JDK 17 ▸ Spring Framework 6.0 ▸ Spring Boot 3.0 将来のバージョンでは、 内容が異なる可能性があります 4
目次 質問は #jsug へ 経験が少ない方は まず[初級]だけ読めばOK! ① [初級] トランザクションとは ② [初級] 入門@Transactional ③ [中級] 発展@Transactional ④ [中級] トランザクションマネージャーとは ⑤ [上級] AOPの仕組み ⑥ [上級] ORマッパーとの連携 5
質問は #jsug へ 目次 ① [初級] トランザクションとは ② [初級] 入門@Transactional ③ [中級] 発展@Transactional ④ [中級] トランザクションマネージャーとは ⑤ [上級] AOPの仕組み ⑥ [上級] ORマッパーとの連携 6
質問は #jsug へ トランザクションとは? ▸ DBへの複数の処理を1つにまとめた単位 ▸ 例: 2つのUPDATE文を1つにまとめる ▸ 「全部実行される」か「一切実行されない」の どちらかになることが、 RDBMSによって保証される ▸ 「トランザクション 原子性」で検索 7
質問は #jsug へ 例: 口座Aから口座Bに1万円送金する ▸ 以下2つの処理で1つのトランザクションとなる ① Aの残高を (Aの現残高 - 1万円) に更新する A: 100,000 → 90,000 ② Bの残高を (Bの現残高 + 1万円) に更新する B: 200,000 → 210,000 8
質問は #jsug へ 途中で障害が発生すると・・・ ▸ データに矛盾が発生する ① Aの残高を (Aの現残高 - 1万円) に更新する 🔥 ② Bの残高を (Bの現残高 + 1万円) に更新する Aが損するだけになる😭 A: 100,000 → 90,000 B: 200,000 → 210,000 9
質問は #jsug へ ロールバック ▸ トランザクション中に障害があったら、 全処理を無かったことにする ③ Aの金額を元に戻す ① Aの残高を (Aの現残高 - 1万円) に更新する 🔥 ② Bの残高を (Bの現残高 + 1万円) に更新する A: 100,000 → 90,000 B: 200,000 → 210,000 ② 障害発生 10
質問は #jsug へ コミット ▸ トランザクション内の全処理が正常終了後、 追加・更新・削除を確定させる ① Aの残高を (Aの現残高 - 1万円) に更新する A: 100,000 → 90,000 ② Bの残高を (Bの現残高 + 1万円) に更新する B: 200,000 → 210,000 ③ コミット これ以降はロールバック不可 11
質問は #jsug へ この章のまとめ ▸ DBへの複数の処理を1つにまとめたもの =トランザクション ▸ 障害発生時に全処理を無かったことにする =ロールバック ▸ トランザクションの正常終了後に変更を確定する =コミット 12
質問は #jsug へ 目次 ① [初級] トランザクションとは ② [初級] 入門@Transactional ③ [中級] 発展@Transactional ④ [中級] トランザクションマネージャーとは ⑤ [上級] AOPの仕組み ⑥ [上級] ORマッパーとの連携 13
質問は #jsug へ トランザクション管理をベタに書くと・・・ ▸ 本来やりたいことが埋もれてしまう ▸ トランザクション管理のためのコードが 各ビジネスロジックに散らばってしまう public void method1() { // このコードはイメージです トランザクション tx = new トランザクション(); tx.開始(); try { db.更新1(); 本来やりたいことはコレだけ db.更新2(); tx.コミット(); } catch(RuntimeException e) { tx.ロールバック(); throw e; } } 14
質問は #jsug へ そこでAOP (Aspect Oriented Programming) ▸ @Transactionalを付けるだけで、 トランザクション処理が対象メソッドに割り込む ▸ トランザクション管理のコードが不要に ▸ 本来やりたいことのみ書けばよい @Transactional public void method1() { db.更新1(); 本来やりたいことだけ書けばいい! db.更新2(); } 15
質問は #jsug へ @Transactionalの効果 ▸ 付加したメソッドに、以下の処理が割り込まれる ▸ メソッド開始直前にトランザクション開始 ▸ メソッド正常終了直後にコミット ▸ メソッドで実行時例外が発生したらロールバック @Transactional public void method1() { 開始 db.更新1(); db.更新2(); コミットorロールバック } 16
質問は #jsug へ ロールバック対象の例外 この挙動を 変える方法は後述 ▸ 非チェック例外のみ対象 ▸ EJBの仕様と合わせたらしい ▸ Springで発生する例外は全て非チェック例外 Throwable Error XxxError Exception RuntimeException その他のException YyyException ZzzException ロールバック対象 17
質問は #jsug へ 注意点 ▸ AOPでの割り込み対象はBean ▸ Beanじゃないと割り込まれないので注意 ▸ 後述するプロキシを使っているため 18
質問は #jsug へ この章のまとめ ▸ @Transactionalをメソッドに付加すると、 以下の処理が割り込まれる ▸ メソッド開始直前にトランザクションの開始 ▸ メソッド正常終了直後にコミット ▸ メソッド内で実行時例外が発生したらロールバック ▸ ロールバック対象の例外は非チェック例外のみ ▸ 割り込み対象はBeanのみ 19
質問は #jsug へ 目次 ① [初級] トランザクションとは ② [初級] 入門@Transactional ③ [中級] 発展@Transactional ④ [中級] トランザクションマネージャーとは ⑤ [上級] AOPの仕組み ⑥ [上級] ORマッパーとの連携 20
質問は #jsug へ @Transactionalの主要要素 ▸ propagation ▸ readOnly ▸ rollbackFor ▸ noRollbackFor ▸ timeout ▸ isolation ▸ transactionManager 21
質問は #jsug へ propagation要素 ▸ @Transactionalメソッドから 別の@Transactionalメソッドを呼んだ際に、 同一トランザクションになるか否かの設定 @Service public class Service1 { Service2 service2; @Transactional(...) public void method1() { ... service2.method2(); ... } } T1 こっち側の設定! @Service public class Service2 { @Transactional(propagation = Propagation.XXXXX) public void method2() { ... } } T? 22
質問は #jsug へ REQUIRED(これがデフォルト値) ▸ 既にトランザクションが開始している場合は、 そのトランザクション内で実行する ▸ トランザクションがまだ開始されていない場合は、 新規にトランザクションを開始する @Service public class Service1 { @Transactional(...) public void method1() {} } @Service public class Service2 { @Transactional(propagation = Propagation.REQUIRED) public void method2() {} } T1 T1 23
質問は #jsug へ REQUIRES_NEW ▸ 既にトランザクションが開始している場合は、 新規にトランザクションを開始する ▸ トランザクションがまだ開始されていない場合も、 新規にトランザクションを開始する @Service public class Service1 { @Transactional(...) public void method1() {} } T1 @Service public class Service2 { @Transactional(propagation = Propagation.REQUIRES_NEW) public void method2() {} } T2 T1 24
質問は #jsug へ Propagation値一覧 値 説明 REQUIRED 既にトランザクションが開始している場合は、そのトランザクション内で実行する。 トランザクションがまだ開始されていない場合は、新規にトランザクションを開始する。 ほとんどの場合はこれでOK。 REQUIRES_NEW 既にトランザクションが開始している場合は、新規にトランザクションを開始する。 トランザクションがまだ開始されていない場合も、新規にトランザクションを開始する。 たまに使う。 SUPPORTS 既にトランザクションが開始している場合は、そのトランザクション内で実行する。 トランザクションがまだ開始されていない場合は、トランザクション無しで実行する。 NOT_SUPPORTED 既にトランザクションが開始している場合は、トランザクションを一時停止する。 トランザクションがまだ開始されていない場合は、トランザクション無しで実行する。 MANDATORY NESTED NEVER ほとんど使わない 既にトランザクションが開始している場合は、そのトランザクション内で実行する。 トランザクションがまだ開始されていない場合は、例外が発生する。 既にトランザクションが開始している場合は、そのトランザクション内で実行する (部分ロールバック可能)。 トランザクションがまだ開始されていない場合は、新規にトランザクションを開始する。 既にトランザクションが開始している場合は、例外が発生する。 トランザクションがまだ開始されていない場合は、トランザクション無しで実行する。 25
質問は #jsug へ readOnly要素 ▸ trueにすると、そのトランザクションは 読み取り専用になる ▸ INSERT・UPDATE・DELETEを実行すると例外 ▸ デフォルト値はfalse ▸ ただし、トランザクションマネージャーによっては この設定は無視されるので注意 26
質問は #jsug へ rollbackFor要素・noRollbackFor要素 ▸ rollbackFor ▸ 指定した例外およびそのサブクラスが発生したら、 それがチェック例外であってもロールバックされる ▸ noRollbackFor ▸ 指定した例外およびそのサブクラスが発生したら、 それが非チェック例外であってもロールバックされない ▸ どちらにも指定していない例外時の挙動は 変わらない 27
質問は #jsug へ timeout要素 ▸ トランザクションのタイムアウト時間を 秒単位で指定 ▸ デフォルト値は-1 ▸ 利用しているRDBのデフォルト時間でタイムアウト 28
質問は #jsug へ isolation要素 ▸ トランザクションの隔離レベルを指定 ▸ デフォルト値はIsolation.DEFAULT ▸ 利用しているRDBのデフォルト設定となる ※隔離レベルについてはRDBの専門書などで調べてください 29
質問は #jsug へ transactionManager要素 ▸ トランザクションマネージャーのBean IDを指定 ▸ トランザクションマネージャーのBeanが複数ある場合に 利用する ▸ デフォルト値は空文字 ▸ DIコンテナ内からTransactionManager型の Beanが取得される(複数あったら例外) 30
質問は #jsug へ この章のまとめ ▸ propagation ▸ 呼び出されたメソッドの トランザクションが ▸ timeout ▸ トランザクションの タイムアウト時間を指定 呼び出し元と同じになるかどうか ▸ readOnly ▸ trueにすると 更新系SQL発行時に例外 ▸ rollbackFor・noRollbackFor ▸ どの例外でロールバックされるか /されないかを指定 ▸ isolation ▸ トランザクションの 隔離レベルを指定 ▸ transactionManager ▸ トランザクションマネージャー のBean IDを指定 31
質問は #jsug へ 目次 ① [初級] トランザクションとは ② [初級] 入門@Transactional ③ [中級] 発展@Transactional ④ [中級] トランザクションマネージャーとは ⑤ [上級] AOPの仕組み ⑥ [上級] ORマッパーとの連携 32
質問は #jsug へ トランザクションマネージャーとは ▸ 実際にトランザクションの 開始・コミット・ロールバックを行う ▸ @Transactionalが付加されたメソッドへの AOP割り込み処理内で使われている ▸ 抽象化することで、どんなORマッパーでも 同じコードでトランザクション管理ができる 33
質問は #jsug へ トランザクションマネージャーの実体 ▸ TransactionManager実装クラス JDBC版 TransactionManager Platform Transaction Manager DataSource Transaction Manager Jdbc Transaction Manager Jpa Transaction Manager リアクティブ版 (今回は説明しない) Reactive Transaction Manager Jta Transaction Manager R2dbc Transaction Manager 34
質問は #jsug へ JdbcTransactionManagerクラス ▸ 現在、ほとんどのORマッパーで利用される トランザクションマネージャー ▸ Spring Framework 5.3で導入 ▸ それ以前は下記のDataSourceTransacitonManagerが よく使われていた ▸ PlatformTransactionManagerの実装クラス ▸ DataSourceTransactionManagerのサブクラス ▸ コミット/ロールバック時の例外変換が追加されている (SQLException→DataAccessException) 35
質問は #jsug へ PlatformTransactionManagerの利用 // トランザクションマネージャー PlatformTransactionManager txManager = ...; // トランザクションの各種設定を保持(propagationとか) DefaultTransactionDefinition txDef = ...; AOP内では こんな処理が 動いている // トランザクション開始 TransactionStatus txStatus = txManager.getTransaction(txDef); try { db.更新1(); db.更新2(); txManager.commit(txStatus); // コミット } catch (HogeException e) { txManager.rollback(txStatus); // ロールバック } ▸ 👆のコードはDIコンテナが無くても動く 36
質問は #jsug へ
TransactionTemplateの利用
PlatformTransactionManager txManager = ...;
DefaultTransactionDefinition txDef = ...;
TransactionTemplate txTemplate =
new TransactionTemplate(txManager, txDef);
// Lambda内で例外が発生したらロールバック
// そうでなければコミット
Hoge hoge = txTemplate.execute(status -> {
db.更新1();
db.更新2();
return new Hoge(); // returnした値は外側の変数で受け取れる
});
▸ コミット忘れ・ロールバック忘れが無いので便利
▸ @Transactionalを使わない場合はこれがおすすめ
37
質問は #jsug へ AOPでトランザクション管理するには ▸ TransactionManagerのBean定義が必要 ▸ Spring Boot環境ではBean定義済み ▸ 非Spring Boot環境ではBean定義が必要👇 @Configuration @EnableTransactionManagement public class TransactionConfig { @Bean public TransactionManager transactionManager( DataSource dataSource) { return new JdbcTransactionManager(dataSource); } } 38
質問は #jsug へ この章のまとめ ▸ トランザクションマネージャーが トランザクションの開始・コミット・ロールバックを 行う ▸ 実体はTransactionManager実装クラス ▸ ほとんどのORマッパーでは JdbcTransactionManagerが使われる ▸ @Transactionalを使わない場合は TransactionTemplateが便利 39
質問は #jsug へ 目次 ① [初級] トランザクションとは ② [初級] 入門@Transactional ③ [中級] 発展@Transactional ④ [中級] トランザクションマネージャーとは ⑤ [上級] AOPの仕組み ⑥ [上級] ORマッパーとの連携 40
質問は #jsug へ 割り込み処理はどうやって実現? @Service public class Service1 { @Transactional public void method1() { 開始 db.更新1(); db.更新2(); コミットorロールバック } } 41
質問は #jsug へ 答: プロキシ ▸ サブクラスが起動時に作られ、それがBeanとなる =プロキシ ▸ プロキシ内には、元クラスのインスタンスと、 割り込み処理を行うインスタンスが同居 Service1のサブクラス Transaction Interceptor Service1 42
質問は #jsug へ プロキシのイメージコード @Service // このコードはイメージです public class Service1Proxy extends Service1 { @Override public void method1() { Service1 service1 = ...; TransactionInterceptor interceptor = ...; interceptor.begin(); // トランザクション開始 try { service1.method1(); // 本来の処理 interceptor.commit(); // コミット } catch (RuntimeException e) { interceptor.rollback(); // ロールバック throw e; } } } 43
質問は #jsug へ プロキシは2種類 ① CGLIB Proxy ▸ 継承によりプロキシを作成 ▸ Spring内部のCGLIBライブラリの機能 ▸ Spring Bootを使っている場合はこちらがデフォルト ② JDK Proxy ▸ インタフェースを実装したプロキシを作成 ▸ java.lang.reflect.Proxyの機能 ▸ Spring Bootを使わない場合はこちらがデフォルト 44
質問は #jsug へ プロキシの制約 ▸ CGLIB Proxy ▸ クラスやメソッドを nalにしてはならない ▸ JDK Proxy ▸ 何らかのインタフェースを実装しなければならない fi 45
質問は #jsug へ プロキシはいつ誰が作るのか ▸ DIコンテナ作成時に、 BeanPostProcessorがプロキシを作る ▸ 詳細は👇の資料で https://www.docswell.com/s/MasatoshiTada/K1XMLK-advanced-spring-for-professionals#p55 46
質問は #jsug へ ハマりやすい落とし穴 @Service public class Service1 { @Transactional(propagation = REQUIRED) public void method1() { method2(); method2()はREQUIRES_NEWだから } 別のトランザクションになるはず!? @Transactional(propagation = REQUIRES_NEW) public void method2() { ... } } 47
質問は #jsug へ 別のトランザクションになってない!? トランザクションが 1つしか開始していない! ... 2023-03-xxT14:04:49.949+09:00 TRACE 33747 --- [main] o.s.t.i.TransactionInterceptor : Getting transaction for [com.example.Service1.method1] method1() start method2() start method2() finish method1() finish 2023-03-xxT14:04:49.950+09:00 TRACE 33747 --- [main] o.s.t.i.TransactionInterceptor : Completing transaction for [com.example.Service1.method1] ... 48
質問は #jsug へ なぜ新規トランザクションが開始しないのか Powered by plantuml.com 同一クラス内だから Interceptorが実行されない! 49
質問は #jsug へ 解決策 ① method2()を別クラスにする ② AopContextを利用する(下記) @Configuration @EnableAspectJAutoProxy(exposeProxy = true) public class AspectConfig { ... } @Service public class Service1 { @Transactional(propagation = REQUIRED) public void method1() { ((Service1) AopContext.currentProxy()) .method2(); // このmethod2()には割り込みされる } @Transactional(propagation = REQUIRES_NEW) public void method2() { ... } } 50
質問は #jsug へ AopContextを利用すると トランザクションが 2つしか開始している! 2つ開始している! ... 2023-03-xxT14:08:48.997+09:00 TRACE 33793 --- [main] o.s.t.i.TransactionInterceptor: Getting transaction for [com.example.Service1.method1] method1() start 2023-03-xxT14:08:48.997+09:00 TRACE 33793 --- [main] o.s.t.i.TransactionInterceptor: Getting transaction for [com.example.Service1.method2] method2() start method2() finish 2023-03-xxT14:08:48.997+09:00 TRACE 33793 --- [main] o.s.t.i.TransactionInterceptor: Completing transaction for [com.example.Service1.method2] method1() finish 2023-03-xxT14:08:49.000+09:00 TRACE 33793 --- [main] o.s.t.i.TransactionInterceptor: Completing transaction for [com.example.Service1.method1] ... 51
質問は #jsug へ AOPの更なる詳細は下記で https://www.docswell.com/s/MasatoshiTada/Z818E5-spring-di-aop-for-every-developers#p41 52
質問は #jsug へ この章のまとめ ▸ AOPはプロキシによって実現 ▸ CGLIB Proxy・JDK Proxyの2種類 ▸ それぞれの制約に注意 ▸ 同一クラス内のメソッド呼び出しでは 割り込み処理が実行されない ▸ 別クラスにする or AopContextを利用で解決 53
質問は #jsug へ 目次 ① [初級] トランザクションとは ② [初級] 入門@Transactional ③ [中級] 発展@Transactional ④ [中級] トランザクションマネージャーとは ⑤ [上級] AOPの仕組み ⑥ [上級] ORマッパーとの連携 54
質問は #jsug へ ORマッパーとトランザクション ▸ 同一トランザクションで処理を行うには、 同一のコネクションを使う必要がある ▸ 一般的なORマッパーのCRUDメソッドは、 呼び出される度に DataSource#getConnection()している ▸ DataSource#getConnection()は、 呼び出されるごとに別のコネクションを返す 55
質問は #jsug へ 下記のコードはなぜ同一トランザクション? DataSource dataSource = ...; JdbcTransactionManager txManager = new JdbcTransactionManager(dataSource); JdbcTemplate jdbcTemplate = update()ごとに new JdbcTemplate(dataSource); TransactionStatus txStatus = txManager.getTransaction(...); getConnection() してるはず?🤔 jdbcTemplate.update("INSERT INTO ..."); jdbcTemplate.update("INSERT INTO ..."); txManager.commit(txStatus); 56
質問は #jsug へ DataSourceUtils.getConnection() ▸ コネクション取得時に、 そのコネクションをスレッドローカルに保存 ▸ トランザクション中の処理では、 スレッドローカルのコネクションを取得 ▸ 後述のTransactionSynchronizationManagerを 利用している 同一トランザクション内では 同一のコネクションを返す! public int update(String sql) { Connection con = DataSourceUtils.getConnection(dataSource); ... } ※実際のコードとは異なります 57
質問は #jsug へ TransactionSynchronizationManager ▸ スレッドローカルに下記を保持するクラス ▸ コネクション ▸ コネクションがトランザクションと同期しているかどうか ▸ 現在トランザクションの処理中かどうか ・・・など ▸ 前述のDataSourceUtils.getConnection() で利用されている 58
質問は #jsug へ 各ORマッパーの対応状況 ▸ DataSourceUtilsを利用 ▸ Spring JDBC ▸ Spring Data JPA ▸ TransactionSynchronizationManager を直接利用 ▸ mybatis-spring 59
質問は #jsug へ Springとの連携ライブラリが無い ORマッパーはどうする? ▸ DataSource実装として TransactionAwareDataSourceProxyを利用する ▸ 内部でDataSourceUtils.getConnection()を利用している public class TransactionAwareDataSourceProxy implements DataSource { private DataSource original; public TransactionAwareDataSourceProxy(DataSource original) { this.original = original; } } @Override // 同一トランザクション内では同一コネクションを返す public Connection getConnection() { return DataSourceUtils.getConnection(original); } ※実際のコードとは異なります 60
質問は #jsug へ Spring Bootでの設定例 spring.datasource.url=... spring.datasource.username=... spring.datasource.password=... @Configuration public class MyJdbcConfig { // このBean定義によって、 // Auto Con gurationによるDataSource Beanは作られなくなる @Bean public TransactionAwareDataSourceProxy dataSource( DataSourceProperties prop) { DataSource original = prop.initializeDataSourceBuilder() .type(HikariDataSource.class).build(); return new TransactionAwareDataSourceProxy(original); } 61 fi } @Bean public MyORM myOrm(TransactionAwareDataSourceProxy dataSource) { return new MyORM(dataSource); }
質問は #jsug へ この章のまとめ ▸ TransactionSynchronizationManagerが スレッドローカルにコネクションを保持している ▸ DataSourceUtils.getConnection()は 上記のスレッドローカル内のコネクションを 取得する ▸ Spring連携ライブラリが無いORマッパーを 使う場合は、DataSource実装として TransactionAwareDataSourceProxy を使えばOK 62
質問は #jsug へ 更に学習するには ▸ Spring徹底入門を読む ▸ 今回紹介した各クラスの ソースコードやJavadocを読む ▸ 公式リファレンスを読む 63
質問は #jsug へ 紹介しきれなかったものたち ▸ Spring BootでDataSourceのBeanを複数作 る ▸ DataSourceが複数ある場合にSpring Testの @Sqlを使う場合の落とし穴 64
質問は #jsug へ ご清聴ありがとうございました! 65