かんがるーさんの日記

最近自分が興味をもったものを調べた時の手順等を書いています。今は Spring Boot をいじっています。

Spring Boot でログイン画面 + 一覧画面 + 登録画面の Webアプリケーションを作る ( その19 )( 設定ファイルでトランザクションを設定する )

概要

Spring Boot でログイン画面 + 一覧画面 + 登録画面の Webアプリケーションを作る ( その18 )( Spring Boot Gradle plugin で Spring Boot のバージョン番号を指定する ) の続きです。

ソフトウェア一覧

参考にしたサイト

手順

定義ファイルによるトランザクション宣言への変更

  1. IntelliJ IDEA 上で 1.0.x-transactionwithconf ブランチを作成します。

  2. トランザクションの設定を記述する定義ファイルを作成します。src/main/resources の下に applicationContext.xml を新規作成します。作成後、リンク先の内容 に変更します。

  3. src/main/java/ksbysample/webapp/basic の下の Application.javaリンク先の内容 に変更します。

  4. src/main/java/ksbysample/webapp/basic/service の下の CountryService.javaリンク先のその1の内容 に変更します。

テストする

  1. テストで使用するデータを作成します。src/test/resources/ksbysample/webapp/basic の下に service ディレクトリを作成します。

  2. src/test/resources/ksbysample/webapp/basic/service の下に countryForm_save.yaml, countryForm_codenull.yaml を新規作成します。作成後、リンク先の内容 に変更します。

  3. src/main/java/ksbysample/webapp/basic/service の下の CountryService.java のテストクラスを作成します。CountryService.java を開いた後、クラス名 CountryService にカーソルを移動して Alt+Enter を押してコンテキストメニューを表示し「Create Test」メニューをクリックします。

    f:id:ksby:20150318195210p:plain

  4. 「Create Test」ダイアログが表示されます。画面下部の「Member」に表示されているメソッドの内 save メソッドのみチェックして「OK」ボタンをクリックします。

    f:id:ksby:20150318195348p:plain

  5. src/test/java/ksbysample/webapp/basic/service の下に CountryServiceTest.java が新規作成されます。リンク先のその1の内容 に変更します。

  6. テストを実行します。CountryServiceTest.java を選択後、コンテキストメニューを表示して「Run 'CountryServiceTest'」メニューを選択します。

    f:id:ksby:20150318222221p:plain

  7. テストが全て成功することが確認できました。

    f:id:ksby:20150318222423p:plain

  8. CountryServicce クラスの save メソッドで RuntimeException が発生した時にロールバックされるか確認します。

  9. src/main/java/ksbysample/webapp/basic/service の下の CountryService.javaリンク先のその2の内容 に変更します。

  10. src/test/java/ksbysample/webapp/basic/service の下の CountryServiceTest.javaリンク先のその2の内容 に変更します。

  11. 「Run 'CountryServiceTest'」メニューを選択しテストを実行します。データが登録されず、アサーションでエラーになりました。

    f:id:ksby:20150318223454p:plain

  12. CountryServicce クラスの save メソッドで Exception が発生した時にロールバックされるか確認します。

  13. src/main/java/ksbysample/webapp/basic/service の下の CountryService.javaリンク先のその3の内容 に変更します。

  14. src/test/java/ksbysample/webapp/basic/service の下の CountryServiceTest.javaリンク先のその3の内容 に変更します。

  15. 「Run 'CountryServiceTest'」メニューを選択しテストを実行します。RuntimeException の時と同じくデータが登録されず、アサーションでエラーになりました。

    f:id:ksby:20150318224453p:plain

  16. src/main/java/ksbysample/webapp/basic/service の下の CountryService.javaリンク先のその1の内容 に戻します。

  17. src/test/java/ksbysample/webapp/basic/service の下の CountryServiceTest.javaリンク先のその1の内容 に戻します。

commit、GitHub へ Push、1.0.x-transactionwithconf -> 1.0.x へ Pull Request、1.0.x でマージ、1.0.x-transactionwithconf ブランチを削除

  1. commit の前に build タスクを実行し、BUILD SUCCESSFUL が表示されることを確認します。

  2. commit、GitHub へ Push、1.0.x-transactionwithconf -> 1.0.x へ Pull Request、1.0.x でマージ、1.0.x-transactionwithconf ブランチを削除、をします。

メモ書き

  • CountryServiceクラスの save メソッドはデータの有無によって insert か update のいずれかが実行されますが、必ず insert を実行させるようなことはできるのでしょうか? update はして欲しくない時がありそうな気もするのですが。

ソースコード

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

    <tx:advice id="txAdvice" transaction-manager="transactionManager">
        <tx:attributes>
            <tx:method name="*" rollback-for="Exception" timeout="3"/>
        </tx:attributes>
    </tx:advice>

    <aop:config>
        <aop:pointcut id="pointcutService" expression="execution(* ksbysample.webapp.basic.service..*Service.*(..))"/>
        <aop:advisor advice-ref="txAdvice" pointcut-ref="pointcutService"/>
    </aop:config>

</beans>
  • txAdvice には以下の設定をしています。
  • ksbysample.webapp.basic.service の下 ( サブパッケージを含む ) にある末尾が "Service" で終わるクラスのメソッド全てに txAdvice の設定を反映します。

Application.java

package ksbysample.webapp.basic;

import org.apache.commons.lang3.StringUtils;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ImportResource;

import java.text.MessageFormat;

@ImportResource("classpath:applicationContext.xml")
@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        String springProfilesActive = System.getProperty("spring.profiles.active");
        if (!StringUtils.equals(springProfilesActive, "product")
                && !StringUtils.equals(springProfilesActive, "develop")
                && !StringUtils.equals(springProfilesActive, "unittest")) {
            throw new UnsupportedOperationException(MessageFormat.format("JVMの起動時引数 -Dspring.profiles.active で develop か unittest か product を指定して下さい ( -Dspring.profiles.active={0} )。", springProfilesActive));
        }

        SpringApplication.run(Application.class, args);
    }

}
  • @SpringBootApplication の上に @ImportResource("classpath:applicationContext.xml") を追加します。

CountryService.java

■その1

package ksbysample.webapp.basic.service;

import ksbysample.webapp.basic.domain.Country;
import ksbysample.webapp.basic.web.CountryForm;
import ksbysample.webapp.basic.web.CountryListForm;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.Collections;
import java.util.List;

@Service
public class CountryService {

    @Autowired
    private CountryMapper countryMapper;

    @Autowired
    private CountryRepository countryRepository;

    public Page<Country> findCountry(CountryListForm countryListForm, Pageable pageable) {
        long count = countryMapper.selectCountryCount(countryListForm);

        List<Country> countryList = Collections.emptyList();
        if (count > 0) {
            countryList = countryMapper.selectCountry(countryListForm);
        }

        return new PageImpl<Country>(countryList, pageable, count);
    }

    public void save(CountryForm countryForm) {
        Country country = new Country();
        BeanUtils.copyProperties(countryForm, country);
        countryRepository.save(country);
    }

}

■その2

    public void save(CountryForm countryForm) {
        Country country = new Country();
        BeanUtils.copyProperties(countryForm, country);
        countryRepository.save(country);
throw new RuntimeException();
    }
  • メソッドの最後で RuntimeException を throw します。

■その3

    public void save(CountryForm countryForm) throws IOException {
        Country country = new Country();
        BeanUtils.copyProperties(countryForm, country);
        countryRepository.save(country);
throw new IOException();
    }
  • メソッドthrows IOException を追加します。
  • メソッドの最後で IOException を throw します。最初は Exception を throw しようとしたのですが、Controller クラスのメソッドの throws 節に Exception を追加しないといけなかったため、既に throws 節に記述されていた IOException にしました。

countryForm_save.yaml, countryForm_codenull.yaml

■countryForm_save.yaml

!!ksbysample.webapp.basic.web.CountryForm
code: JP2
name: Japan2
continent: Asia
region: Eastern Asia
surfaceArea: 1.00
population: 2
localName: Nippon2
governmentForm: test
code2: J2

■countryForm_codenull.yaml

!!ksbysample.webapp.basic.web.CountryForm
code: 
name: Japan2
continent: Asia
region: Eastern Asia
surfaceArea: 1.00
population: 2
localName: Nippon2
governmentForm: test
code2: J2

CountryServiceTest.java

■その1

package ksbysample.webapp.basic.service;

import ksbysample.webapp.basic.Application;
import ksbysample.webapp.basic.domain.Country;
import ksbysample.webapp.basic.test.TestDataResource;
import ksbysample.webapp.basic.test.TestHelper;
import ksbysample.webapp.basic.web.CountryForm;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.yaml.snakeyaml.Yaml;

import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.notNullValue;
import static org.junit.Assert.assertThat;

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = Application.class)
@WebAppConfiguration
public class CountryServiceTest {

    @Rule
    @Autowired
    public TestDataResource testDataResource;

    @Autowired
    private CountryService countryService;

    @Autowired
    private CountryRepository countryRepository;

    // テストデータ
    private CountryForm countryFormSave
            = (CountryForm) new Yaml().load(getClass().getResourceAsStream("countryForm_save.yaml"));
    private CountryForm countryFormCodeNull
            = (CountryForm) new Yaml().load(getClass().getResourceAsStream("countryForm_codenull.yaml"));

    @Test
    public void データを1件保存する() throws Exception {
        countryService.save(countryFormSave);

        // 保存されていることを確認する
        Country country = countryRepository.findOne(countryFormSave.getCode());
        assertThat(country, is(notNullValue()));
        TestHelper.assertEntityByForm(country, countryFormSave);
    }

    @Test(expected = Exception.class)
    public void Codeが空の場合Exceptionが発生する() throws Exception {
        countryService.save(countryFormCodeNull);
    }

}
  • Codeが空の場合Exceptionが発生する() テストメソッドでは本当は org.springframework.orm.jpa.vendor.HibernateJpaDialect.convertHibernateAccessException という例外が発生するのですが、@Test(expected = convertHibernateAccessException.class) と記述することができなかったため @Test(expected = Exception.class) と記述しています。

■その2

    @Test
    public void データを1件保存する() throws Exception {
        try {
            countryService.save(countryFormSave);
        } catch (RuntimeException ignored) {}

        // 保存されていることを確認する
        Country country = countryRepository.findOne(countryFormSave.getCode());
        assertThat(country, is(notNullValue()));
        TestHelper.assertEntityByForm(country, countryFormSave);
    }
  • countryService.save(countryFormSave); を呼び出した時に throw される RuntimeException を catch する処理を追加します。

■その3

    @Test
    public void データを1件保存する() throws Exception {
        try {
            countryService.save(countryFormSave);
        } catch (IOException ignored) {}

        // 保存されていることを確認する
        Country country = countryRepository.findOne(countryFormSave.getCode());
        assertThat(country, is(notNullValue()));
        TestHelper.assertEntityByForm(country, countryFormSave);
    }
  • countryService.save(countryFormSave); を呼び出した時に throw される IOException を catch する処理を追加します。

履歴

2015/03/18
初版発行。