Springプロジェクトにおける組み込みの宣言的トランザクション@Transactionalの使用は推奨されない理由

トランザクションの基本概念

トランザクション、分離レベル、データベースの同時実行問題については、以下の記事を参照してください:データベーストランザクション、分離レベル、および同時実行問題の入門(MySQL)

データベースの主なトランザクション分離レベルには以下のものがあります:

  • READ_UNCOMMITTED(読み取り未コミット):最も低い分離レベルで、あるトランザクションがコミットされていないデータを他のトランザクションが読み取ることができます。
  • READ_COMMITTED(読み取りコミット):あるトランザクションがコミットされた後でなければ、他のトランザクションはそのデータを読み取ることができません。
  • REPEATABLE_READ(読み取り一貫性):ダーティリードを防ぎますが、ファントムリードが発生する可能性があります。
  • SERIALIZABLE(直列化可能):最もコストがかかりますが、最も信頼性の高い分離レベルです。トランザクションは順次実行されます。

ダーティリード、非再現読み取り、ファントムリードの概念:

  • ダーティリード:あるトランザクションがコミットされていないデータを読み取ること。
  • 非再現読み取り:同じトランザクション内で、同じクエリが複数回実行された場合に、異なる結果が返されること。
  • ファントムリード:あるトランザクションが同じクエリを複数回実行した際に、他のトランザクションによって挿入された新しい行が見えたり、削除された行が見えなくなったりすること。

Springにおけるトランザクション管理

Springのトランザクションは、デフォルトでデータベースの分離レベルを使用します。もちろん、@Transactionalアノテーションのisolationパラメータを通じて、現在のセッションレベルの分離レベルを調整することも可能です。

isolationパラメータには以下の5種類があります:

  • Isolation.DEFAULT:データソースのデフォルト分離レベルを使用します。
  • Isolation.READ_UNCOMMITTED:未コミット読み取りレベル。
  • Isolation.READ_COMMITTED:コミット読み取りレベル。
  • Isolation.REPEATABLE_READ:読み取り一貫性レベル。
  • Isolation.SERIALIZABLE:直列化可能レベル。

トランザクションの伝播動作:トランザクションメソッドが相互に呼び出される場合、トランザクションがこれらのメソッド間でどのように伝播するかを定義します。Springは@Transactional(propagation = Propagation.REQUIRES_NEW)propagationパラメータを通じて7種類の伝播動作をサポートしています。

Springのデフォルトのトランザクション伝播動作はPROPAGATION_REQUIREDです。これはほとんどの場合に適しています。例えば、ServiceX#methodX()がすべてトランザクション環境で動作していると仮定すると、プログラム内にService1#method1() -> Service2#method2() -> Service3#method3()のような呼び出しチェーンが存在する場合、これら3つのサービスクラスの3つのメソッドはSpringのトランザクション伝播メカニズムによって同じトランザクション内で動作します。

Springの宣言的トランザクション@Transactionalが機能しないシナリオ

宣言的トランザクション管理はAOP(アスペクト指向プログラミング)を基盤としており、その本質はメソッドの前後にインターセプトを行い、ターゲットメソッドの開始前にトランザクションを作成または参加させ、ターゲットメソッドの実行後に実行結果に基づいてコミットまたはロールバックを行うことです。宣言的トランザクションの最大の利点は、トランザクションをプログラミングで管理する必要がないことです。設定ファイルで関連するトランザクションルールを宣言するか、アノテーション方式で、トランザクションルールをビジネスロジックに適用できます。

1. アクセス修飾子の問題

アクセス修飾子(privatedefaultprotectedpublic)が不適切に設定されていると、トランザクション機能が正しく動作せず、場合によっては無効になることがあります。例えば:

@Service
public class ProductService {
    @Transactional
    private void create(Product product) {
        saveProduct(product);
        updateProduct(product);
    }
}

上記のコードでは、createメソッドのアクセス修飾子がprivateに設定されています。これによりトランザクションが無効になります。理由は、Springがプロキシされるメソッドはpublicである必要があるためです。Springのソースコードを確認すると、以下のようになっています:

protected TransactionAttribute computeTransactionAttribute(Method method, @Nullable Class<?> targetClass) {
    // 非publicメソッドは許可しない。
    if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
        return null;
    }
    // ... (後続のコード)
}

上記のソースコードからわかるように、AbstractFallbackTransactionAttributeSourceクラスのcomputeTransactionAttributeメソッドには、メソッドの修飾子がpublicでない場合、nullを返し、トランザクションをサポートしないというチェックがあります。つまり、カスタムのトランザクションメソッド(ターゲットメソッド)のアクセス権がpublicではなく、privatedefaultprotected修飾子の場合、Springはトランザクションを提供しません。

2. メソッドがfinalまたはstaticで修飾されている

メソッドがfinalまたはstaticで修飾されていると、トランザクションが無効になることがあります。

@Service
public class ProductService {
    @Transactional
    private final void create(Product product) {
        saveProduct(product);
        updateProduct(product);
    }
}

理由は、Springのトランザクションの背後にはAOP(JDKダイナミックプロキシまたはCGLIB)が使用されており、プロキシクラスが生成され、その中でトランザクション機能が実装されています。しかし、メソッドがfinalで修飾されている場合、プロキシクラス内でそのメソッドをオーバーライドできず、トランザクション機能を追加できません。同様に、staticメソッドもダイナミックプロキシによってトランザクションメソッドに変換できません。

3. 同一クラス内でのメソッド呼び出し

あるメソッド内から、同じクラス内の別のトランザクションメソッドを呼び出すと、トランザクションが無効になることがあります。

@Service
public class OrderService {
    @Autowired
    private OrderMapper orderMapper;

    public void process(Order order) {
        orderMapper.insert(order);
        updateStatus(order); // この呼び出しはトランザクションを生成しない
    }

    @Transactional
    public void updateStatus(Order order) {
        // トランザクション処理
    }
}

この問題の解決策はいくつかあります:

  • 解決策1:新しいServiceクラスを追加する
@Service
public class OrderServiceA {
    @Autowired
    private OrderServiceB serviceB;

    public void process(Order order) {
        serviceB.doProcess(order);
    }
}

@Service
public class OrderServiceB {
    @Transactional
    public void doProcess(Order order) {
        // トランザクション処理
    }
}
  • 解決策2:自身をインジェクトする
@Service
public class OrderService {
    @Autowired
    private OrderService self;

    public void process(Order order) {
        self.doProcess(order);
    }

    @Transactional
    public void doProcess(Order order) {
        // トランザクション処理
    }
}
  • 解決策3:AopContextを使用する
@Service
public class OrderService {
    public void process(Order order) {
        ((OrderService) AopContext.currentProxy()).doProcess(order);
    }

    @Transactional
    public void doProcess(Order order) {
        // トランザクション処理
    }
}

4. Springの管理下にないオブジェクト

トランザクションを使用するためには、オブジェクトがSpringによって管理されている必要があります。通常、@Service@Componentなどのアノテーションをクラスに付与することで、Beanインスタンスの生成と依存性の注入が行われます。もし@Serviceアノテーションが欠けている場合、そのクラスはSpringによって管理されず、そのメソッドもトランザクションを生成しません。

5. マルチスレッド環境での呼び出し

マルチスレッド環境でSpringトランザクションを使用すると、問題が発生することがあります。

@Slf4j
@Service
public class OrderService {
    @Autowired
    private OrderMapper orderMapper;
    @Autowired
    private PaymentService paymentService;

    @Transactional
    public void create(Order order) throws Exception {
        orderMapper.insert(order);
        new Thread(() -> {
            paymentService.processPayment();
        }).start();
    }
}

@Service
public class PaymentService {
    @Transactional
    public void processPayment() {
        // 支払い処理
    }
}

理由は、Springのトランザクションはデータベース接続によって実現されるためです。Springはデータベース接続をThreadLocalに保存しています。同じトランザクションは同じデータベース接続を使用する必要があります。しかし、マルチスレッド環境では、取得するデータベース接続が異なるため、異なるトランザクションになります。その結果、processPaymentメソッドで例外が発生しても、createメソッドはロールバックされません。

6. トランザクションをサポートしないテーブルエンジン

MySQL 5.x以前のデフォルトのストレージエンジンであるMyISAMは、トランザクションをサポートしていません。InnoDBエンジンはトランザクションをサポートするため、現在の開発ではInnoDBが推奨されます。

7. トランザクションが有効になっていない

Spring Bootプロジェクトでは、DataSourceTransactionManagerAutoConfigurationクラスによってトランザクションがデフォルトで有効になっています。ただし、従来のSpringプロジェクトでは、以下の設定が必要です:

<!-- トランザクションマネージャーの設定 -->
<bean class="org.springframework.jdbc.datasource.DataSourceTransactionManager" id="transactionManager">
    <property name="dataSource" ref="dataSource"></property>
</bean>
<tx:advice id="advice" transaction-manager="transactionManager">
    <tx:attributes>
        <tx:method name="*" propagation="REQUIRED"/>
    </tx:attributes>
</tx:advice>
<!-- アドバイスをポイントカットに適用 -->
<aop:config>
    <aop:pointcut expression="execution(* com.example.*.*(..))" id="pointcut"/>
    <aop:advisor advice-ref="advice" pointcut-ref="pointcut"/>
</aop:config>

トランザクションがロールバックされないシナリオ

8.1. 不適切な伝播動作

例えば、Propagation.NEVERはトランザクションをサポートしないため、トランザクション内で使用すると例外がスローされます。

8.2. 例外をキャッチして処理する

トランザクション内で例外をキャッチし、再スローしないと、Springは正常終了とみなし、ロールバックしません。

@Transactional
public void update(Order order) {
    try {
        // データ更新処理
    } catch (Exception e) {
        log.error("エラーが発生しました", e);
        // 例外を再スローしないとロールバックされない
    }
}

8.3. 不適切な例外タイプをスローする

SpringはデフォルトでRuntimeExceptionErrorのみをロールバックします。チェック例外(Exception)をスローした場合、ロールバックされません。

8.4. カスタムロールバック例外の設定ミス

@Transactional(rollbackFor = CustomException.class)のように設定した場合、スローされる例外がCustomExceptionでないとロールバックされません。

プログラムによるトランザクション管理の推奨

宣言的トランザクションは便利ですが、例外をキャッチする必要がある場合や、マルチスレッド環境での使用には柔軟性に欠けます。プログラムによるトランザクション管理は、より細かい制御が可能です。

TransactionTemplateの使用

TransactionTemplateは、トランザクション管理を簡素化するためのテンプレートクラスです。

@Service
public class PaymentService {
    private final TransactionTemplate transactionTemplate;

    @Autowired
    public PaymentService(PlatformTransactionManager transactionManager) {
        this.transactionTemplate = new TransactionTemplate(transactionManager);
    }

    public void executePayment(Payment payment) {
        transactionTemplate.execute(new TransactionCallbackWithoutResult() {
            @Override
            protected void doInTransactionWithoutResult(TransactionStatus status) {
                try {
                    createPaymentRecord(payment);
                    callExternalApi(payment);
                } catch (Exception e) {
                    status.setRollbackOnly();
                    throw e;
                }
            }
        });
    }

    private void createPaymentRecord(Payment payment) { /* ... */ }
    private void callExternalApi(Payment payment) { /* ... */ }
}

PlatformTransactionManagerの直接使用

より低レベルの制御が必要な場合は、PlatformTransactionManagerを直接使用できます。

@Service
public class InventoryService {
    private final PlatformTransactionManager transactionManager;

    @Autowired
    public InventoryService(PlatformTransactionManager transactionManager) {
        this.transactionManager = transactionManager;
    }

    public void adjustStock(Item item, int quantity) {
        DefaultTransactionDefinition def = new DefaultTransactionDefinition();
        def.setName("stockAdjustment");
        def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);

        TransactionStatus status = transactionManager.getTransaction(def);
        try {
            updateStockLevel(item, quantity);
            transactionManager.commit(status);
        } catch (Exception e) {
            transactionManager.rollback(status);
            throw new RuntimeException("在庫調整に失敗しました", e);
        }
    }

    private void updateStockLevel(Item item, int quantity) { /* ... */ }
}

タグ: Spring Framework トランザクション管理 TransactionTemplate PlatformTransactionManager AOP

7月18日 00:53 投稿