Spring 2.0によるDBアプリケーションの作成

第3回 トランザクション管理

最終更新日:2007/06/14

目次

宣言的トランザクション管理

トランザクション管理の方法

Spring でトランザクション管理を行う方法として、大きく分けて次の2つがあります。

推奨されている方法は前者の宣言的トランザクション管理です。プログラミングによるトランザクション管理では、トランザクション管理のコードがPOJOにそのまま書かれることになるため、好ましくありません。そのため今回は宣言的トランザクション管理の方法を紹介していきます。

宣言的トランザクション管理の実現方法

宣言的トランザクション管理の実現方法として、以下の2つの方法があります。

XMLベースのアプローチではBean定義ファイルにトランザクションの方法を記述します。アノテーションベースのアプローチではJava 5のアノテーションを使って記述します(ただしBean定義ファイルの編集も必要です)。そのため後者はJava 5以上が必要です。

記述のスタイルが異なるだけで、本質的にできることは同じです。今回はこの2つのアプローチによるトランザクション管理の方法について見ていきます。

宣言的トランザクション管理とAOP

Springの宣言的トランザクション管理はSpring AOPを用いて実現されています。そのため宣言的トランザクションを記述するためにはAOP(Aspect Oriented Programming)についてある程度知っておく必要があるでしょう。ここでトランザクション管理に関わる最低限のAOPの用語について簡単に説明すると、

JoinPoint
メソッドが呼びされた場合などの、プログラムのある特定の実行箇所。
Pointcut
JoinPointを選び出すためのフィルタのようなもの。例えば「メソッドが呼び出されたとき」という条件では、Javaのクラスライブラリ含めて非常に多くのJoinPointが存在するため、条件による絞込みが必要になる。Spring AOPでは正規表現を用いてこの絞込みを行う(AspectJと同様)。
Advice
JoinPointに織り込む処理を表します。

Spring AOPはAspectJと密接な関係があり、AspectJと同じPointcut記法が使えたり、AspectJと同じアノテーションをサポートしています。そのためAspectJについて知っていた方が良いですが、トランザクション管理については気にする必要はないと思います。特にアノテーションベースのアプローチではAOPをあまり意識せず、直感的にトランザクションを設定できます。

トランザクション対象クラス

今回トランザクション管理の対象とするクラスは、第1回で作成した以下のSetupServiceクラスです。setupメソッドで部門の登録と社員の登録という2つの処理を行っていますが、この2つの処理を一つのトランザクションで実行します。つまり、社員の登録が失敗したら部門の登録はロールバックされなければなりません。このトランザクション管理をSpringの宣言的トランザクションで行います。

尚、以下のクラスでは実際にはbumonDao, shainDaoに対するGetter/Setterが定義されていますが、ここでは省略してあります。

package service;

public class SetupService implements ISetupService {

    private IBumonDao bumonDao;

    private IShainDao shainDao;

    public void setup(Bumon bumon, Shain shain) {
        // 部門の登録
        bumonDao.insert(bumon);

        // 社員の登録
        shainDao.insert(shain);
    }
}

XMLベースのトランザクション管理

<tx:advice/>

XMLベースのトランザクション管理を行う場合は、Bean定義ファイルに<tx:advice/>要素を追加することにより、Adviceを定義します。Advice(JoinPointに織り込む処理)を定義するといっても、今回織り込むべき処理はトランザクション管理という共通処理なので、その処理自体を作りこむわけではありません。

トランザクション管理を行うクラスはSpringで用意されているため、トランザクション管理を行うクラスをBeanとして定義し、<tx:advice/>のtransaction-manager属性でそのBeanの名前を与えることで、Adviceを定義します。

トランザクション管理を行うBeanの定義

Springでは使用するデータアクセス方法の種類に合わせて、トランザクション管理クラスが用意されています。前回実装したようなSpring JDBCによるデータアクセスを行う場合は、以下のようにDataSourceTransactionManagerクラスをBeanとして定義します。

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

<bean>要素のid属性の値は何でも良いですが、"transactionManager"とすると後述の<tx:advice/>要素のtransaction-manager属性を省略することができるため、ここでもそうしています。

dataSourceプロパティではデータソースを定義したBeanの名前を与えます。前回のデータソースの設定でデータソース定義を作成しているので、そのBeanの名前"dataSource"をref属性に与えます。

<tx:advice/>の定義

次に<tx:advice/>要素を定義します。

<tx:advice id="txAdvice" transaction-manager="transactionManager">
  <tx:attributes>
    <tx:method name="read*" read-only="true" />
    <tx:method name="*" />
  </tx:attributes>
</tx:advice>

transaction-manager属性で先ほどのトランザクション管理を行うBeanの名前("transactionManager")を与えます。名前が"transactionManager"なので省略可能ですが、ここではあえて省略せずに書いています。

子要素に<tx:attributes/>があり、更にその子要素に<tx:method/>があります。<tx:method/>ではトランザクションの対象メソッドやその振る舞いを細かく制御します。<tx:method/>の属性一覧を以下に示します。デフォルト値が定義されているため、トランザクションの振る舞いを変更する必要がなければ<tx:method/>は省略可能です。

<tx:method/>の属性一覧

属性 必須 デフォルト値 詳細
name Yes   トランザクションを結びつけるメソッド名。ワイルドカード(*)を使用可能。(ex. get*, handle*, on*Event)
propagation No REQUIRED トランザクションの振る舞い。REQUIREDはトランザクションが既に存在していればそのトランザクション内で処理を行い、無ければ新規にトランザクションを生成する。指定可能な値は、SpringのJavadocのTransactionDefinitionインタフェースを参照。
isolation No DEFAULT トランザクションの分離レベル。こちらも詳細はTransactionDefinitionインタフェースを参照。
timeout No -1 トランザクションのタイムアウト(秒)
read-only No false 読み込み専用のトランザクションかどうか
rollback-for No   ロールバックのトリガーとなる例外クラス。複数ある場合はカンマで区切る。デフォルトではRuntimeException及びその派生クラスがトリガー対象であり、その他のException派生クラスはトリガー対象でないことには注意が必要。
no-rollback-for No   ロールバックのトリガーとしない例外クラス。複数ある場合はカンマで区切る。

<tx:method/>は複数記述できます。トランザクションの実行対象は後述のPointcutで指定しますが、メソッド毎にトランザクションの振る舞いが異なることもあるでしょう。そのような場合に<tx:method/>要素を複数設けることで、メソッド毎のトランザクションの振る舞いを制御します。例えば上の例では"read"で始まるメソッドは読み込み専用のトランザクションが行われ、それ以外のメソッドはデフォルトのトランザクションが行われます。(今回の例ではreadで始まるメソッドはありません。説明用としてこのように書いています。)

<aop:config/>

次に<aop:config/>要素によりPointcutを定義し、先ほど定義したAdviceとPointcutを結び付けます。

<aop:config>
    <aop:pointcut id="setupOperation"
                  expression="execution(* service.SetupService.setup(..))" />
    <aop:advisor advice-ref="txAdvice"
                 pointcut-ref="setupOperation" />
</aop:config>

<aop:pointcut>要素でPointcutを定義します。expression属性ではAspectJのPointcut記法を用いてPointcutを記述します。この例では、「service.SetupServiceクラスのsetupメソッド(戻り値と引数は任意)を実行したとき」がPointcutになります(最初の*は戻り値が任意であること、setupメソッドの(..)は引数が任意であることを示している)。

<aop:advisor/>でPointcutとそれに結びつけるAdviceを指定します。pointcut-refでは<aop:pointcut/>のid属性値、advice-refでは先ほど定義した<tx:advice/>要素のid属性値を与えます。

名前空間宣言の追加

これまで見てきたように、宣言的トランザクション管理ではtx, aopという名前空間を用いているため、Bean定義ファイルにそれぞれの名前空間宣言が必要です。既に第1回で作成したBean定義ファイルにその宣言が書かれていましたが、確認のためもう一度宣言を載せておきます。

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

上の太字部分が追加すべき名前空間宣言(とスキーマ文書の宣言)です。

トランザクション適用箇所の確認

Bean定義ファイルの作成が完了すると、エディタビューの左側に矢印のアイコンが表示されます。これはこの場所にAdviceが織り込まれることを示しています。今回はAdviceとしてトランザクション管理を設定しているので、setupメソッドが実行されるとトランザクション管理が行われるということがわかるようになっています。

もしこのアイコンが表示されない場合は、Bean定義ファイルを再確認して下さい。

社員DAOスタブの作成

トランザクション対象クラスであるSetupServiceのsetupメソッドで、社員の登録時に例外を発生させるべく、社員DAOのスタブを作成します。insertメソッドでRuntimeExceptionをスローし、それ以外は空実装です。

package dao;

import java.util.List;

import dto.Shain;

public class ShainDaoStub implements IShainDao {

    public void insert(Shain shain) {
        throw new RuntimeException();
    }

    public Shain load(String cdShain) {
        return null;
    }

    public void update(Shain shain) {}

    public void delete(String cdShain) {}

    public List<Shain> findAll() {
        return null;
    }
}

スタブを作成したら忘れずにBean定義ファイルへ登録します。また後のテスト時にスタブDAOを利用するSetupServiceが必要になるため、setupServiceStubとういBeanを新しく定義し、shainDaoプロパティのref属性をスタブDAOにしています。

<bean id="setupServiceStub" class="service.SetupService">
    <property name="shainDao" ref="shainDaoStub" />
    <property name="bumonDao" ref="bumonDao" />
</bean> 

<bean id="shainDaoStub" class="dao.ShainDaoStub" />

Bean定義ファイル

最終的なBean定義ファイルは以下のようになりました。太字部分はトランザクション管理のために追加した部分です。

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

">

  <!-- DataSource -->
  <bean id="dataSource"
      class="org.apache.commons.dbcp.BasicDataSource"
      destroy-method="close">
    <property name="driverClassName" value="${db.driver}" />
    <property name="url" value="${db.url}" />
    <property name="username" value="${db.username}" />
    <property name="password" value="${db.password}" />
  </bean>

  <!-- PropertyPlaceholder -->
  <bean
    class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
    <property name="location" value="database.properties" />
  </bean>

  <!-- Transaction -->
  <tx:advice id="txAdvice" transaction-manager="transactionManager">
    <tx:attributes>
      <tx:method name="read*" read-only="true" />
      <tx:method name="*" rollback-for="java.lang.Exception" />
    </tx:attributes>
  </tx:advice>


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


  <!-- AOP -->
  <aop:config>
    <aop:pointcut id="setupOperation"
             expression="execution(* service.SetupService.setup(..))" />
    <aop:advisor advice-ref="txAdvice"
             pointcut-ref="setupOperation" />
  </aop:config>


  <!-- Service -->
  <bean id="bumonService" class="service.BumonService">
    <property name="bumonDao" ref="bumonDao" />
  </bean>
  <bean id="shainService" class="service.ShainService">
    <property name="shainDao" ref="shainDao" />
  </bean>
  <bean id="setupService" class="service.SetupService">
    <property name="shainDao" ref="shainDao" />
    <property name="bumonDao" ref="bumonDao" />
  </bean>
  <bean id="setupServiceStub" class="service.SetupService">
    <property name="shainDao" ref="shainDaoStub" />
    <property name="bumonDao" ref="bumonDao" />
  </bean>

  <!-- DAO -->
  <bean id="bumonDao" class="dao.BumonDao">
    <property name="dataSource" ref="dataSource" />
  </bean>
  <bean id="shainDao" class="dao.ShainDao">
    <property name="dataSource" ref="dataSource" />
  </bean>
  <bean id="shainDaoStub" class="dao.ShainDaoStub" />

</beans>

テストケースの作成

SetupServiceを駆動するテストケースを作成します。テストケースではコミットとロールバックの動作確認を行います。ロールバックのテストでは先ほどのsetupServiceStubを使用しているため、部門の登録が成功した後、社員の登録時に例外が発生します。そのときに登録に成功した部門がロールバックされていることを確認します。

public class SetupServiceTest {

    ISetupService service;
    ISetupService serviceStub;
    IBumonService b;
    IShainService s;

    @Before
    public void setUp() {
        ApplicationContext context = new ClassPathXmlApplicationContext(
                "applicationContext.xml");
        service = (ISetupService) context.getBean("setupService");
        serviceStub = (ISetupService) context.getBean("setupServiceStub");
        b = (IBumonService) context.getBean("bumonService");
        s = (IShainService) context.getBean("shainService");
    }

    /**
     * コミットが正常に行われていることを確認する
     */
    @Test
    public void testCommit() {
        Bumon bumon = new Bumon("000001", "ESC");
        Shain shain = new Shain("100001", "Oki Tarou", new Date(), "000001");
        try {
            service.setup(bumon, shain);
            assertEquals(1, b.getAllBumon().size());
            assertEquals(1, s.getAllShain().size());
        } finally {
            s.removeShain(shain.getCdShain());
            b.removeBumon(bumon.getCdBumon());
        }
    }

    /**
     * ロールバックが正常に行われていることを確認する
     */
    @Test
    public void testRollback() {
        Bumon bumon = new Bumon("000001", "ESC");
        Shain shain = new Shain("100001", "Oki Tarou", new Date(), "000001");
        try {
            serviceStub.setup(bumon, shain);
        } catch (Exception e) {
            assertEquals(0, b.getAllBumon().size());
        } finally {
            try {
                b.removeBumon(bumon.getCdBumon());
            } catch (Exception ignore) {}
        }
    }

}

テスト実行

テストケースを実行し、グリーンのバーが表示されればテストは成功です。

アノテーションベースのトランザクション管理

@Transactional

XMLベースのトランザクション管理ではトランザクションの振る舞いやトランザクションを適用位置を、<tx:advice/>や<aop:config/>によって記述しましたが、アノテーションベースのアプローチでは@Transactionalをトランザクション管理が行われるべきクラスやメソッドに直接記述します。

@Transactionalはクラス定義やpublicメソッドに付けることができます。インタフェース宣言やインタフェースのメソッド宣言にも付けることができますが、この方法は推奨されていないため実装クラス(及びそのメソッド)にのみ@Transactionalを使うようにすると良いでしょう(詳細はSpringのドキュメントを参照して下さい)。

@Transationalには以下のようにいくつかプロパティがあり、トランザクションの振る舞いを制御することができます。XMLベースの<tx:method/>の属性とほとんど同じです。

@Transactionalアノテーションのプロパティ

プロパティ 詳細
propagation enum: Propagation トランザクションの振る舞い
isolation enum: Isolation トランザクションの分離レベル
readOnly boolean 読み込み専用のトランザクションかどうか
timeout int(秒) トランザクションのタイムアウト(秒)
rollbackFor Classオブジェクトの配列 ロールバックのトリガーとなる例外クラス
rollbackForClassname クラス名の配列 ロールバックのトリガーとなる例外クラス名
noRollbackFor Classオブジェクトの配列 ロールバックのトリガーとしない例外クラス
noRollbackForClassname クラス名の配列 ロールバックのトリガーとしない例外クラス名

例えばこのように記述します。

@Transactional(propagation=Propagation.REQUIRES_NEW)
public void setup(Bumon bumon, Shain shain) {

Eclipseでアノテーションを記述する場合、属性名やその値はコンテンツ・アシストで補完できます。もしデフォルトの振る舞いでよければ単に@Transactionalアノテーションを付与するだけです。

@Transactional
public void setup(Bumon bumon, Shain shain) {

尚、以下のようにクラス定義とメソッドの両方に@Transactionを付与した場合は、メソッドに付与した方が優先されます。クラスの一部のメソッドのみトランザクションの振る舞いを変更したい場合は、このようにすると良いでしょう。

@Transactional(propagation=Propagation.REQUIRED)
public class SetupService implements ISetupService {

    private IBumonDao bumonDao;

    private IShainDao shainDao;

    @Transactional(propagation=Propagation.REQUIRES_NEW)
    public void setup(Bumon bumon, Shain shain) throws ServiceException {
        // 部門の登録
        bumonDao.insert(bumon);

        // 社員の登録
        shainDao.insert(shain);
    }

<tx:annotation-driven/>

アノテーションベースのアプローチを行う場合は、Bean定義ファイルに<tx:annotation-driven/>という要素を追加する必要があります。その属性は以下の通りです。

<tx:annotation-driven/>の属性一覧

属性 必須 デフォルト値 詳細
transaction-manager No transactionManager トランザクションマネージャの名前(トランザクション管理を行うBeanの名前)。その名前が"transactionManager"ならば省略可能。
proxy-target-class No   トランザクションプロキシのタイプを指定する(true or false)
order No   Adviceが適用される順序

transaction-manager属性では<tx:advice/>と同様に、トランザクション管理Beanの名前を指定します。トランザクション管理BeanはXMLベースのトランザクション管理で作成したものと同じですので省略します。<tx:advice/>と同様に、その名前が"transactionManager"ならばtransaction-manager属性は省略可能です。

Bean定義ファイル

アノテーションベースのアプローチによる最終的なBean定義ファイルは以下のようになりました。太字部分はトランザクション管理のために追加した部分です。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-2.0.xsd
   http://www.springframework.org/schema/tx
   http://www.springframework.org/schema/tx/spring-tx-2.0.xsd
   http://www.springframework.org/schema/aop
   http://www.springframework.org/schema/aop/spring-aop-2.0.xsd

">

  <!-- DataSource -->
  <bean id="dataSource"
    class="org.apache.commons.dbcp.BasicDataSource"
    destroy-method="close">
    <property name="driverClassName" value="${db.driver}" />
    <property name="url" value="${db.url}" />
    <property name="username" value="${db.username}" />
    <property name="password" value="${db.password}" />
  </bean>

  <!-- PropertyPlaceholder -->
  <bean
    class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
    <property name="location" value="database.properties" />
  </bean>

  <!-- Transaction -->
  <tx:annotation-driven />

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


  <!-- Service -->
  <bean id="bumonService" class="service.BumonService">
    <property name="bumonDao" ref="bumonDao" />
  </bean>
  <bean id="shainService" class="service.ShainService">
    <property name="shainDao" ref="shainDao" />
  </bean>
  <bean id="setupService" class="service.SetupService">
    <property name="shainDao" ref="shainDao" />
    <property name="bumonDao" ref="bumonDao" />
  </bean>
  <bean id="setupServiceStub" class="service.SetupService">
    <property name="shainDao" ref="shainDaoStub" />
    <property name="bumonDao" ref="bumonDao" />
  </bean>

  <!-- DAO -->
  <bean id="bumonDao" class="dao.BumonDao">
    <property name="dataSource" ref="dataSource" />
  </bean>
  <bean id="shainDao" class="dao.ShainDao">
    <property name="dataSource" ref="dataSource" />
  </bean>
  <bean id="shainDaoStub" class="dao.ShainDaoStub" />

</beans>

@Transactionalの組み込み

これまでの説明で既に出てきていますが、SetupServiceクラスのsetupメソッドに@Transactionアノテーションを追加したコードを挙げておきます。トランザクションの振る舞いはデフォルトで良いため、単に@Transactionアノテーションをsetupメソッドに追加しています。

package service;

public class SetupService implements ISetupService {

    private IBumonDao bumonDao;

    private IShainDao shainDao;

    @Transactional
    public void setup(Bumon bumon, Shain shain) {
        // 部門の登録
        bumonDao.insert(bumon);

        // 社員の登録
        shainDao.insert(shain);
    }
}

テスト実行

XMLベースの場合と同じテストケースを実行し、グリーンのバーが表示されればテストは成功です。

デフォルトのロールバックの挙動

デフォルトの挙動は、

このようになっています。すべての例外が発生した時にロールバックを行わせるようにするには、XMLベースの場合は<tx:advice/>, アノテーションベースの場合は@TransactionのrollbackForプロパティでThrowable.classを指定します。

@Transactional(rollbackFor=Throwable.class)
public void setup(Bumon bumon, Shain shain) 

今回のまとめ

宣言的トランザクションをXMLベースとアノテーションベースで実装する方法について紹介しました。XMLベースの方法ではSpring AOPの機能を使用しましたが、次回はそのSpring AOPについて紹介する予定です。

今回アノテーションベースで作成したソースコードとBean定義ファイルはここからダウンロードできます。

番外編 Spring AOPによるロギングの実装へ

Spring 2.0 トップへ

資料室へ戻る


Copyright (c) 2007 OKI Software Co., Ltd.