読者です 読者をやめる 読者になる 読者になる

Java EE 事始め!

主にJava EEについて、つらつらとマイペースに書いていきます。「Java EEを勉強するときに、一番最初に読んでもらえるブログ」を目指して頑張ります!

@Transactional(Spring Framework)のpropagation属性

表題の件を調べていて、ほぼJava EEJTAと同じかと思いきや、NESTEDというJTAには無いものがあったのでまとめました。

@Transactionalとは?

メソッドに付加すると、メソッドの開始がトランザクションの開始、メソッドの終了がトランザクションの終了になります。

メソッド内で非チェック例外(RuntimeException及びそのサブクラス)が発生した場合はロールバックされます。

チェック例外の場合はロールバックされません。

具体的なコードで説明します。

実際にDBにアクセスするTxTestRepositoryクラス(以下「リポジトリ」)があり、それを呼び出しているTxTestService1(以下「サービス1」)とTxTestService2(以下「サービス2」)があります。

そして、サービス1はサービス2も呼び出しているとします。

@Repository
public class TxTestRepository {

    @Autowired
    private NamedParameterJdbcTemplate jdbcTemplate;

    public void insert(int id) {
        Map<String, Object> params = new HashMap<>();
        params.put("id", id);
        params.put("created_at", new Date());
        jdbcTemplate.update("INSERT INTO tx_test VALUES(:id, :created_at)", params);
    }
}
@Service
public class TxTestService2 {

    @Autowired
    private TxTestRepository repository;

    @Transactional(propagation = Propagation.REQUIRED)
    public void insert(int id) {
        repository.insert(id);
    }
}
@Service
public class TxTestService1 {

    @Autowired
    private TxTestRepository repository;
    @Autowired
    private TxTestService2 service2;

    @Transactional(propagation = Propagation.REQUIRED)
    public void insert() {
        repository.insert(1);
        service2.insert(2);
        repository.insert(3);

    }

サービス1のinsertメソッドを呼び出すと、「1」と「3」はリポジトリ経由で、「2」はサービス2経由でDBに追加されます。

問題は、サービス1からサービス2を呼び出した時に、この2つが同一トランザクションなのか、別のトランザクションなのかです。

それを決めているのが、@Transactionalのpropagation属性です。

REQUIRED

上記のコードでは、サービス1のpropagationはREQUIRED、サービス2もREQUIREDです。

REQUIREDの場合、そのメソッドが呼び出された時にトランザクションが開始されていなければ新規に開始し、すでに開始されていればそのトランザクションをそのまま利用します。

上記のコードの場合、まずサービス1のinsertメソッドが呼ばれたら、トランザクションが開始されます。

その中でサービス2のinsertメソッドを読んだ時に、サービス1のトランザクションがそのまま利用されます。つまり、サービス1とサービス2は同一トランザクションです。

ここで、サービス2の中で、わざと例外をスローしてみます。

propagationはREQUIREDのままです。

@Service
public class TxTestService2 {
    ...
    @Transactional(propagation = Propagation.REQUIRED)
    public void insert(int id) {
        repository.insert(id);
        throw new RuntimeException("ERROR 2");
    }
}

サービス1では、service2.insert(2)をtry-catchで囲みます。

こちらも、propagationはREQUIREDのままです。

@Service
public class TxTestService1 {
    ...
    @Transactional(propagation = Propagation.REQUIRED)
    public void insert() {
        repository.insert(1); // ロールバックされる
        try {
            service2.insert(2); // ロールバックされる
        } catch (Exception e) {
            e.printStackTrace();
        }
        repository.insert(3); // ロールバックされる
    }
}

こうすると、サービス2が非チェック例外発生→ロールバックしたら、同一トランザクションであるサービス1もロールバックされます。

すなわち、追加される件数は0件です。

例外をサービス1で発生させても同じです。

サービス2では例外が発生していないので、サービス2側はコミットされるかと思いきや、サービス1側で例外が発生してロールバックされるので、同一トランザクションであるサービス2で追加したレコードもロールバックされます。

@Service
public class TxTestService2 {
    ...
    @Transactional(propagation = Propagation.REQUIRED)
    public void insert(int id) {
        repository.insert(id);
    }
}
@Service
public class TxTestService1 {
    ...
    @Transactional(propagation = Propagation.REQUIRED)
    public void insert() {
        repository.insert(1); // ロールバックされる
        try {
            service2.insert(2); // ロールバックされる
        } catch (Exception e) {
            e.printStackTrace();
        }
        repository.insert(3); // ロールバックされる
        throw new RuntimeException("ERROR 1");
    }
}

REQUIRES_NEW

REQUIRES_NEWの場合、そのメソッドが呼び出された時にトランザクションが開始されていようがなかろうが、常に新規のトランザクションを開始します。

サービス2のみをREQUIRES_NEWに変更し、サービス2内で例外をスローしてみます。

@Service
public class TxTestService2 {
    ...
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void insert(int id) {
        repository.insert(id);
        throw new RuntimeException("ERROR 2");
    }
}
@Service
public class TxTestService1 {
    ...
    @Transactional(propagation = Propagation.REQUIRED)
    public void insert() {
        repository.insert(1); // コミットされる
        try {
            service2.insert(2); // これはロールバックされる
        } catch (Exception e) {
            e.printStackTrace();
        }
        repository.insert(3); // コミットされる
    }
}

こうすると、サービス1とサービス2は別のトランザクションなので、サービス2で例外発生→ロールバックされても、サービス1には影響がありません。

よって、サービス2で追加した「2」だけロールバックされ、サービス1で追加した「1」「3」はコミットされます。つまり、追加される件数は2件です。

サービス1で例外が発生した場合、サービス1で追加した「1」「3」はロールバックされ、サービス2で追加した「2」はコミットされます。つまり、追加される件数は1件です。

@Service
public class TxTestService2 {
    ...
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void insert(int id) {
        repository.insert(id);
    }
}
@Service
public class TxTestService1 {
    ...
    @Transactional(propagation = Propagation.REQUIRED)
    public void insert() {
        repository.insert(1); // ロールバックされる
        try {
            service2.insert(2); // これはコミットされる
        } catch (Exception e) {
            e.printStackTrace();
        }
        repository.insert(3); // ロールバックされる
        throw new RuntimeException("ERROR 1");
    }
}

NESTED

NESTEDの場合、REQUIREDと同様に、そのメソッドが呼び出された時にトランザクションが開始されていなければ新規に開始し、すでに開始されていればそのトランザクションをそのまま利用します。

しかし、「部分的ロールバック」が可能になっており、サービス2で例外発生→ロールバックされても、サービス1はロールバックされません。

部分的ロールバックについてはこちら→http://qiita.com/yuba/items/9b5b86bc3e128a84db5e

内部的には、JDBC 3.0以降の機能であるjavax.sql.Savepointを利用しているようです。

Savepointについてはこちら→http://java-reference.sakuraweb.com/java_db_savepoint.html

サービス2のみをNESTEDに変更し、サービス2内で例外をスローしてみます。

@Service
public class TxTestService2 {
    ...
    @Transactional(propagation = Propagation.NESTED)
    public void insert(int id) {
        repository.insert(id);
        throw new RuntimeException("ERROR 2");
    }
}
@Service
public class TxTestService1 {
    ...
    @Transactional(propagation = Propagation.REQUIRED)
    public void insert() {
        repository.insert(1); // コミットされる
        try {
            service2.insert(2); // これはロールバックされる
        } catch (Exception e) {
            e.printStackTrace();
        }
        repository.insert(3); // コミットされる
    }
}

サービス1とサービス2は同一トランザクションですが、サービス2には部分的ロールバックが適用され、サービス1には影響がありません。

よって、サービス2で追加した「2」だけロールバックされ、サービス1で追加した「1」「3」はコミットされます。つまり、追加される件数は2件です。

サービス1で例外が発生した場合、サービス1で追加した「1」「3」はロールバックされるのは当然ですが、あくまで同一トランザクションなので、サービス2で追加した「2」もロールバックされます。つまり、追加される件数は0件です。

ここが、完全に別トランザクションであるREQUIRES_NEWと違うところですね。

@Service
public class TxTestService2 {
    ...
    @Transactional(propagation = Propagation.NESTED)
    public void insert(int id) {
        repository.insert(id);
    }
}
@Service
public class TxTestService1 {
    ...
    @Transactional(propagation = Propagation.REQUIRED)
    public void insert() {
        repository.insert(1); // ロールバックされる
        try {
            service2.insert(2); // これもロールバックされる
        } catch (Exception e) {
            e.printStackTrace();
        }
        repository.insert(3); // ロールバックされる
        throw new RuntimeException("ERROR 1");
    }
}

ちなみに、サービス1とサービス2をNESTEDにした場合でも、追加件数は0件です。

部分的ロールバックというのは、あくまで内部トランザクション(今回はサービス2)がロールバックされた時に適用され、外部トランザクション(今回はサービス1)がロールバックされた場合は、内部トランザクションも一緒にロールバックされます。

NESTEDの注意点

http://docs.spring.io/spring-framework/docs/4.2.x/javadoc-api/org/springframework/transaction/annotation/Propagation.html#NESTED

Javadocを読むと、「特定のトランザクションマネージャでのみ動作する」と書いてあります。

http://docs.spring.io/spring-framework/docs/4.2.x/javadoc-api/org/springframework/jdbc/datasource/DataSourceTransactionManager.html

DataSourceTransactionManagerの場合は「nestedTransactionAllowedはデフォルトでtrue」と書かれていますが、

http://docs.spring.io/spring-framework/docs/4.2.x/javadoc-api/org/springframework/orm/jpa/JpaTransactionManager.html

JpaTransactionManagerの場合は「nestedTransactionAllowedはデフォルトでfalse」と書かれています。

他のトランザクションマネージャの種類によって違うっぽいので注意ですね。

参考資料

Spring Framework Reference

http://docs.spring.io/spring/docs/current/spring-framework-reference/html/transaction.html#tx-propagation

Java EE 7 Tutorial (JTA)

https://docs.oracle.com/javaee/7/tutorial/transactions003.htm

Java EEチュートリアルなのでNESTEDの説明はありませんが、Table 51-1にその他の属性とトランザクションが新規か否かの関係がまとめられており、分かりやすいです。