Springにおけるトランザクション管理の実装と制御メカニズム

Spring FrameworkはJDBC操作を抽象化し、データベースアクセスを簡素化しますが、その過程で必ず直面する課題がトランザクション制御です。Springはこの要件に対応するため、柔軟かつ多層的なトランザクション管理機能を提供しています。

1. トランザクションの設定手法

Springでは、トランザクションの適用方法を大きく「宣言型」と「プログラム型」に分類できます。どちらを選択するにせよ、事前にデータソースとトランザクションマネージャーの定義が必要です。

<!-- データソース定義 -->
<bean id="dataSource" class="com.zaxxer.hikari.HikariDataSource" destroy-method="close">
  <property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
  <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/demo?serverTimezone=UTC"/>
  <property name="username" value="app_user"/>
  <property name="password" value="secure_pass"/>
</bean>

<!-- JDBCトランザクションマネージャー -->
<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
  <property name="dataSource" ref="dataSource"/>
</bean>

1.1 宣言型トランザクション

これはAOPに基づく非侵入的アプローチで、ビジネスロジックからトランザクション制御を分離します。

1.1.1 @Transactionalアノテーションによる制御

XML設定でアノテーション駆動を有効化します:

<tx:annotation-driven transaction-manager="txManager" proxy-target-class="true"/>

その後、対象メソッドにアノテーションを付与します:

@Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.READ_COMMITTED)
public UserProfile createAndFetchProfile(UserProfile profile, Long id) {
  userRepository.insert(profile);
  return userRepository.findById(id).orElseThrow();
}

注意点:アノテーションはpublicメソッドのみに有効です。privateやpackage-privateなメソッド、および同一クラス内からの直接呼び出し(self-invocation)ではプロキシが適用されず、トランザクションが開始されません。

1.1.2 XMLベースのAOP切点定義

特定の命名規則に従うメソッド群に一括適用できます:

<tx:advice id="txAdvice" transaction-manager="txManager">
  <tx:attributes>
    <tx:method name="save*" propagation="REQUIRED" timeout="30"/>
    <tx:method name="find*" read-only="true"/>
    <tx:method name="*" rollback-for="BusinessValidationException"/>
  </tx:attributes>
</tx:advice>

<aop:config>
  <aop:pointcut id="serviceOperation" 
    expression="execution(* com.example.service..*Service.*(..))"/>
  <aop:advisor advice-ref="txAdvice" pointcut-ref="serviceOperation"/>
</aop:config>

1.1.3 TransactionProxyFactoryBean(非推奨)

古いSpringバージョンで使われた手法で、現在はアノテーションまたはJava Configが標準です。代替として以下のようなBean定義が可能です:

<bean id="baseTxProxy" abstract="true"
      class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean">
  <property name="transactionManager" ref="txManager"/>
  <property name="transactionAttributes">
    <props>
      <prop key="create*">PROPAGATION_REQUIRED</prop>
      <prop key="get*">PROPAGATION_SUPPORTS,readOnly=true</prop>
    </props>
  </property>
</bean>

1.2 プログラム型トランザクション

明示的な制御が必要な場合に使用され、コード内にトランザクションフローが記述されます。

1.2.1 TransactionTemplateの活用

再利用可能なテンプレートで、コールバック形式で処理を記述します:

@Service
public class AccountService {

  private final TransactionTemplate txTemplate;
  private final AccountRepository repository;

  public AccountService(TransactionTemplate txTemplate, AccountRepository repository) {
    this.txTemplate = txTemplate;
    this.repository = repository;
  }

  public Account transferFunds(Long fromId, Long toId, BigDecimal amount) {
    return txTemplate.execute(status -> {
      try {
        Account source = repository.lockAndLoad(fromId); // SELECT ... FOR UPDATE
        Account target = repository.findById(toId).orElseThrow();
        
        source.debit(amount);
        target.credit(amount);
        
        repository.update(source);
        repository.update(target);
        
        return source;
      } catch (InsufficientBalanceException e) {
        status.setRollbackOnly();
        throw e;
      }
    });
  }
}

1.2.2 ReactiveTransactionManager(Spring WebFlux対応)

リアクティブ環境向けに、Mono/Fluxを用いたトランザクション制御が可能です:

@Service
public class OrderProcessingService {

  private final TransactionalOperator operator;

  public OrderProcessingService(ReactiveTransactionManager txManager) {
    this.operator = TransactionalOperator.create(txManager);
  }

  public Mono<OrderConfirmation> processOrder(OrderRequest request) {
    return validateOrder(request)
        .flatMap(this::reserveInventory)
        .flatMap(this::chargePayment)
        .flatMap(this::persistOrder)
        .as(operator::transactional);
  }
}

1.2.3 PlatformTransactionManagerの直接利用

完全な制御を求める場合、低レベルAPIを直接呼び出します:

public void executeWithManualControl(UserData data) {
  TransactionDefinition def = new DefaultTransactionDefinition();
  def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
  def.setTimeout(60);

  TransactionStatus status = txManager.getTransaction(def);

  try {
    userRepository.create(data);
    notificationService.sendWelcomeEmail(data.getEmail());
    txManager.commit(status);
  } catch (Exception ex) {
    txManager.rollback(status);
    throw new ServiceException("Operation failed", ex);
  }
}

2. トランザクション伝播挙動(Propagation Behavior)

複数のトランザクション付きメソッドが相互呼び出しを行う際の振る舞いを定義します。代表的な値は以下の通りです:

  • REQUIRED:既存トランザクションがあれば参加、なければ新規作成(デフォルト)
  • REQUIRES_NEW:常に新規トランザクションを開始し、親トランザクションを一時停止
  • NESTED:保存ポイント(savepoint)を用いたネストされた実行(JDBCサポート必須)
  • SUPPORTS:トランザクションがあれば参加、なければ非トランザクションで実行
  • NOT_SUPPORTED:トランザクションを明示的に無効化して実行
  • NEVER:トランザクション内で呼び出された場合、例外をスロー

3. 分離レベル(Isolation Level)

データの一貫性保証レベルを指定します。SpringはJDBCドライバー経由でDBの分離レベルを設定します:

Spring列挙値 対応するSQL標準 主な課題
DEFAULTDB依存データベースのデフォルト設定を継承
READ_UNCOMMITTED未コミット読み取りダーティリード、非反復的読み取り、ファントムリード
READ_COMMITTEDコミット済み読み取り非反復的読み取り・ファントムリードあり
REPEATABLE_READ反復可能読み取りファントムリードのみ発生可
SERIALIZABLE直列化完全な一貫性確保(パフォーマンス低下)

4. ロールバック条件

デフォルトでは、RuntimeExceptionおよびErrorのサブクラスに対して自動ロールバックが発生します。チェック例外(Exceptionの非ランタイムサブクラス)はデフォルトでロールバックされません。

カスタマイズ例:

// 指定例外でもロールバック
@Transactional(rollbackFor = {IOException.class, SQLException.class})

// 特定ランタイム例外はロールバックしない
@Transactional(noRollbackFor = ArithmeticException.class)

5. タイムアウトと読み取り専用フラグ

timeout属性はトランザクションの最大実行時間を秒単位で指定し、超過時にロールバックを強制します。

readOnly = trueは、JDBC接続のsetReadOnly(true)呼び出しに反映され、一部のデータベース(例:MySQL InnoDB)では最適化(例:排他ロック抑制)が適用されます。ただし、この設定は論理的な制約ではなく、物理的な制限ではありません。

6. トランザクションのライフサイクルとMyBatisキャッシュの関係

トランザクション境界は、MyBatisのSqlSessionライフサイクルにも影響を与えます。同一トランザクション内で複数のクエリを実行すると、1次キャッシュが有効になり、重複したSELECTがDBアクセスなしで完了します:

@Transactional
public User fetchUserTwice(Long id) {
  User first = userMapper.selectById(id); // DBアクセス
  // 中間処理(UPDATEなどがない限りキャッシュが維持される)
  User second = userMapper.selectById(id); // キャッシュヒット → DBアクセスなし
  return second;
}

この挙動により、「不可反復読み取り」問題を回避できるだけでなく、I/O負荷も削減されます。

タグ: spring-framework transaction-management JDBC MyBatis AOP

6月9日 18:03 投稿