Spring Boot でログイン画面 + 一覧画面 + 登録画面の Webアプリケーションを作る ( その19 )( 設定ファイルでトランザクションを設定する )
概要
- 今回の手順で確認できるのは以下の内容です。
- 以前 トランザクションの検証をした時 に見つけた、設定ファイルによるトランザクションの設定を反映します。
ソフトウェア一覧
参考にしたサイト
手順
定義ファイルによるトランザクション宣言への変更
IntelliJ IDEA 上で 1.0.x-transactionwithconf ブランチを作成します。
トランザクションの設定を記述する定義ファイルを作成します。src/main/resources の下に applicationContext.xml を新規作成します。作成後、リンク先の内容 に変更します。
src/main/java/ksbysample/webapp/basic の下の Application.java を リンク先の内容 に変更します。
src/main/java/ksbysample/webapp/basic/service の下の CountryService.java を リンク先のその1の内容 に変更します。
テストする
テストで使用するデータを作成します。src/test/resources/ksbysample/webapp/basic の下に service ディレクトリを作成します。
src/test/resources/ksbysample/webapp/basic/service の下に countryForm_save.yaml, countryForm_codenull.yaml を新規作成します。作成後、リンク先の内容 に変更します。
src/main/java/ksbysample/webapp/basic/service の下の CountryService.java のテストクラスを作成します。CountryService.java を開いた後、クラス名 CountryService にカーソルを移動して Alt+Enter を押してコンテキストメニューを表示し「Create Test」メニューをクリックします。
「Create Test」ダイアログが表示されます。画面下部の「Member」に表示されているメソッドの内 save メソッドのみチェックして「OK」ボタンをクリックします。
src/test/java/ksbysample/webapp/basic/service の下に CountryServiceTest.java が新規作成されます。リンク先のその1の内容 に変更します。
テストを実行します。CountryServiceTest.java を選択後、コンテキストメニューを表示して「Run 'CountryServiceTest'」メニューを選択します。
テストが全て成功することが確認できました。
CountryServicce クラスの save メソッドで RuntimeException が発生した時にロールバックされるか確認します。
src/main/java/ksbysample/webapp/basic/service の下の CountryService.java を リンク先のその2の内容 に変更します。
src/test/java/ksbysample/webapp/basic/service の下の CountryServiceTest.java を リンク先のその2の内容 に変更します。
「Run 'CountryServiceTest'」メニューを選択しテストを実行します。データが登録されず、アサーションでエラーになりました。
CountryServicce クラスの save メソッドで Exception が発生した時にロールバックされるか確認します。
src/main/java/ksbysample/webapp/basic/service の下の CountryService.java を リンク先のその3の内容 に変更します。
src/test/java/ksbysample/webapp/basic/service の下の CountryServiceTest.java を リンク先のその3の内容 に変更します。
「Run 'CountryServiceTest'」メニューを選択しテストを実行します。RuntimeException の時と同じくデータが登録されず、アサーションでエラーになりました。
src/main/java/ksbysample/webapp/basic/service の下の CountryService.java を リンク先のその1の内容 に戻します。
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 ブランチを削除
commit の前に build タスクを実行し、BUILD SUCCESSFUL が表示されることを確認します。
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
初版発行。