Lakers 98 - 84 Rockets
\(^o^)/
序盤苦しみながらも,後半突き放した模様.
コービー36点,やっぱこれくらい取ってもらわないとね.いよっ,大黒柱!
さぁ,この調子で一気にいって欲しいぞ.
Spring Framework 入門記 Transactionその1 TransactionProxyFactoryBean
AOP編を終えてちょっとお休みしている間に,日記の内容が様変わりしてしまいました.職場で見るとなんて異様なんだろう.こんなの見てたら怒られますがな.心より恥じる.
さて,今回からは「Chapter 6. Transaction management」に突入です.
IoCコンテナ+AOPを使う動機として,最も分かりやすいのがこのトランザクション管理ですね.EJBの数少ない魅力ともいえるあのCMT(Container Managed Transaction)を,EJBなんぞ使わずとも利用できるという,ありがたいしろものです.ということで,はじめませう.
なんでもSpring的には,トランザクションはグローバルトランザクションとローカルトランザクションに分類されるとのことです.
- グローバルトランザクション
- リソースから独立したトランザクションで,通常はJTAにより制御されます.
- ローカルトランザクション
- JDBC,JDO,JMS,Hibernate,iBATISなど,APIや実装固有の方法で制御されます.
Springは,これらのトランザクションを統一的に扱うために,PlatformTransactionManager
というinterface
を用意しています.
ふーん.JTAのUserTransaction
を利用するんじゃダメなの? って気もしたんですが,どうせコンテナにお任せなのだからいいか.
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
- JTAの
UserTransaction
を利用してトランザクション制御を行います. DataSourceTransactionManager
- JDBCの
Connection
を利用してトランザクション制御を行います. JdoTransactionManager
- JDOの
PersistenceManagerFactory
を利用してトランザクション制御を行います. HibernateTransactionManager
- Hibernameの
SessionFactory
を利用してトランザクション制御を行います.
おもしろいことに,Springでは固有のJTA実装は提供していないようです.必要ならアプリケーションサーバが提供するJTA実装を使うとか,ObjectWebのJOTMを使うとかしろってことみたいです.このあたりのアプローチも,S2とは随分違いますね.
とまぁそういうわけなんで,ここではDataSourceTransactionManager
を見ていくことにしましょう.
このトランザクションマネージャは,JDBCのConnection
が提供するローカルトランザクション(この言葉,覚えてましたか? ちょっと上で書いたやつです)を制御します.ということは,分散トランザクションには対応しません.無念だ.うんにゃ,そういう場合にのみ使うものなので,無念って事はありませんね.心より恥じる.
この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で着るようになります.
TransactionFactoryBean
はProxyFactoryBean
ですから,target
などのプロパティはそのまま同じように使えます.それに加えて,固有のプロパティがいくつかあります.
transactionManager
transactionAttributes
preInterceptors
postInterceptors
transactionManager
は簡単ですね.PlatformTransactionManager
実装クラスを与えてあげればOKです.
transactionAttributes
が重要で,ここでAspectを組み込むメソッドと,そのトランザクション属性を指定します.
transactionAttributes
プロパティの型はProperties
で,そのキーはメソッド名(ワイルドカード'*'が使えます)です.値には,前述のTransactionDefinition
のPROPERGATION〜
を指定します.その後に,カンマ(',
')で区切っていくつかのオプションを指定することができます.
その一つは"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
を使ってみようと思います.
お仕事スタイル
- 黒のシルクのカットソー(JIL SANDER 03SS)
- 黒のコットンのパンツ(JIL SANDER 03SS)
- 黒のストレートチップ(sergio rossi 03SS)
いかん,お仕事スタイルは入門記の直前にアップすることにしていたのに,忘れてしまった.無念だ.
今日は最高気温が28度だとか聞いたので,薄着です.さぁ,帰りは大丈夫か? 先週は寒かったんですが.
っていうより,職場のエアコン効きすぎ.ちょっと寒いよ.
00:05追記
ちょー寒かったよっ! まだこの格好は無理! 5月までは何か羽織る!
日記の著作権とか?
自分が書いた入門記なのに,もう忘れちゃってることがあったりします.あうあう.心より恥じる.
そんなわけで読み返すこともあるのですが,これが読みにくい!
1回分だけ読むのならいいのですが,連載(?)を続けて読もうとすると,下に向かって読んだ後上に向かわなきゃいけなかったりするのがよくないっぽい.ちゃんとした目次もないし.
ということで,一段落したAOP編までをWikiにでも持っていこうかと思ったのですが,ここに書いた内容の著作権とかって大丈夫なんでしょうか? 規約を見ても,「はてなは責任を負いません」みたいなのしか見当たらなくて,特に何も主張しているようには見えないんですが.
何も問題ないのなら,本家からリンクされているSpring Padにお願いして天才^H^H転載(っていうの?)させてもらいたいなぁって思ってます.
その辺詳しい方,教えてくださいませ.あるいはここを読めとかコメントいただけるとうれしいです.お願いします.
01:15 追記
はてなは規約で何も主張していないんだから,著作権は原著作者に帰属って事でいいみたい?
いろいろぐぐってみたのですが,これといったものが引っかからないので,何も問題ないと解釈してみるてすと.