2012年4月27日金曜日

トランザクションを“Declarative”に管理する

データベースにアクセスする際の“boilerplate(決まり文句)”にはうんざりさせられます。しかし、begin, commit/rollback といった決まり文句は、トランザクションの境界線をはっきりさせるために必要です。また、例外を捕まえて rollback するために try {..} catch {..}を使います。これもまた面倒です。

Declarative Transaction Management
Spring Framework はそうした面倒を緩和してくれる仕掛けを持っています。“Declarative Transaction Management(宣言的トランザクション管理)”もその一つです。これを使うと、「どの例外が発生したら rollback するか(declarative rollback rules)」とか「ダーティリードやファントムリードを許すか(isolation level)」とか「現在のトランザクションをサスペンドして、新しいトランザクションを生成するか(transaction propagation)」といったことを“宣言”できます。

宣言は XML で記述する方法と、@Transactional アノテーションの属性として記述する方法があります。今回、もちろん @Transactional は使いますが、種々の設定は XML で行うことにします。

その前に『Hibernate でデータアクセス(3)』で作成した hibernate.cfg.xml から、以下の行を削除しておきます。

<property name="hibernate.current_session_context_class">thread</property>

というか、JTA を使わないのであればこの項目は厳禁みたいです。以下のような書き込みが初歩的な相談事として巷に溢れていました。

宣言
リファレンス“11.5.1 Understanding the Spring Framework's declarative transaction implementation”に、Spring の宣言的トランザクションは AOP Proxy を介して実現されているとあります。実際、AOP と同様に AdvicePointcut を定義して、それらを Advisor で結び付けるというのが基本になります。

では、11.5.2 Example of declarative transaction implementation 以降の例と説明に従いながらトランザクションを宣言していきます。

名前空間
まずは、<tx:advice/> などの要素を利用するための名前空間を追加します。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    :
  xmlns:tx="http://www.springframework.org/schema/tx"
  xmlns:aop="http://www.springframework.org/schema/aop"
    :
  xsi:schemaLocation="
    :
  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">
    :

<tx:advice/>
この advice にトランザクションに対する各種属性を記述していきます。トランザクション属性を適用したいメソッド名を <tx:method/> タグの name で指定します。デフォルト設定のままでいい場合は、その他の記述は必要ありません。例えば <tx:method name="*"/> みたいな感じです。
read-only
トランザクションがリードオンリーの場合は true にします。
timeout
トランザクションがタイムアウトするまでの時間を秒数で指定します。
rollback-for
ロールバックの切欠となる例外クラスを指定します。複数の場合はカンマ区切り。@Transactional アノテーションの説明には、Throwable のサブクラスでなければならないと記述されています。FQCN(完全修飾クラス名)で書いた方が無難かも。
no-rollback-for
rollback-for の逆の意味です。
propagation
トランザクションの伝播(propagation)に関する設定です。11.5.7 Transaction propagationREQUIRED, REQUIRES_NEW, NESTED の動きが解説されています。

また、IBM developerWorks の記事“Transaction strategies: Understanding transaction pitfalls”は、propagation と他の設定を組み合わせた際の動作を知る参考になると思います。同じ read-only + propagation.REQUIRED でも JDBC と JPA では内部の動きが違うんですね。

因みにこの記事にもある Unit of Work の考え方については Hibernate Core リファレンス“13.1.1. Unit of work”が参考になります。
isolation:
トランザクションの分離(isolation)レベルに関する設定です。コミット前のデータでもかまわない場合は READ_UNCOMMITTED, 最低限コミットされていなければならない場合は READ_COMMITTED, トランザクション内での繰り返しリードでデータが変わると困る場合は REPEATABLE_READ, 他のトランザクションの影響は絶対に許さない場合は SERIALIZABLE, その辺のことはデータソースに丸投げで構わない場合は DEFAULT という感じになると思います。ロックやパフォーマンスにも関わる部分なので慎重に考えたい項目です。

以上を勘案して次のように設定してみました。「change」で始まるメソッドでは、全ての例外が rollback のトリガーとなるよう設定していますが、もっと条件を細かくしてもいいかもしれません。
:
  <bean id="txManager"
    class="org.springframework.orm.hibernate4.HibernateTransactionManager"
    p:sessionFactory-ref="sessionFactory">
  </bean>
  
  <tx:advice id="noRollBackTxAdvice" transaction-manager="txManager">
    <tx:attributes>
      <tx:method name="get*" read-only="true" isolation="READ_UNCOMMITTED" propagation="REQUIRED"/>
    </tx:attributes>
  </tx:advice>
  <tx:advice id="rollBackTxAdvice" transaction-manager="txManager">
    <tx:attributes>
      <tx:method name="change*" read-only="false" isolation="READ_COMMITTED" propagation="REQUIRES_NEW"
        rollback-for="java.lang.Throwable"/> 
      <tx:method name="*"/>
    </tx:attributes>
  </tx:advice>
    :

<aop:pointcut/>
pointcut と次の advisor は <aop:config/> 内に定義します。記述方法は AOP の規則に則ります。

以下のコードでは更新系トランザクションメソッドの pointcut に「wriderChangeOperation」、参照系に「wriderGetOperation」、ストアドプロシージャを呼び出すメソッドの pointcut に「wriderMakeOperation」という ID を割り当てています。

<aop:config>
    <aop:pointcut 
      expression="execution(* wrider.service.*.get*(..))" 
      id="wriderGetOperation"/>
    <aop:pointcut 
      expression="execution(* wrider.service.*.make*(..))" 
      id="wriderMakeOperation"/>
    <aop:pointcut 
      expression="execution(* wrider.service.*.change*(..))" 
      id="wriderChangeOperation"/>

<aop:advisor/>
advisor で上記 advice と pointcut を紐付けます。

<aop:advisor advice-ref="noRollBackTxAdvice" pointcut-ref="wriderGetOperation"/>
    <aop:advisor advice-ref="rollBackTxAdvice" pointcut-ref="wriderMakeOperation"/>
    <aop:advisor advice-ref="rollBackTxAdvice" pointcut-ref="wriderChangeOperation"/>
  </aop:config>

@Transactional アノテーションの有効化
リファレンス“11.5.6 Using @Transactional”に従って @Transactional を利用できるようにします。

<tx:annotation-driven transaction-manager="txManager"/>

次回は DAO レイヤーの boilerplate な部分を排除します。

0 件のコメント:

コメントを投稿