Spring Framework 入門記 Transactionその1 TransactionProxyFactoryBean

AOP編を終えてちょっとお休みしている間に,日記の内容が様変わりしてしまいました.職場で見るとなんて異様なんだろう.こんなの見てたら怒られますがな.心より恥じる.
さて,今回からは「Chapter 6. Transaction management」に突入です.
IoCコンテナAOPを使う動機として,最も分かりやすいのがこのトランザクション管理ですね.EJBの数少ない魅力ともいえるあのCMT(Container Managed Transaction)を,EJBなんぞ使わずとも利用できるという,ありがたいしろものです.ということで,はじめませう.


なんでもSpring的には,トランザクションはグローバルトランザクションとローカルトランザクションに分類されるとのことです.

グローバルトランザクション
リソースから独立したトランザクションで,通常はJTAにより制御されます.
ローカルトランザクション
JDBC,JDO,JMS,HibernateiBATISなど,APIや実装固有の方法で制御されます.

Springは,これらのトランザクションを統一的に扱うために,PlatformTransactionManagerというinterfaceを用意しています.
ふーん.JTAUserTransactionを利用するんじゃダメなの? って気もしたんですが,どうせコンテナにお任せなのだからいいか.
PlatformTransactionManagerは,次のメソッドを持っています.

  • TransactionStatus getTransaction(TransactionDefinition definition)
  • void commit(TransactionStatus status)
  • void rollback(TransactionStatus status)

ちょっと分かりにくいですが,getTransaction(TransactionDefinition)は,現在のスレッドでトランザクションが開始されていなければそれを開始してくれたりします.UserTransaction#begin()相当ということですね.ただし,実際の動きは引数であるTransctionDefinitionによって異なります.
ということで,TransactionDefinitionを見ると,次のように4つのプロパティに対するgetterを持つinterfaceのようです.

  • int getIsolationLevel()
  • int getPropagationBehavior()
  • int getTimeout()
  • boolean isReadOnly()

readOnlyなプロパティは,Hibernateで利用されるようです.
ここでの注目は,getPropagationLevel()です.これは,EJBトランザクション属性に相当するもので,次の値がTransctionDefinitionに定義されています.

  • PROPAGATION_REQUIRED
  • PROPAGATION_SUPPORTS
  • PROPAGATION_MANDATORY
  • PROPAGATION_REQUIRES_NEW
  • PROPAGATION_NOT_SUPPORTED
  • PROPAGATION_NEVER

ふむふむ.EJBそのまんまですね.
この値と現在のスレッドにおけるトランザクションの状況により,PlatformTransactionManager#getTransaction(TransactionDefinition)の動きが変わります.このあたりもEJBそのまんまと考えてよさそうです.見ていませんが,おそらく.普通意図的にそうしますよね.違ったら... 心より恥じる.
そして,PlatformTransactionManager#getTransaction(TransactionDefinition)が返すTransactionStatusを指定してcommit/rollbackするわけですが,本当にトランザクションがcommit/rollbackされるのは,PlatformTransactionManager#getTransaction(TransactionDefinition)で新規のトランザクションが開始された場合のみです.
一応TransactionStatusも見ておくと,これは2つのプロパティへのgetter/setterを持つinterfaceです.

  • boolean isNewTransaction()
  • void setRollbackOnly()
  • boolean isRollbackOnly()

こちらも一目瞭然ですね.


Springでは,PlatformTransactionManagerの実装として,次のclassを用意しています.

JtaTransactionManager
JTAUserTransactionを利用してトランザクション制御を行います.
DataSourceTransactionManager
JDBCConnectionを利用してトランザクション制御を行います.
JdoTransactionManager
JDOのPersistenceManagerFactoryを利用してトランザクション制御を行います.
HibernateTransactionManager
HibernameのSessionFactoryを利用してトランザクション制御を行います.

おもしろいことに,Springでは固有のJTA実装は提供していないようです.必要ならアプリケーションサーバが提供するJTA実装を使うとか,ObjectWebのJOTMを使うとかしろってことみたいです.このあたりのアプローチも,S2とは随分違いますね.
とまぁそういうわけなんで,ここではDataSourceTransactionManagerを見ていくことにしましょう.


このトランザクションマネージャは,JDBCConnectionが提供するローカルトランザクション(この言葉,覚えてましたか? ちょっと上で書いたやつです)を制御します.ということは,分散トランザクションには対応しません.無念だ.うんにゃ,そういう場合にのみ使うものなので,無念って事はありませんね.心より恥じる.
このclassは,dataSourceというプロパティを持っています.そう,たいていはコネクションプーリングを提供してくれる,あのDataSourceです.それだけです.簡単に使えそう.
しかしですね,ここでもS2と異なり,Springは固有のDataSourceの実装を提供しないようです.やはりアプリケーションサーバが提供するデータソースを使うとか,Apache Jakarta CommonsのDBCPを使うとかしろってことみたいです.
このように,任意のデータソースを使えることがメリットとなる場合もあるでしょうが,デメリットになる場合もあります.その一つは,トランザクションマネージャとデータソースが密に連携することができないということです.DataSourceの仕様では,スレッドコンテキストについて何も書かれていないようです(少なくともJavaDocでは).ということは,あるスレッド上でDataSource#getConnection()を複数呼び出した場合,同一のConnectionが返ってくると決め付けてはいけないわけで,それだとトランザクションマネージャの実装はたいへんになりそう.
そんなわけで,SpringはDataSource#getConnection()を直接使わず,代わりにDataSourceUtils#getConnection(DataSource)を使いましょう,ということになってます.こいつがスレッドコンテキストを提供するわけです.うーん,なんだかなぁー.
S2の場合はトランザクションマネージャと連携する独自のデータソース実装が提供されるため,アプリケーションは普通にDataSource#getConnection()を呼び出すことができます.どっちが魅力的か... 自分はS2だなぁ.
まぁ,忘れましょう.
とにかく,DataSourceさえあればDataSourceTransactionManagerを使うことができます.使い方は,次の3とおり解説されています.

  • プログラマティックに使う.
  • TransactionProxyFactoryBeanを使う.
  • BeanNameAutoProxyCreatorを使う.

他にも,独自のProxyFactoryBeanを作るとか,ProxyFactoryBeanを使ってがんばって組み立てるとかできそうです.
ということなのですが,どう考えてもプログラマティックにやりたいとは思えないので,今回はTransactionProxyFactoryBeanを使ってみましょう.


TransactionFactoryBeanは,「AOPその12 My ProxyFactoryBean」で学習した,カスタムなProxyFactoryBeanです.これを使うことで,トランザクションマネージャへの操作を行ってくれるAspectを容易に(?)Weavingで着るようになります.
TransactionFactoryBeanProxyFactoryBeanですから,targetなどのプロパティはそのまま同じように使えます.それに加えて,固有のプロパティがいくつかあります.

  • transactionManager
  • transactionAttributes
  • preInterceptors
  • postInterceptors

transactionManagerは簡単ですね.PlatformTransactionManager実装クラスを与えてあげればOKです.
transactionAttributesが重要で,ここでAspectを組み込むメソッドと,そのトランザクション属性を指定します.
transactionAttributesプロパティの型はPropertiesで,そのキーはメソッド名(ワイルドカード'*'が使えます)です.値には,前述のTransactionDefinitionPROPERGATION〜を指定します.その後に,カンマ(',')で区切っていくつかのオプションを指定することができます.
その一つは"readOnly"で,リードオンリーのトランザクションであることを示します.
もう一つは,先頭が'+'または'-'で,その後に例外のクラス名が続くものです.これは,その例外が発生した場合にトランザクションをコミットするか('+'),ロールバックするか('-')を示します.ちなみに指定されていない例外の場合,ErrorまたはRuntimeExceptionならロールバック,それ以外はコミットのようです.EJBと同じ((RemoteExceptionは別として))... でしたよね? 忘れてしまった.心より恥じる.
あとの2つのプロパティは,トランザクションAspectの前後に適用するAspectを指定するものです.


よし,だいたいこんなところで予習完了,実践に入りましょう.
今回いやーんなのは,なにかしらDBMSを用意しなければならないことですね.面倒だなぁ.こんなときはHSQLDBです.そうに決まっています.いやその,あまり使ったことないんですけど.心より恥じる.ともかく,HSQLDBとCommons DBCPの組み合わせでお試しすることにしましょう.
まず何を作るかといったら,DBを操作するクラスです.ものすっごーく手抜きですが,こんな感じで.

package study;
import java.sql.ResultSet;
import java.sql.Statement;
import javax.sql.DataSource;
import org.springframework.jdbc.datasource.DataSourceUtils;

public class Foo {
    private DataSource dataSource;
    public DataSource getDataSource() {
        return dataSource;
    }
    public void setDataSource(DataSource dataSource) {
        this.dataSource = dataSource;
    }
    public void createTable() throws Throwable {
        Statement st = DataSourceUtils.getConnection(dataSource).createStatement();
        st.execute("create table pair (key varchar, value varchar, primary key(key))");
    }
    public String query(String key) throws Exception {
        Statement st = DataSourceUtils.getConnection(dataSource).createStatement();
        ResultSet rs = st.executeQuery("select value from pair where key='" + key + "'");
        rs.next();
        return rs.getString(1);
    }
    public void insert(String key, String value, Throwable throwable) throws Throwable {
        Statement st = DataSourceUtils.getConnection(dataSource).createStatement();
        st.execute("insert into pair (key, value) values('" + key + "', '" + value + "')");
        if (throwable != null) {
            throw throwable;
        }
    }
    public void update(String key, String value, Throwable throwable) throws Throwable {
        Statement st = DataSourceUtils.getConnection(dataSource).createStatement();
        st.execute("update pair set value='" + value + "' where key='" + key + "'");
        if (throwable != null) {
            throw throwable;
        }
    }
}

心より恥じる.
ともかくですね,insert/updateの最後の引数で例外オブジェクトが渡されると,それをスローするようにしています.
で,これを使った定義ファイル.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
    <bean id="dataSource" 
        class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
        <property name="driverClassName"><value>org.hsqldb.jdbcDriver</value></property>
        <property name="url"><value>jdbc:hsqldb:.</value></property>
        <property name="username"><value>sa</value></property>
        <property name="password"><value></value></property>
        <property name="minIdle"><value>1</value></property>
    </bean>

    <bean id="transactionManager"
        class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource"><ref local="dataSource"/></property>
    </bean>

    <bean id="debug" class="org.springframework.aop.interceptor.DebugInterceptor"/>

    <bean id="foo" 
        class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean">
        <property name="target">
            <bean class="study.Foo">
                <property name="dataSource"><ref bean="dataSource"/></property>
            </bean>
        </property>
        <property name="transactionManager"><ref bean="transactionManager"/></property>
        <property name="transactionAttributes">
            <props>
                <prop key="insert">PROPAGATION_REQUIRED,+java.lang.UnsupportedOperationException</prop>
                <prop key="update">PROPAGATION_REQUIRED,-java.io.IOException</prop>
                <prop key="*">PROPAGATION_REQUIRED</prop>
            </props>
        </property>
        <property name="postInterceptors"><ref bean="debug"/></property>
    </bean>
</beans>

なかなか大変な感じですねぇ.
冒頭でデータソースを定義しています.そのすぐ後でトランザクションマネージャを定義.
そして,TransactionProxyFactoryBeanを使ってFooトランザクションAspectをWeavingしています.そのpostInterceptorとして,おなじみのDebugInterceptorを指定しています.
で,実行用のクラス.

package study;
import java.io.IOException;
import org.springframework.beans.factory.access.BeanFactoryLocator;
import org.springframework.beans.factory.access.BeanFactoryReference;
import org.springframework.context.ApplicationContext;
import org.springframework.context.access.ContextSingletonBeanFactoryLocator;

public class Main {
    public static void main(String[] args) {
        try {
            BeanFactoryLocator locator = ContextSingletonBeanFactoryLocator.getInstance();
            BeanFactoryReference ref = locator.useBeanFactory("context");
            ApplicationContext context = (ApplicationContext) ref.getFactory();

            Foo foo = (Foo) context.getBean("foo");
            foo.createTable();
            foo.insert("Yuri", "Ebihara", null);
            System.out.println(foo.query("Yuri"));

            try {
                foo.insert("Akiko", "Yada", new UnsupportedOperationException());
            }
            catch (UnsupportedOperationException ignore) {
            }
            System.out.println(foo.query("Akiko"));

            try {
                foo.update("Akiko", "Wada", new IOException());
            }
            catch (IOException ignore) {
            }
            System.out.println(foo.query("Akiko"));

            ref.release();
        }
        catch (Throwable e) {
            e.printStackTrace();
        }
    }
}

こいつを実行!!!

 Debug interceptor: count=1 invocation=[Invocation: method=[public void study.Foo.createTable 以下略
 Debug interceptor: next returned
 - Initiating transaction commit
 Debug interceptor: count=2 invocation=[Invocation: method=[public void study.Foo.insert 以下略
 Debug interceptor: next returned
 - Initiating transaction commit
 Debug interceptor: count=3 invocation=[Invocation: method=[public java.lang.String study.Foo.query 以下略
 Debug interceptor: next returned
 - Initiating transaction commit
 Ebihara
 Debug interceptor: count=4 invocation=[Invocation: method=[public void study.Foo.insert 以下略
 - Initiating transaction commit
 Debug interceptor: count=5 invocation=[Invocation: method=[public java.lang.String study.Foo.query 以下略
 Debug interceptor: next returned
 - Initiating transaction commit
 Yada
 Debug interceptor: count=6 invocation=[Invocation: method=[public void study.Foo.update 以下略
 - Invoking rollback for transaction on method 'update' in class [study.Foo] due to throwable [java.io.IOException]
 - Initiating transaction rollback
 Debug interceptor: count=7 invocation=[Invocation: method=[public java.lang.String study.Foo.query 以下略
 Debug interceptor: next returned
 - Initiating transaction commit
 Yada

という具合に,Springがトランザクションを制御してくれている様子がうかがえます.
2番目のinsert()では,通常ならロールバックされるUnsupportedOperationExceptionをスローしているのですが,ちゃんとコミットされています.
逆に,update()では通常はコミットされるIOExceptionをスローしているのですが,ロールバックされています.定義ファイルの"-java.io.IOException"がないと,矢田亜希子和田アキ子に更新されてしまいます.危ない危ない.


という感じで,たいしてつまずくこともなく,無事にトランザクションAspectを利用することが出来ました.その割には時間かかったなーって思ったら,いつもより文字数が多いみたい? 無念だ.
次回は,BeanNameAutoProxyCreatorを使ってみようと思います.

お仕事スタイル

いかん,お仕事スタイルは入門記の直前にアップすることにしていたのに,忘れてしまった.無念だ.
今日は最高気温が28度だとか聞いたので,薄着です.さぁ,帰りは大丈夫か? 先週は寒かったんですが.
っていうより,職場のエアコン効きすぎ.ちょっと寒いよ.
00:05追記
ちょー寒かったよっ! まだこの格好は無理! 5月までは何か羽織る!

日記の著作権とか?

自分が書いた入門記なのに,もう忘れちゃってることがあったりします.あうあう.心より恥じる.
そんなわけで読み返すこともあるのですが,これが読みにくい!
1回分だけ読むのならいいのですが,連載(?)を続けて読もうとすると,下に向かって読んだ後上に向かわなきゃいけなかったりするのがよくないっぽい.ちゃんとした目次もないし.
ということで,一段落したAOP編までをWikiにでも持っていこうかと思ったのですが,ここに書いた内容の著作権とかって大丈夫なんでしょうか? 規約を見ても,「はてなは責任を負いません」みたいなのしか見当たらなくて,特に何も主張しているようには見えないんですが.
何も問題ないのなら,本家からリンクされているSpring Padにお願いして天才^H^H転載(っていうの?)させてもらいたいなぁって思ってます.
その辺詳しい方,教えてくださいませ.あるいはここを読めとかコメントいただけるとうれしいです.お願いします.
01:15 追記
はてなは規約で何も主張していないんだから,著作権は原著作者に帰属って事でいいみたい?
いろいろぐぐってみたのですが,これといったものが引っかからないので,何も問題ないと解釈してみるてすと.