Spring Boot でログイン画面 + 一覧画面 + 登録画面の Webアプリケーションを作る ( その13 )( 登録画面作成3 )
概要
Spring Boot でログイン画面 + 一覧画面 + 登録画面の Webアプリケーションを作る ( その12 )( 登録画面作成2 ) の続きです。
- 今回の手順で確認できるのは以下の内容です。
- 登録画面 ( 入力→確認→完了 ) の作成、確認。今回は登録処理と完了画面を実装します。
- properties ファイルのキャッシュ?を無効化できないか調べます。
ソフトウェア一覧
参考にしたサイト
Spring Boot Reference Guide - Appendix A. Common application properties
http://docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.htmlSpring Data JPA
http://projects.spring.io/spring-data-jpa/Spring Data JPA Examples
https://github.com/spring-projects/spring-data-jpa-examplesWhat is difference between CrudRepository and JpaRepository interfaces in Spring Data JPA
http://stackoverflow.com/questions/14014086/what-is-difference-between-crudrepository-and-jparepository-interfaces-in-springSpring Boot + JPA : Column name annotation ignored
http://stackoverflow.com/questions/25283198/spring-boot-jpa-column-name-annotation-ignoredFailed to convert property value of type java.lang.String to required type double
http://stackoverflow.com/questions/10778822/failed-to-convert-property-value-of-type-java-lang-string-to-required-type-doubl
手順
Tomcat を起動する
Spring Loaded を生かすために今回も最初に Tomcat を起動して必要な時だけ再起動します。
- Gradle tasks View から bootRun タスクを実行します。
properties ファイルのキャッシュ?を無効化する。
Spring Boot Reference Guide - Appendix A. Common application properties を見ると、以下の設定がありました。cache-seconds = -1 とあるので、この設定で永久にキャッシュされているようです。また basename=messages という設定もあり、この設定ならばわざわざ messageSource Bean を定義する必要がないことが分かりました ( いろいろな Spring Boot のサンプルを見ていた時に messages.properties を使用したい時は messageSource Bean は必ず定義するものと思い込んでいたようです )。
spring.messages.basename=messages
spring.messages.cache-seconds=-1
spring.messages.encoding=UTF-8develop 環境の時だけ properties ファイルのキャッシュを無効にします。src/main/resources の下の application-develop.properties を リンク先のその1の内容 に変更します。
messageSource Bean を削除します。src/main/java/ksbysample/webapp/basic/config の下の ApplicationConfig.java を リンク先の内容 に変更します。
確認します。まずは Ctrl+F5 を押して Tomcat を再起動します ( application-develop.properties の変更を反映するためです )。
ブラウザで http://localhost:8080/ にアクセスしてログイン画面を表示し、ID に "test" を入力して「ログイン」ボタンをクリックします。ログインエラーとなり、エラーメッセージとして「入力された ID あるいはパスワードが正しくありません」が表示されます。
src/main/resources の下の messages_ja_JP.properties の AbstractUserDetailsAuthenticationProvider.badCredentials の文字列の末尾に "xxx" を付けます。
再度ログイン画面の「ログイン」ボタンをクリックします。ログインエラーとなり、今度はエラーメッセージとして「入力された ID あるいはパスワードが正しくありませんxxx」が表示されます。Tomcat を再起動しなくても properties ファイルの変更が反映されています。
src/main/resources の下の messages_ja_JP.properties の AbstractUserDetailsAuthenticationProvider.badCredentials の文字列の末尾に付けた "xxx" を削除します。
登録処理の作成
確認画面で「登録」ボタンが押された時の処理を作成します。
country テーブルの Entity クラスは既に作成済なので、Repository インターフェースを作成します。 src/main/java/ksbysample/webapp/basic/service の下に CountryRepository.java を作成します。作成後、リンク先の内容 に変更します。
CountryService クラスに登録用のメソッドを追加します。src/main/java/ksbysample/webapp/basic/service の下の CountryService.java を リンク先の内容 に変更します。
CountryController クラスの update メソッドを変更します。src/main/java/ksbysample/webapp/basic/web の下の CountryController.java を リンク先の内容 に変更します。
確認します。まずは Ctrl+F5 を押して Tomcat を再起動します。
ブラウザで http://localhost:8080/country/input にアクセスして登録画面を表示し、値を入力して「確認」ボタンをクリックします。
確認画面が表示されますので「登録」ボタンをクリックします。
HTTP 500 が返ってきました。ログを見ると
com.mysql.jdbc.exceptions.jdbc4.MySQLSyntaxErrorException: Unknown column 'country0_.government_form' in 'field list'
というエラーが出力されていました。原因を調査します。調査して分かったことを以下に記載します。
CrudRepository の save メソッドの実装は、いきなり update するのではなくまずは select していました。Exception が出力される直前のログに select 文が出力されていました。
DB のカラム名は GovernmentForm、Entity クラスのフィールド名は governmentForm で定義しているので、select 文では GovernmentForm になると思ったのですが、実際には government_form になっていました。Entity クラスではキャメルケースで定義していたものが、select 文の時にスネークケースに変更されたようです。
更にいろいろ調べてみると、どうもデフォルトの設定ではキャメルケースがスネークケースに変換されるようになっているようです。また @Column アノテーションでカラム名を明示しても効かないようです。
spring.jpa.hibernate.naming_strategy=org.hibernate.cfg.EJB3NamingStrategy
を定義すれば解決するとのことでした。
src/main/resources の下の application-develop.properties を リンク先のその2の内容 に変更します。
src/main/resources の下の application-product.properties を リンク先の内容 に変更します。
再度 Ctrl+F5 を押して Tomcat を再起動した後、確認画面まで進めて「登録」ボタンをクリックしますが、また HTTP 500 が返ってきました。ログを見ると今度は
com.mysql.jdbc.exceptions.jdbc4.MySQLIntegrityConstraintViolationException: Column 'Population' cannot be null
というエラーが出力されていました。調査すると、原因は CountryForm クラスのフィールド population の型が BigDecimal なのに対し、Country クラスのフィールド population の型が Long のためでした。country テーブルの Poplation は INT(11) なので、どちらも Long型に変更します。src/main/java/ksbysample/webapp/basic/web の下の CountryForm.java を リンク先の内容 に変更します。
再度 Ctrl+F5 を押して Tomcat を再起動した後、確認画面まで進めて「登録」ボタンをクリックします。今度は完了画面まで進みました。
データも入力した通り登録されています。
また、完了画面の「次のデータを登録する」「検索/一覧画面へ」ボタンをクリックするとそれぞれ登録画面、検索/一覧画面へ遷移されましたので、完了画面は何も変更しません。
Population の型を Long に変更したので、数値以外の値を入力した時のエラーメッセージが今のままだと英語のものが表示されます。
src/main/resources の下の messages_ja_JP.properties を リンク先の内容 に変更します。今度は日本語のエラーメッセージが表示されます。
Tomcat を終了する
commit する
本来テストクラスの作成が残っていますが、かなり時間がかかったので一旦 commit します。
commit の前に build タスクが成功するか確認します。Gradle tasks View から build を実行しますが、BUILD FAILED が出力されました。test タスクでエラーが出ているようです。
「Run 'Tests in 'ksbysample...' with Coverage」を実行してエラーが出ているテストを確認すると、CountryControllerTest クラスの testConfirm, testUpdate でした。一旦この2つのテストは実行されないようにします。src/main/test/java/ksbysample/webapp/basic/web の下の CountryControllerTest.java を リンク先の内容 に変更します。
再度 Gradle tasks View から build を実行します。今度は BUILD SUCCESSFUL が出力されました。
commit します。
「Code Analysis」ダイアログが表示されますので「Review」ボタンをクリックして内容を確認します。以下の対応だけ行います。
Method 'initBinder(...)' is never used
の Warning はSuppress for methods ...
を選択して無視されるようにします。
Local variable 'page' is redundant
の Warning はInline variable
を選択して修正します。
- あと2つ
Class 'ApplicationTest' is never used
とPrivate field 'CONTINENT_LIST' is never assinged
が出ますが、これらは何も対応しません。
再度 commit し、「Code Analysis」ダイアログが表示されたら「Commit」ボタンをクリックします。
GitHub へ Push、1.0.x-makecountryinput -> 1.0.x へ Pull Request、1.0.x でマージ、1.0.x-makecountryinput ブランチを削除
GitHub で Pull Request を作成します。
IntelliJ IDEA で 1.0.x へ切り替えた後、1.0.x-makecountryinput を merge します。
GitHub へ Push します。
ローカル及び GitHub の 1.0.x-makecountryinput を削除します。
次回は。。。
- いくつか気になっている点があるので、その辺を修正します。
- 修正後に登録画面 ( 入力→確認→完了 ) のテストクラスを作成します。
ソースコード
application-develop.properties
■その1
spring.thymeleaf.cache = false spring.datasource.url = jdbc:log4jdbc:mysql://localhost/world spring.datasource.username = root spring.datasource.password = xxxxxxxx spring.datasource.driverClassName = net.sf.log4jdbc.sql.jdbcapi.DriverSpy spring.jpa.hibernate.ddl-auto = none hibernate.dialect = org.hibernate.dialect.MySQLDialect spring.messages.cache-seconds = 0
spring.messages.cache-seconds = 0
を追加します。値を 0 で設定することでキャッシュを無効にします。
■その2
spring.thymeleaf.cache = false spring.datasource.url = jdbc:log4jdbc:mysql://localhost/world spring.datasource.username = root spring.datasource.password = xxxxxxxx spring.datasource.driverClassName = net.sf.log4jdbc.sql.jdbcapi.DriverSpy spring.jpa.hibernate.ddl-auto = none spring.jpa.hibernate.naming_strategy = org.hibernate.cfg.EJB3NamingStrategy hibernate.dialect = org.hibernate.dialect.MySQLDialect spring.messages.cache-seconds = 0
spring.jpa.hibernate.naming_strategy = org.hibernate.cfg.EJB3NamingStrategy
を追加します。
ApplicationConfig.java
package ksbysample.webapp.basic.config; import org.apache.ibatis.session.SqlSessionFactory; import org.mybatis.spring.SqlSessionFactoryBean; import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.env.YamlPropertySourceLoader; import org.springframework.context.MessageSource; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.support.ReloadableResourceBundleMessageSource; import org.springframework.core.env.PropertySource; import org.springframework.core.io.ClassPathResource; import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; import javax.sql.DataSource; import java.io.IOException; @Configuration @MapperScan("ksbysample.webapp.basic") public class ApplicationConfig { @Bean @ConfigurationProperties("spring.datasource") public DataSource dataSource() { return DataSourceBuilder.create().build(); } @Bean public SqlSessionFactory sqlSessionFactory() throws Exception { SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean(); sqlSessionFactoryBean.setDataSource(dataSource()); sqlSessionFactoryBean.setTypeAliasesPackage("ksbysample.webapp.basic"); return sqlSessionFactoryBean.getObject(); } }
- messageSource Bean を削除します。
CountryRepository.java
package ksbysample.webapp.basic.service; import ksbysample.webapp.basic.domain.Country; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; @Repository public interface CountryRepository extends CrudRepository<Country, String> { }
CRUD の機能だけ必要なので JpaRepository インターフェースではなく CrudRepository インターフェースを継承しています。ちなみに JpaRepository が一番機能があり以下の画像の全てのメソッドが使用できます。CrudRepository が一番機能が少なく下半分のメソッドしか使用できません。
※この画像のダイアログは
public interface CountryRepository
をpublic class CountryRepository
に変更した後、Alter+Enter を押してコンテキストメニューを表示して「Implement Methods」を選択すると表示されます。
CountryService.java
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); } Page<Country> page = new PageImpl<Country>(countryList, pageable, count); return page; } @Transactional public void save(CountryForm countryForm) { Country country = new Country(); BeanUtils.copyProperties(countryForm, country); countryRepository.save(country); } }
private CountryRepository countryRepository;
を追加します。- save メソッドを追加します。save メソッドには @Transactional アノテーションを付加します。
CountryController.java
package ksbysample.webapp.basic.web; import ksbysample.webapp.basic.config.Constant; import ksbysample.webapp.basic.service.CountryService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.validation.BindingResult; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.WebDataBinder; import org.springframework.web.bind.annotation.InitBinder; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.servlet.mvc.support.RedirectAttributes; import javax.servlet.http.HttpServletResponse; import java.io.IOException; @Controller @RequestMapping("/country") public class CountryController { @Autowired private Constant constant; @Autowired private CountryFormValidator countryFormValidator; @Autowired private CountryService countryService; @InitBinder public void initBinder(WebDataBinder binder) { binder.addValidators(countryFormValidator); } @RequestMapping("/input") public String input(CountryForm countryForm , Model model) { model.addAttribute("continentList", constant.getCONTINENT_LIST()); return "country/input"; } @RequestMapping("/input/back") public String inputBack(CountryForm countryForm , RedirectAttributes redirectAttributes) { redirectAttributes.addFlashAttribute("countryForm", countryForm); return "redirect:/country/input"; } @RequestMapping("/confirm") public String confirm(@Validated CountryForm countryForm , BindingResult bindingResult , Model model) { if (bindingResult.hasErrors()) { model.addAttribute("continentList", constant.getCONTINENT_LIST()); return "country/input"; } return "country/confirm"; } @RequestMapping("/update") public String update(@Validated CountryForm countryForm , BindingResult bindingResult , Model model , HttpServletResponse response) throws IOException { if (bindingResult.hasErrors()) { response.sendError(HttpStatus.BAD_REQUEST.value()); return null; } countryService.save(countryForm); return "redirect:/country/complete"; } @RequestMapping("/complete") public String complete() { return "country/complete"; } }
private CountryService countryService;
を追加します。- update メソッドの引数に
@Validated CountryForm countryForm
,BindingResult bindingResult
,Model model
,HttpServletResponse response
を追加します。またメソッド内の処理で、チェックエラーが発生した場合には HTTPステータスコード 400 を返す処理を追加し、問題なければ countryService.save を呼び出して入力された値を DB に登録します。
application-product.properties
spring.thymeleaf.cache = true spring.datasource.url = jdbc:mysql://localhost/world spring.datasource.username = root spring.datasource.password = xxxxxxxx spring.datasource.driverClassName = com.mysql.jdbc.Driver spring.jpa.hibernate.ddl-auto = none spring.jpa.hibernate.naming_strategy = org.hibernate.cfg.EJB3NamingStrategy hibernate.dialect = org.hibernate.dialect.MySQLDialect
spring.jpa.hibernate.naming_strategy = org.hibernate.cfg.EJB3NamingStrategy
を追加します。
CountryForm.java
@NotNull @Digits(integer=11, fraction=0, message = "{error.digits.integeronly}") private Long population;
- population の型を BigDecimail → Long に変更します。
messages_ja_JP.properties
AbstractUserDetailsAuthenticationProvider.locked=入力された ID はロックされています AbstractUserDetailsAuthenticationProvider.disabled=入力された ID は使用できません AbstractUserDetailsAuthenticationProvider.expired=入力された ID の有効期限が切れています AbstractUserDetailsAuthenticationProvider.credentialsExpired=入力された ID のパスワードの有効期限が切れています AbstractUserDetailsAuthenticationProvider.badCredentials=入力された ID あるいはパスワードが正しくありません typeMismatch.java.math.BigDecimal=数値を入力して下さい。 typeMismatch.java.lang.Long=数値を入力して下さい。 countryForm.code2.equalCode = Code2 には Code と異なる文字列を入力して下さい。 countryForm.continent.notAsia = Name に "Japan" あるいは "日本" を入力している場合、Continent は "Asia" を選択して下さい。 countryForm.region.notAsiaPattern = Continent に "Asia" を選択している場合、Region には "Eastern Asia", "Middle East", "Southeast Asia", "Southern and Central Asia" のいずれかの文字列を入力して下さい。
typeMismatch.java.lang.Long
を追加します。
CountryControllerTest.java
@Ignore @Test public void testConfirm() throws Exception { this.mvc.perform(get("/country/confirm")).andExpect(status().isOk()) .andExpect(content().contentType("text/html;charset=UTF-8")) .andExpect(view().name("country/confirm")) .andExpect(xpath("/html/head/title").string("Countryデータ登録画面(確認)")); } @Ignore @Test public void testUpdate() throws Exception { this.mvc.perform(get("/country/update")).andExpect(status().isFound()) .andExpect(redirectedUrl("/country/complete")); }
履歴
2015/02/20
初版発行。