◀ 24.

【Spring】@Transactionalアノテーションとは

▶
この記事の要点
  • @Transactional はメソッド / クラスに付けてトランザクションの範囲を宣言するアノテーション
  • メソッドが正常終了すれば コミット、例外で抜ければ ロールバック。ただし既定でロールバックするのは RuntimeException と Error だけ
  • チェック例外でもロールバックしたいなら rollbackFor = Exception.class
  • 主なオプション: propagation / isolation / readOnly / timeout / rollbackFor
  • 最大の落とし穴は同じクラス内からの呼び出し(自己呼び出し)では効かないこと。プロキシ経由でしか動かない

@Transactional とは

@Transactional は、Spring Framework で DB を更新する際のトランザクションを管理するアノテーションです。付けたメソッドの開始時にトランザクションを開始し、正常に終わればコミット、例外が投げられればロールバックします。commit() や rollback() を自分で書かずに、「このメソッドの中の DB 操作はすべて成功するか、すべて取り消されるか」を保証できます。

クラスもしくはメソッド単位で付与することができます。クラスに付けるとそのクラスの public メソッドすべてに適用され、クラスとメソッドの両方に付与した場合はメソッドのアノテーションの設定が優先されます。

Spring Boot では、spring-boot-starter-data-jpa や spring-boot-starter-jdbc を依存に加えるとトランザクション管理が自動で有効になるため、@EnableTransactionManagement を自分で書く必要は通常ありません。

基本的な使い方

一般的には、業務ロジックをまとめる Service クラスのメソッドに付けます。

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class TransferService {

    private final AccountRepository accountRepository;

    public TransferService(AccountRepository accountRepository) {
        this.accountRepository = accountRepository;
    }

    @Transactional
    public void transfer(Long fromId, Long toId, long amount) {
        Account from = accountRepository.findById(fromId).orElseThrow();
        Account to = accountRepository.findById(toId).orElseThrow();
        from.withdraw(amount);   // 残高不足なら RuntimeException
        to.deposit(amount);
        // 例外が出れば、両方の変更がロールバックされる
    }
}

次は JpaRepository で、更新系のクエリメソッドに @Transactional を使用した例です。@Modifying 付きの更新クエリはトランザクション内で実行する必要があります。

@Repository
public interface TestRepository extends JpaRepository<TestEntity, String> {

    @Transactional
    @Modifying
    @Query("UPDATE TestEntity te SET te.colA = 1 WHERE te.id = :id")
    Integer updateTest(@Param("id") String id);
}

なお、save() や findById() など Spring Data JPA が標準で提供するメソッドには、最初からトランザクション設定が付いています。

主なオプション

属性既定値意味
propagationREQUIRED既にトランザクションがあるときの振る舞い(下表)
isolationDEFAULT分離レベル。DEFAULT は DB の既定に従う(MySQL InnoDB なら REPEATABLE READ)
readOnlyfalse読み取り専用のヒント。JPA ではフラッシュを省くなどの最適化が効く
timeout-1(無制限)タイムアウト秒数
rollbackForなし追加でロールバック対象にする例外クラス
noRollbackForなしロールバックしない例外クラス
transactionManager(value)既定のもの複数 DB を使うときに使うトランザクションマネージャを指定

propagation(伝播)の種類

値既存トランザクションがある場合ない場合
REQUIREDそれに参加する新しく開始する
REQUIRES_NEW既存を一時停止して、別の新しいトランザクションを開始する新しく開始する
NESTEDセーブポイントを作って入れ子で実行する新しく開始する
SUPPORTS参加するトランザクションなしで実行する
MANDATORY参加する例外を投げる
NOT_SUPPORTED既存を一時停止してトランザクションなしで実行するトランザクションなしで実行する
NEVER例外を投げるトランザクションなしで実行する

REQUIRES_NEW は「本処理が失敗しても操作ログだけは必ず残したい」といった場面で使います。

ロールバックされる例外・されない例外

既定では、RuntimeException(非チェック例外)と Error ではロールバックし、チェック例外(IOException など)ではコミットします。チェック例外でもロールバックしたい場合は次のように指定します。

@Transactional(rollbackFor = Exception.class)
public void importCsv(Path file) throws IOException {
    // IOException が出てもロールバックされる
}

@Transactional(readOnly = true)
public List<User> findActiveUsers() {
    return userRepository.findByActiveTrue();
}

Spring Framework 6.2 以降では、@EnableTransactionManagement(rollbackOn = RollbackOn.ALL_EXCEPTIONS) でアプリ全体の既定を「すべての例外でロールバック」に変えることもできます。

効かないときの落とし穴

  • 同じクラス内のメソッドから呼んでいる: @Transactional は Spring が作るプロキシを経由した呼び出しにだけ効く。this.save() のような自己呼び出しではプロキシを通らないため、トランザクションが開始されない。別の Bean に切り出すのが基本の対処
  • Spring 管理外のオブジェクト: new で作ったインスタンスのメソッドには効かない
  • 例外を握りつぶしている: メソッド内で catch して外に投げないと、正常終了とみなされてコミットされる
  • private メソッド: プロキシから呼べないため効かない。Spring 6 以降は、クラスベースのプロキシなら protected やパッケージプライベートのメソッドにも適用される
  • アノテーションの取り違え: jakarta.transaction.Transactional も Spring で動くが、readOnly・isolation・timeout などの属性は org.springframework.transaction.annotation.Transactional にしかない。import を確認する
  • 別スレッドでの処理: トランザクションはスレッドに紐づくため、@Async や自前のスレッドで実行した処理は呼び出し元のトランザクションに含まれない

確認方法

トランザクションが開始・コミット・ロールバックされているかは、ログで確認するのが確実です。application.properties に次を追加します。

logging.level.org.springframework.transaction=DEBUG
logging.level.org.springframework.orm.jpa.JpaTransactionManager=DEBUG

メソッド呼び出し時に「Creating new transaction with name [...]」、終了時に「Committing」や「Rolling back」といったログが出れば、正しく適用されています。コード内で確認したい場合は TransactionSynchronizationManager.isActualTransactionActive() が true を返すかを見ます。

関連

Post Share
子ページ

子ページはありません

同階層のページ
  1. @After
  2. @Autowired
  3. @Bean
  4. @Before
  5. @Column
  6. @Component
  7. @Configuration
  8. @Controller
  9. @Data
  10. @Entity
  11. @GeneratedValue
  12. @Id
  13. @Modifying
  14. @PathVariable
  15. @PropertySource
  16. @Repository
  17. @RequestBody
  18. @RequestMapping
  19. @ResponseBody
  20. @RestController
  21. @Service
  22. @SpringBootApplication
  23. @Table
  24. @Transactional
  25. @Value