トランザクションの設定に関する設計指針

ひとまず、開発実践がない状態なので妄想に過ぎませんがだらだらメモってみます。

Spring Transactionの機能を用いると、トランザクションの設定をIoCコンテナの設定ファイル(XMLファイル)を用いて行うことができます。またアノテーションを用いて行うこともできます。初心者的に一番簡単なのは後者の@Transactionalを付与するやり方です。このやり方の場合は、トランザクショナルにしたいクラスやメソッドに


@Transactional(propagation=Propagation.REQUIRES_NEW)
public void service() throws Exception{
とか付ければいいわけです。逆にIoCコンテナの設定ファイルで指定するには

<tx:advice id="txAdvice" transaction-manager="transactionManager"/>
<aop:config>
<aop:pointcut id="serviceOperation"
expression="execution(public * hello.spring.transaction.*TransactionalService.*(..))
<aop:advisor advice-ref="txAdvice" pointcut-ref="serviceOperation"/>
</aop:config>
とか書けばいいわけです*1アノテーションのほうがたくさん記述する場合は楽なので、アプリケーション開発者が個別に定義する必要がある場合はアノテーションにしたいですね。逆に、システムでひとつしかないようなパターンを定義するには、IoCコンテナの設定ファイルを使用したほうが良さそうですね。*2

んで。Springの中の人的には@Transactionalによる設定をオススメしているらしいですが、正直いまいちだと思います。トランザクションの発生箇所ごとに同じような@Transactionalを設定して回るのは頭おかしすぎると思います。…とか言うと、Springの中の人は今度は「じゃーカスタムアノテーション作れよ!」と言ってくるわけです*3

たしかにカスタムアノテーションを使えば設定値は使いまわせます。でもカスタムアノテーションを一つ一つ、トランザクションを適用する場所に付与して回るのは、AOPの肝である「アドバイスを注入する箇所の定義の一極集中化」が実現できないのでの全然嬉しくありません。一般的には、アプリケーションの構造上、トランザクションを適用する場所はだいたいきまっており、いわゆるサービスクラスのメソッドで開始終了するのが普通でしょう。このトランザクションに対する設定値は、普通はプロジェクトのデフォルト値を適用しておけばよいはずで、個別に変更されることは余りないと思います。*4。こう言ったものこそAOPらしく扱うべきです。つまり、サービスクラスのメソッドをポイントカットとして定義し、これに対してトランザクションを設定するのが当たり前のやり方かなと思います。

例えば全ての@Serviceを付与したpublicメソッドをトランザクションの対象とする設定として


<tx:advice id="txAdvice" transaction-manager="transactionManager"/>
<aop:config>
<aop:pointcut id="serviceOperation"
expression="execution(public * *(..)))
and @target(org.springframework.stereotype.Service)"/>
<aop:advisor advice-ref="txAdvice" pointcut-ref="serviceOperation"/>
</aop:config>
とかしちまえと。こうすれば一つ一つトランザクションの設定を書く必要がなく、設定ファイルの記述で一元化できるので、とてもハッピーですね!

ただし、実際はレアケースってやつが出てきます。

  1. このメソッドだけはどうしてもトランザクションの扱い変えたいんだYo!
  2. あとからバッチ処理とか出てきてねー、サービスクラス以外のメソッドにもトランザクション入れたいんですよ
  3. 一意番号採番するDAOはノントラでよろしく!
  4. プロジェクトのルールに沿ってないクラスが幾つかあってですね。ええ納期的に今更直せないんですよ(最悪)

基本的にこれらは(若干強引ですが)プロジェクトのルールを適用できない例外ケースと考えます。APIの設計としては、デフォルト値は一箇所で定義して、それを個別に上書き設定できる設計が綺麗だと思います。この場合はプロジェクトのルールである「@Serviceを付与した全てのpublicメソッドに、プロジェクトとしてのデフォルト値でトランザクション開始する」を、アプリケーション全体のデフォルト設定とし、上記以外の個別の例外ケースはそれぞれ個別に上書き設定することにします。ここで、個別の上書き設定を設定ファイルに記述すると、設定ファイルにサービスクラスの具象クラス名などが登場することになるでしょう。こうしてしまうと、設定ファイルと実装クラスの結合度があがってしまいます。結果としてファイルがなんかゴチャついてきたり、将来的に実装クラスを怖くてリファクタできないなーんて事態を招きそうな気がします*5。こういうのはアノテーションで指定してあげると良いかなと思いました。具体的には

  1. 設定ファイルにプロジェクトのデフォルト設定を記述する。。
  2. 設定ファイルに記述したデフォルト設定に従えない場合は、個別のトランザクションの設定内容を@Transactionalを付与することで上書きする。
  3. そもそも1に該当しない箇所にトランザクションを適用する場合も、2と同様個別に@Transaactionalを設定する。*6

でよさそうです。@Tranasactionalの設定値のダブリ(二重定義)を避けるためには、カスタムアノテーションを作成すればよいかなと*7アノテーションを付与する箇所が限定されているなら、ありかと思います。

ただし、単純にIoC設定ファイルに記述した内容を上書きするつもりで、サービスクラスの個別のメソッドに@Transactionalを付与すると、TransactionalInterceptorが二つ設定されてしまいます。P6Spyとかでログ観てるとしっかり2回commitとか打たれてしまう。これは悲しい。悲しいですよお母さん。というわけでTransactionalInterceptorが二重に設定されないように設定ファイルに定義してあげればいいわけですね。
ということでサンプル。

XMLファイル。P6とか余計な設定あるのはすいません。


<?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:context="http://www.springframework.org/schema/context"
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/context
http://www.springframework.org/schema/context/spring-context-3.0.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-3.0.xsd
http://www.springframework.org/schema/tx
">http://www.springframework.org/schema/tx/spring-tx-3.0.xsd">

<bean id="jdbcDataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="org.apache.derby.jdbc.ClientDriver" />
<property name="url" value="jdbc:derby://localhost:1527/sampledb" />
<property name="username" value="admin" />
<property name="password" value="admin" />
</bean>

<!--データソース-->
<bean id="dataSource" class="com.p6spy.engine.spy.P6DataSource">
<constructor-arg>
<ref local="jdbcDataSource"/>
</constructor-arg>
</bean>

<!--トランザクションマネージャ。ここではデフォルト名のトランザクションマネージャを登録。-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>

<!--ステレオタイプのスキャン+@Aspectのスキャン-->
<context:component-scan base-package="hello.spring.transaction">
<context:include-filter type="annotation" expression="org.aspectj.lang.annotation.Aspect" />
</context:component-scan>

<!--デフォルトのトランザクションに関する設定。伝搬属性をREQUIREDとする。-->
<tx:annotation-driven/>
<tx:advice id="txAdvice" transaction-manager="transactionManager"> <tx:attributes>
<tx:method name="*" propagation="REQUIRED"/>
</tx:attributes>
</tx:advice>

<!--@Serviceを付与し、クラスorメソッドに@Transactionalが付与されていない全てのpublicメソッドにtxAdviceを適用-->
<!--パッケージを絞り込んだのは循環参照エラーを避けるため-->
<aop:config>
<aop:pointcut id="serviceOperation"
expression=
"execution(public * hello.spring.transaction.*.*(..))
and @target(org.springframework.stereotype.Service)
and !@annotation(org.springframework.transaction.annotation.Transactional)
and !@target(org.springframework.transaction.annotation.Transactional)"/>
<aop:advisor advice-ref="txAdvice" pointcut-ref="serviceOperation"/>
</aop:config>
</beans>

サービスクラスその1


@Service
public class TransactionalService {

@Resource(name="dataSource")
private DataSource dataSource;

@Autowired
private NestTransactionalService nestTransaction = null;

//@Serviceが付与されているので、デフォルトのトランザクション属性
public void execute() throws Exception{
executeSQL();
nestTransaction.service();
}
}

サービスクラスその2


@Service
public class NestTransactionalService {

@Resource(name="dataSource")
private DataSource dataSource;

//@Serviceが付与されているが@Transactionalが設定されている場合は@Transactionalで上書き。
@Transactional(propagation=Propagation.REQUIRES_NEW)
public void service() throws Exception{
executeSQL();
}
}

NestTransactionalServiceで例外投げるとこんな感じのログが出て、

main INFO 00:01:16:345 p6spy (95) [rollback] 5: 0[ms]
main INFO 00:01:16:349 p6spy (95) [rollback] 6: 6[ms]
java.lang.RuntimeException
at hello.spring.transaction.NestTransactionalService.service(NestTransactionalService.java:21)
at hello.spring.transaction.NestTransactionalService$$FastClassByCGLIB$$57404ab9.invoke()
at net.sf.cglib.proxy.MethodProxy.invoke(MethodProxy.java:191)
at org.springframework.aop.framework.Cglib2AopProxy$CglibMethodInvocation.invokeJoinpoint(Cglib2AopProxy.java:688)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:150)
at org.springframework.aop.aspectj.MethodInvocationProceedingJoinPoint.proceed(MethodInvocationProceedingJoinPoint.java:80)
at hello.spring.transaction.RollbackAspect.invoke(RollbackAspect.java:22)
…(略)
main INFO 00:01:16:354 p6spy (95) [commit] 1: 0[ms]
main INFO 00:01:16:355 p6spy (95) [commit] 2: 1[ms]

ちゃんとNestTransactionalServiceのトランザクションロールバックされ、TransactionalServiceのトランザクションはコミットされることが確認できました。

え。ログが二行出てる。えーこれはp6が何故か二回吐くんですよね…なんでだろう。本質的でないので割(ry

*1:詳細はhttp://d.hatena.ne.jp/minokuba/20110501/1304265347の10.5.6.3 Custom shortcut annotationsのあちこちを参照w

*2:実際には普通のAOPだったらアスペクトに直接ポイントカット書いてしまえば設定ファイルに書く必要もないわけだけど。この場合は既製のアスペクトを使うのでそーゆー話になると。

*3:詳細はhttp://d.hatena.ne.jp/minokuba/20110501/1304265347参照w

*4:とか書いていてトランザクションタイムアウト時間は違うかもと思ってしまった。これ、この設計だとまずいかもしれず

*5:SpringIoCの設定ファイル編集用のツールはまだ知識がない段階でモノを言っています…ということに今気が付きました。そんなにリスクないですかね。。。

*6:もちろん適用対象がある程度一般化できるなら設定ファイルに引き上げる

*7:設定ファイルに外出しする案もありそうだけどまだ考えてない。そのうち考える