doma2を使ってみた (3) - @Repositoryを使う -

2015-03-16T00:00:00+09:00 Java doma Spring Framework

doma2の機能ではなくSpring Frameworkの@Repositoryで定義した場合どうなるのか気になったのでやってみた

SampleRepository.java

正式的?だと@Repositoryを付けるのはインターフェースだという概念があるっぽいけど、めんどくさいので普通にクラスにつけちゃう

package sample.repository;

import java.util.List;

import org.springframework.stereotype.Repository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;

import sample.dao.SampleDao;
import sample.entity.Sample;

@Repository
public class SampleRepository {

    @Autowired
    private SampleDao dao;

    public List<Sample> findAll() throws Exception {
        List<Sample> samples = dao.findAll();

        if (samples.size() <= 0) throw new Exception();

        return samples;
    }

    public Sample find(int id) {
        return dao.find(id);
    }

    @Transactional
    public int save(Sample sample) throws Exception {
        int status = dao.save(sample);

        // わざと例外が起きるようにしてrollbackされるかを確認する
        if (status > 0) {
            throw new Exception("error");
        }

        return status;
    }
}

まぁ@Repositoryを付けてDAOとのやりとりをした結果を返すだけのしょうもないのを作っておく(前回は@Serviceにそういうのをやってたけど、あれはあれで微妙なんじゃないかって思う所もあるので)

でこの状態のまま前回のテストをちょいと書き換えてテストを実行する

SampleDaoTest.java

package sample.dao;

import java.util.List;

import org.junit.Test;
import org.junit.runner.RunWith;

import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.beans.factory.annotation.Autowired;

import sample.entity.Sample;
import sample.repository.SampleRepository;

import static org.junit.Assert.*;
import static org.hamcrest.Matchers.*;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations="classpath:applicationContext.xml")
public class SampleDaoTest {

    @Autowired
    SampleRepository dao;

    @Test
    public void test_save() throws Exception {
        Sample sample = new Sample();
        sample.setName("hoge fuga foobar");

        // 問題がなければこれで例外が送出されてrollbackされる
        dao.save(sample);
    }
}

まぁsave側でExceptionをスローしているからテストは落ちる。でもその例外が

org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only

となる。詳解な原因がちゃんと特定できた訳じゃないのだけど、恐らくはTransactionInterceptor等によるトランザクションが重なってしまっているのではないかと。要は<tx:annotation-driven />によるTransactionInterceptorと<aop:advice/>によるTransactionInterceptorによってトランザクションが重なりメソッドで例外を創出するとsetRollbackOnlyはしているので問題なくロールバックはされるが結果的にその上のトランザクションスコープによってcommit実行しようとして結果上記のような例外が送出されてしまうのではないかという予測(あくまで予測なので確証はありません)

っていうことでこれを回避するには

  • <tx:annotation-driven />を止める (@Transactionalは効かなくなる?)
  • <aop:advice>によるトランザクション属性のインターセプトをしない (@Transactionalで属性を自前で定義する必要性が出てくる)

のどっちかをやれば良い

applicationContext.xml

<?xml version="1.0" ?>
<beans
    xmlns="http://www.springframework.org/schema/beans"
    xmlns:aop="http://www.springframework.org/schema/aop"
    xmlns:context="http://www.springframework.org/schema/context"
    xmlns:tx ="http://www.springframework.org/schema/tx"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/aop
        http://www.springframework.org/schema/aop/spring-aop.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context.xsd
        http://www.springframework.org/schema/tx
        http://www.springframework.org/schema/tx/spring-tx.xsd">

    <context:annotation-config />
    <context:component-scan base-package="sample" />

    <bean id="dataSource" class="org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy">
        <constructor-arg>
            <bean class="org.springframework.jdbc.datasource.DriverManagerDataSource">
                <property name="driverClassName" value="com.mysql.jdbc.Driver" />
                <property name="url" value="jdbc:mysql://localhost:3306/sample" />
                <property name="username" value="kinjouj" />
                <property name="password" value="1234" />
            </bean>
        </constructor-arg>
    </bean>

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

    <tx:annotation-driven />

</beans>

んまぁ<aop:advice>を使わないので@Transactionalには適切な属性を設定する必要がある

SampleRepository.java (修正)

package sample.repository;

import java.sql.SQLException;
import java.util.List;

import org.springframework.stereotype.Repository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.annotation.Propagation;

import sample.dao.SampleDao;
import sample.entity.Sample;

@Repository
public class SampleRepository {

    @Autowired
    private SampleDao dao;

    public List<Sample> findAll() throws Exception {
        List<Sample> samples = dao.findAll();

        if (samples.size() <= 0) throw new Exception();

        return samples;
    }

    public Sample find(int id) {
        return dao.find(id);
    }

    // 属性を正しく設定しないと例外が送出されて正しくロークバックされないなる
    @Transactional(rollbackFor=Exception.class, propagation=Propagation.REQUIRED)
    public int save(Sample sample) throws Exception {
        int status = dao.save(sample);

        if (status > 0) {
            throw new Exception("error");
        }

        return status;
    }
}

てな感じで実行すれば上記のUnexpectedRollbackExceptionは発生しなくなる。一応、上記でのエラーの時点でTransaction PropagationをREQUIREDからREQUIRES_NEW等にすればこの問題は出なくなる。でも例えばひとつのメソッドで他のトランザクションを実行する処理をやる場合にその他のメソッド単位でトランザクションが管理される為部分的に例外等が発生しても部分的にロールバックされなくなるのではないかと

まぁとりあえずそういう問題でちょっとハマってたのでメモ。

ちなみに@Serviceならこういう問題は出てこない

doma2を使ってみた (2) - DI Containerを使う場合 -