[spring]10.5 Declarative transaction management

宣言的トランザクションについて。
宣言的トランザクションはSpringAOPにより実現されるが、トランザクションを扱う専用のAspectを決まりきったやり方で適用するだけでいいので、AOPの概念をちゃんと理解してなくても使用できる。Springの宣言的トランザクションEJBのCMTと似ており、メソッド単位でトランザクションの制御が可能。setRolbackOnlyをすることもできる。

SpringとEJB-CMTとの違いは以下のとおり。

  1. JTAに依存していないので、JavaEE環境でなくても動く。設定変更するだけで、JTAとも連携できるし、ローカルなJDBC/JPA/Hibernateとも連携できる。
  2. EJB以外の全てのクラスに対してトランザクションを設定できる。
  3. Springはトランザクション制御に関して、プログラムで制御する方法と、宣言的に扱う方法の二通り提供する。
  4. Springはトランザクションの振る舞いをAOPでカスタマイズ出来る。例えば、トランザクションロールバックする際の振る舞いを拡張できる(任意のアドバイスを追加する)。
  5. Springはリモートクラスをまたがったトランザクションの伝搬をサポートしない。

ロールバックの指定については、ロールバックのトリガとなる例外クラスを指定することで実現できる。これを書けばTransactionStatusのsetRolbackOnlyを実行しなくても例外が発生したら自動的にロールバックされる。普通はアプリケーションの独自例外が発生したらロールバックする設定にするだろう。これによりビジネスロジックトランザクションに依存しなくなる。
(以下、容易に想像できるEJBの悪口があるが面倒なので割愛)

10.5.1 Understanding the Spring Framework's declarative transaction implementation

@Transactionalを付与し、を設定ファイルに書けばトランザクションは機能する。ここではSpringの宣言的トランザクション管理に関する内部的な仕組みを説明する。

もっとも重要な点は、SpringAOPにより宣言的トランザクションが実現されていることである。そして、トランザクションを実現するAdviceがビジネスロジックに織り込まれるわけである。これにより適切なPlatformTransactionManagerと関連付けられたTransactionInterceptorを用いるAOPプロキシが生成され、メソッド呼出前後にトランザクション制御が行なわれる。

10.5.2 Example of declarative transaction implementation

トランザクションの設定方法について。なおここではXMLによる設定を説明する。アノテーションベースの設定は、10.5.6 Using @Transactionalあたりを見ると良いかと。
以下はシンプルな設定例。

アスペクトを設定するサービスクラス


package x.y.service;
public interface FooService {
Foo getFoo(String fooName);
Foo getFoo(String fooName, String barName);
void insertFoo(Foo foo);
void updateFoo(Foo foo);
}

class DefaultFooService implements FooService {
//省略
}

getFooメソッドはリードオンリーのトランザクションとし、insertFoo/updateFooは更新可能なトランザクションにする。

・設定ファイル


<!-- from the file 'context.xml' -->
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-3.0.xsd
http://www.springframework.org/schema/aop
">http://www.springframework.org/schema/aop/spring-aop-3.0.xsd">

<!-- サービスクラスをSpringIOCに登録 -->
<bean id="fooService" class="x.y.service.DefaultFooService"/>

<!-- トランザクション管理のアドバイスを登録。あとでで登録するPlatformTransactionManagerをtransaction-manageに設定-->
<tx:advice id="txAdvice" transaction-manager="txManager">
<!-- the transactional semantics... -->
<tx:attributes>
<!-- リードオンリーなメソッドを設定 -->
<tx:method name="get*" read-only="true"/>
<!-- その他のメソッドはデフォルト(更新可能) -->
<tx:method name="*"/>
</tx:attributes>
</tx:advice>

<!-- サービスクラスのメソッドをポイントカットとして適用し、このポイントカットにtxAdviceを織込む -->
<aop:config>
<aop:pointcut id="fooServiceOperation" expression="execution(* x.y.service.FooService.*(..))"/>
<aop:advisor advice-ref="txAdvice" pointcut-ref="fooServiceOperation"/>
</aop:config>

<!-- データソースの定義 -->
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="oracle.jdbc.driver.OracleDriver"/>
<property name="url" value="jdbc:oracle:thin:@rj-t42:1521:elvis"/>
<property name="username" value="scott"/>
<property name="password" value="tiger"/>
</bean>

<!-- PlatformTransactionManagerの定義 -->
<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>

<!-- other <bean/> definitions here -->

</beans>

・クライアントコード


public final class Boot {
public static void main(final String[] args) throws Exception {
ApplicationContext ctx = new ClassPathXmlApplicationContext("context.xml", Boot.class);
FooService fooService = (FooService) ctx.getBean("fooService");
fooService.insertFoo (new Foo());
}
}

これを実行すると、ちゃんとこんなログが出ます!


<!-- the Spring container is starting up... -->
[AspectJInvocationContextExposingAdvisorAutoProxyCreator] - Creating implicit proxy
for bean 'fooService' with 0 common interceptors and 1 specific interceptors
<!-- the DefaultFooService is actually proxied -->
[JdkDynamicAopProxy] - Creating JDK dynamic proxy for [x.y.service.DefaultFooService]

<!-- ... the insertFoo(..) method is now being invoked on the proxy -->

[TransactionInterceptor] - Getting transaction for x.y.service.FooService.insertFoo
<!-- the transactional advice kicks in here... -->
[DataSourceTransactionManager] - Creating new transaction with name [x.y.service.FooService.insertFoo]
[DataSourceTransactionManager] - Acquired Connection
[org.apache.commons.dbcp.PoolableConnection@a53de4] for JDBC transaction

<!-- the insertFoo(..) method from DefaultFooService throws an exception... -->
[RuleBasedTransactionAttribute] - Applying rules to determine whether transaction should
rollback on java.lang.UnsupportedOperationException
[TransactionInterceptor] - Invoking rollback for transaction on x.y.service.FooService.insertFoo
due to throwable [java.lang.UnsupportedOperationException]

<!-- and the transaction is rolled back (by default, RuntimeException instances cause rollback) -->
[DataSourceTransactionManager] - Rolling back JDBC transaction on Connection
[org.apache.commons.dbcp.PoolableConnection@a53de4]
[DataSourceTransactionManager] - Releasing JDBC Connection after transaction
[DataSourceUtils] - Returning JDBC Connection to DataSource

Exception in thread "main" java.lang.UnsupportedOperationException
at x.y.service.DefaultFooService.insertFoo(DefaultFooService.java:14)
<!-- AOP infrastructure stack trace elements removed for clarity -->
at $Proxy0.insertFoo(Unknown Source)
at Boot.main(Boot.java:11)

10.5.3 Rolling back a declarative transaction

前にも言ったけど、Spring的にはロールバックする例外を定義する宣言的なやり方がおすすめなんだからねっ!
…で、でもあとでプログラムでやるやり方も教えたげる…。*1


ということで発生例外ごとの扱いに関する設定についてのお話。Springはデフォルトでは非チェック例外が送出されたらロールバックし、チェック例外であればロールバックしない。振る舞いを変えるには、アドバイスに属性を追加する。


・チェック例外を含む全ての例外について、どの例外をロールバックするか指定できる。この例はチェック例外の特定の例外をロールバック対象にしている例。


<tx:advice id="txAdvice" transaction-manager="txManager">
<tx:attributes>
<tx:method name="get*" read-only="true" rollback-for="NoProductInStockException"/>
<tx:method name="*"/>
</tx:attributes>
</tx:advice>

・逆にロールバック対象から外すこともできる。


<tx:advice id="txAdvice">
<tx:attributes>
<tx:method name="updateStock" no-rollback-for="InstrumentNotFoundException"/>
<tx:method name="*"/>
</tx:attributes>
</tx:advice>

・ちなみにもし設定が競合する場合は、より型付けの強いほうが採用される。例えば以下の例ではInstrumentNotFoundException以外のすべての例外でロールバックする。


<tx:advice id="txAdvice">
<tx:attributes>
<tx:method name="*" rollback-for="Throwable" no-rollback-for="InstrumentNotFoundException"/>
</tx:attributes>
</tx:advice>

・で、でね…、プログラムでロールバックするやり方なんだけど…。とっても簡単なんだよ。でもSpringと強く結びついちゃうからできるだけやっちゃだめなの。


public void resolvePosition() {
try {
// some business logic...
} catch (NoProductInStockException ex) {
// trigger rollback programmatically
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}
}

10.5.4 Configuring different transactional semantics for different beans

Bean毎に異なるトランザクションを設定したい場合。普通に、複数アドバイス設定すればいいだけ。疲れたので省略ー。
http://static.springsource.org/spring/docs/3.0.x/spring-framework-reference/html/transaction.html#tx-resource-synchronization#transaction-declarative-diff-tx

10.5.5 <tx:advice/> settings

<tx:advice/>に紐付けるElementである<tx:method/>の設定について。表にまとめます。

属性必須デフォルト説明
nameY-トランザクション属性が関連付けられるメソッド名。*を指定できる。またget*とかon*Eventとかできる。
propagationNREQUIREDトランザクション伝搬に関する設定
isolationNDEFAULTトランザクション分離レベルを設定
timeoutN-1トランザクションタイムアウト(秒)
read-onlyNfalseRead-Onlyであるかを設定
rollback-forN-ロールバック対象とする例外をカンマ区切りで指定
no-rollback-forN-ロールバック対象外とする例外をカンマ区切りで指定

10.5.6 Using @Transactional

アノテーショントランザクションを指定するやり方について。ソースコード中に直接アノテーショントランザクションを指定するやり方。アノテーションを使うと、ロジックとSpringの結合度が挙がることを気にするかもしれないが、そんなに心配しなくてよい…とSpringの人たちは主張している模様です。以下の例を見ると@Transactionアノテーションの使いやすさがわかると思います。

トランザクションを設定するサービスクラス


@Transactional
public class DefaultFooService implements FooService {
Foo getFoo(String fooName);
Foo getFoo(String fooName, String barName);
void insertFoo(Foo foo);
void updateFoo(Foo foo);
}

XMLファイルにはこう書けば良い。


<!-- from the file 'context.xml' -->
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-3.0.xsd
http://www.springframework.org/schema/aop
">http://www.springframework.org/schema/aop/spring-aop-3.0.xsd">

<!-- this is the service object that we want to make transactional -->
<bean id="fooService" class="x.y.service.DefaultFooService"/>

<!-- enable the configuration of transactional behavior based on annotations -->
<tx:annotation-driven transaction-manager="txManager"/>

<!-- a PlatformTransactionManager is still required -->
<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!-- (this dependency is defined somewhere else) -->
<property name="dataSource" ref="dataSource"/>
</bean>

<!-- other <bean/> definitions here -->

</beans>

@Transactionalアノテーションはインタフェース、インタフェース上のメソッド、クラス、クラス上のメソッドに対して付与することができる。付与したアノテーションは<tx:annotation-driven/>を定義することで評価され、トランザクションとしての振る舞いをするようになる。

TIPS*2

@Transactionalは、インタフェースではなく実装クラス(のメソッド)に付与することをおすすめする。インタフェースに定義することも可能だが、そうした場合はインタフェースベースのプロキシを使わないとトランザクションが有効にならない。*3
Javaアノテーションはインタフェースから継承されない(おおっと!*4)ので、CGLIBを用いたクラスに基づくプロキシ*5もしくはAspectJによるアスペクトの織り込み*6をする場合は、インタフェースに付与したアノテーションは使用されないので、トランザクションとして扱われなくなってしまう。

またAspectJではなくプロキシを使用する場合(JavaのProxyかCGLIBを使う場合)は、プロキシを経由した呼出でないとトランザクションとして扱われない*7。逆にaspectjの場合は実体のクラスに織り込まれるのでそのようなことはない。もし自分のインスタンスを呼び出したときにもトランザクション境界にしたい場合は(this.methodってやったときなど)、AspectJを使うようにする必要がある。

<tx:annotation-driven/>の設定

属性デフォルト説明
transaction-managertransactionManager使用するTransactionManagerの名前。
modeproxyproxy:SpringAOPのproxyモードを使ってトランザクションAOPする。
aspectj:AspectJトランザクションを直接織り込む。spring-aspectj.jarが必要。LoadTimeWeavingやComplieTimeWeavingなどはAOPのマニュアル見てね*8
proxy-target-classfalsetrueの場合はCGLIB,falseはJDK DynamicProxy
orderOrdered.LOWEST_PRECEDENCE@Transactionalアスペクトを適用する優先順位

注意:
<tx:annotation-driven/>は同じApplicationContextにある@Transactionalだけを検索する。たとえばWebApplicationContextに<tx:annotation-driven/>を付与した場合、コントローラに付与した@Trasnactionalは検索するけれども、サービスの@Transactionalは検索されないということになる。細かくは15.2章を参照。


メソッドに対して付与した@Transactionalが一番優先される。たとえば次の例を考える。


@Transactional(readOnly = true)
public class DefaultFooService implements FooService {

public Foo getFoo(String fooName) {
// do something
}

// these settings have precedence for this method
@Transactional(readOnly = false, propagation = Propagation.REQUIRES_NEW)
public void updateFoo(Foo foo) {
// do something
}
}

DefaultFooServiceはリードオンリーなトランザクションに設定されているが、updateFooメソッドについては更新可能なトランザクションで、REQUIRES_NEWに上書き設定されている。

10.5.6.1 @Transactional settings

@Transactionalをインタフェース・クラス・メソッドに付与すると、例えば「新規のリードオンリートランザクションを始めろ」などといったトランザクションに関する意味付けを与えることができる。デフォルトの設定は以下のとおり。

これらは@Transactionalの属性で変更可能。

valueStringトランザクションマネージャを特定する識別子
propagationenum: Propagationトランザクション伝播属性を設定。
isolationenum: Isolationトランザクション分離レベルを設定。
readOnlybooleanリードオンリーなトランザクションかどうかを指定する。
timeout秒で指定トランザクションタイムアウト時間。
rollbackForThrowableを実装する例外クラスの配列ロールバックする例外を指定。
rollbackForClassnameThrowableを実装する例外クラス名の配列ロールバックする例外を指定。
noRollbackForThrowableを実装する例外クラスの配列ロールバックしない例外を指定。
noRollbackForClassnameThrowableを実装する例外クラス名の配列ロールバックしない例外を指定。

#ここは10.5.5 <tx:advice/> settingsと本質的に同じね。

トランザクションの名前を明示することはできない。トランザクション名とは、Weblogicなどのトランザクションモニタで取得できる名称であり、(たぶんトランザクションモニタの)ログに出力される。宣言的トランザクションでは、トランザクション名はトランザクション名は常にトランザクションのアドバイスを注入されたクラスのFQCN+メソッド名となる。

10.5.6.2 Multiple Transaction Managers with @Transactional

複数のトランザクションを扱う場合について。この場合は@Transactionalアノテーションに使用するPlatformTransactionManagerを指定する識別子を指定する。具体的には、IoCコンテナに登録したPlatformTransactionManagerのbean名もしくはqualifierの値で指定される。例えば以下のとおり書く。

複数のトランザクションを扱うコード:


public class TransactionalService {

@Transactional("order")
public void setSomething(String name) { ... }

@Transactional("account")
public void doSomething() { ... }
}

XMLファイル:


<tx:annotation-driven/>

<bean id="transactionManager1" class="org.springframework.jdbc.DataSourceTransactionManager">
...
<qualifier value="order"/>
</bean>

<bean id="transactionManager2" class="org.springframework.jdbc.DataSourceTransactionManager">
...
<qualifier value="account"/>
</bean>

10.5.6.3 Custom shortcut annotations

このままだとあちこちのメソッドに同じような@Transactionalをくっつけることになり重複定義となる。Springメタアノテーション機能を使うと、同じ意味を持つアノテーションを表すショートカットアノテーション*9を定義できる。

こんな感じで定義したアノテーションを:


@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Transactional("order")
public @interface OrderTx {
}

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Transactional("account")
public @interface AccountTx {
}

こんな感じで付与する。


public class TransactionalService {

@OrderTx
public void setSomething(String name) { ... }

@AccountTx
public void doSomething() { ... }
}

10.5.7 Transaction propagation

Springにおけるトランザクション伝播について説明する。なお前提として、Springでは物理的なトランザクションと論理的なトランザクションを区別していることを念頭においてください。*10

10.5.7.1 Required

これの意味自体は以下見れ。

PROPAGATION_REQUIREDの場合、内側のトランザクションがrollback-markを付与した場合はそれは外側のトランザクションに伝搬する。もし内側のトランザクションが指示かにrollback-markを付与したが、外側のトランザクションで明示的にロールバックしなかった場合は*11、外側のトランザクションスコープに対するインターセプターでロールバックする際に、UnexpectedRollbackExceptionを送出する。よって外側のトランザクションを呼び出す人は、UnexpectedRollbackExceptionを必要に応じて適切に処理する必要がある。*12

10.5.7.2 RequiresNew

これの意味自体は以下見れ。

PROPAGATION_REQUIRES_NEWは、それぞれのトランザクションスコープは完全に独立する。物理的なトランザクションはそれぞれのスコープに閉じており、トランザクションの状態がスコープをまたがって伝搬することはない。

10.5.7.3 Nested

ようするに、JDBC3.0のセーブポイントAPI機能を使って、ネストされたトランザクションを実現することです。トランザクション境界毎にセーブポイントが設定され、トランザクションがネストします。以下参照すると幸せになるかも。

http://db.apache.org/derby/docs/dev/ja_JP/ref/crefjavsavesetroll.html
http://forum.springsource.org/archive/index.php/t-16594.html

以下はSpringMLにあった大事なこと。2度言うべき。


>PROPAGATION_NESTED on the other hand starts a "nested" transaction,
>which is a true subtransaction of the existing one.
>What will happen is that a savepoint will be taken at the start of the nested transaction.
>If the nested transaction fails, we will roll back to that savepoint.
>The nested transaction is part of of the outer transaction, so it will only be committed
>at the end of of the outer transaction.

10.5.8 Advising transactional operations

SpringAOPで自分で設定するアドバイスと、<tx:annotation-driven/>で設定されるアドバイスの優先度設定の仕方。アスペクトに設定する優先度(order)でコントロールする。<tx:annotation-driven/>に属性orderを指定することで、トランザクションアスペクトの適用優先順位を指定できるので、あとはhttp://d.hatena.ne.jp/minokuba/20110302/1299075764の7.2.4.7 Advice orderingなどを参考にすると良い。

以下はアスペクトXMLで設定する場合のサンプルコード。単純化のために内容ちょっとかえた。

アスペクト


package x.y;

import org.aspectj.lang.ProceedingJoinPoint;
import org.springframework.util.StopWatch;
import org.springframework.core.Ordered;

public class SimpleProfiler implements Ordered {

//アスペクトの優先度は1
public int getOrder() {
return 1;
}

public Object profile(ProceedingJoinPoint call) throws Throwable {
Object returnValue;
StopWatch clock = new StopWatch(getClass().getName());
try {
clock.start(call.toShortString());
returnValue = call.proceed();
} finally {
clock.stop();
System.out.println(clock.prettyPrint());
}
return returnValue;
}
}

XMLファイル


<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-3.0.xsd
http://www.springframework.org/schema/aop
">http://www.springframework.org/schema/aop/spring-aop-3.0.xsd">

<!--アスペクトを織込むBeanの定義-->
<bean id="fooService" class="x.y.service.DefaultFooService"/>

<!--アスペクトの定義-->
<bean id="profiler" class="x.y.SimpleProfiler"/>

<!--トランザクションの設定。アスペクトの優先度は200-->
<tx:annotation-driven transaction-manager="txManager" order="200"/>

<!--アスペクトの折り込み-->
<aop:config>
<!-- this advice will execute around the transactional advice -->
<aop:aspect id="profilingAspect" ref="profiler">
<aop:pointcut id="serviceMethodWithReturnValue"
expression="execution(!void x.y..*Service.*(..))"/>
<aop:around method="profile" pointcut-ref="serviceMethodWithReturnValue"/>
</aop:aspect>
</aop:config>

<!--データソースとトランザクションマネージャの設定-->
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
…(略)
</bean>
<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
</beans>

こう設定すると、優先順位通り①SimpleProfiler→②トランザクション開始→③DefaultFooService→④トランザクション決着→SimpleProfilerの順に呼ばれる。

10.5.9 Using @Transactional with AspectJ

こいつは後回し。AspectJ勉強するまで後回し。

*1:疲れたのでツンデレ口調。たぶん@NHK_PRの影響だと思われ

*2:そもそもこれはトランザクション以外の普通のアスペクトについても同じ話だ

*3:要確認:JavaのProxy使用した場合にクラスにアノテーションしたらそれは有効なのか?たぶん有効だったはず

*4:Wisardly的な事書いてみたかっただけです。

*5:proxy-target-class="true"

*6:mode="aspectj"

*7:これはアスペクトに書いてあったthis呼出の件と同じですね。

*8:素朴な疑問なんだけどSpringAOPの設定と、<tx:annotation-driven/>の設定で、proxyモードやproxy-target-classの設定が競合したらどうなるんだろう?

*9:主観:このまま使うのは正直イマイチ。トランザクションは、トランザクションではなくアプリのセマンティクスで引っかけたいと思わない?つまり@Serviceと同粒度のアノテーションソースコード上は記述し、プロパティで紐づけるべき。

*10:補足:#トランザクション属性の網羅的な意味では以下を見るとよさそう。ただSpring2.0時代でしかなさげでアノテーションについて描いてない。http://www.techscore.com/tech/Others/Spring/6.html

*11:TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()を外側のトランザクションで呼べば例外起きなくなる

*12:個人的にはこの仕様いまいち。ロールバックロールバック、例外は例外じゃないかな。例外によりロールバックを支持するがビジネスロジックとして戻り値返す場合は別のインターセプタでTransactionAspectSupport.currentTransactionStatus().setRollbackOnly()するのかい?