Spring Boot でログイン画面 + 一覧画面 + 登録画面の Webアプリケーションを作る ( その21 )( 検索画面 ( Spring Data JPA 版 ) 作成 )
概要
Spring Boot でログイン画面 + 一覧画面 + 登録画面の Webアプリケーションを作る ( その20 )( 登録画面作成5 ) の続きです。
- 今回の手順で確認できるのは以下の内容です。
- 検索/一覧画面 ( Spring Data JPA 版 ) の作成
- Spring Data JPA の Specifications を使用して検索/一覧画面を作成します。ページネーションも実装します。
- URL は 検索/一覧画面 ( MyBatis-Spring版 ) とは別の /countryListJpa にします。ただし機能は /countryList と全く一緒です。
- テストは書きません。
ソフトウェア一覧
参考にしたサイト
Spring Data JPA の Specificationでらくらく動的クエリー
http://qiita.com/tag1216/items/3a408d2751a6310e2948Spring Data JPA でのクエリー実装方法まとめ
http://qiita.com/tag1216/items/55742fdb442e5617f727
手順
ブランチの作成
- IntelliJ IDEA 上で 1.0.x-makecountrylistjpa ブランチを作成します。
テンプレートファイルの新規作成、ヘッダーの修正
src/main/resources/templates の下の countryList.html をコピー&ペーストして countryListJpa.html を作成します。作成後、リンク先の内容 に変更します。
src/main/resources/templates/common の下の header.html を リンク先の内容 に変更します。
画面上部に "検索/一覧(JPA)" のメニューが表示されているか確認します。bootRun タスクを実行して Tomcat を起動後、ログイン画面からログインして検索/一覧画面を表示します。
CountryRepository クラスの変更
CountrySpecifications クラスの新規作成、及び CountryService クラスの変更
src/main/java/ksbysample/webapp/basic/service の下に CountrySpecifications.java を新規作成します。作成後、リンク先の内容 に変更します。
src/main/java/ksbysample/webapp/basic/service の下の CountryService.java を リンク先の内容 に変更します。
Controller クラスの新規作成
- src/main/java/ksbysample/webapp/basic/web の下に CountryListJpaController.java を新規作成します。作成後、リンク先の内容 に変更します。
動作確認
Tomcat は起動したままでしたのでそのまま行けるかと思いましたが、/countryListJpa にアクセスできませんでした。Run View で Ctrl+F5 を押して Tomcat を再起動します。
ブラウザで http://localhost:8080/countryListJpa にアクセスして検索/一覧画面を表示して動作を確認します。
- 画面の表示は問題なし。
ページングも正常に動作します。ページャーの2ページ目のリンクをクリックしたら以下の SQL ( countで件数取得 → データ取得 の順で select を2回 ) が実行されていました。
select count(country0.code) as col_0_0 from Country country0
select country0.code as code1_0, ..... from Country country0 limit 5, 5検索条件を入力すると以下の SQL ( where 句だけ記載 ) が実行されていました。
■全ての検索条件を入力した場合
where (country0.code like '%J%') and (country0.name like '%Japan%') and (country0.continent like '%Asia%') and (country0.localName like '%Nippon%')
■localName だけ入力した場合
where country0_.localName like '%Ni%'
Run View で Ctrl+F2 を押して Tomcat を停止します。
commit、GitHub へ Push、1.0.x-makecountrylistjpa -> 1.0.x へ Pull Request、1.0.x でマージ、1.0.x-makecountrylistjpa ブランチを削除
commit の前に build タスクを実行し、BUILD SUCCESSFUL が表示されることを確認します。
commit、GitHub へ Push、1.0.x-makecountrylistjpa -> 1.0.x へ Pull Request、1.0.x でマージ、1.0.x-makecountrylistjpa ブランチを削除、をします。
考察
MyBatis と Spring Data JPA のインターフェースを合わせておいたので、今回は実装が楽でした。
基本的には Spring Data JPA で実装し、検索条件が複雑で SQL ファイルで実装したいものだけ MyBatis で実装するという判断はありではないでしょうか。MyBatis でも Pageable クラスを使用したインターフェースにすることは Spring Boot でログイン画面 + 一覧画面 + 登録画面の Webアプリケーションを作る ( その7 )( 検索/一覧画面 ( MyBatis-Spring版 ) 作成2 ) で書いた通り可能です。
次回は。。。
以下に記載した内容の内、出来るところまで進めます。
- 登録画面で既に code が同じデータが登録されている場合にはエラーになるようにします。エラーメッセージは code の入力フィールドの下ではなく画面上部に共通エラーメッセージ表示エリアを新規追加してそこに表示させます。
- Spring Boot でログイン画面 + 一覧画面 + 登録画面の Webアプリケーションを作る ( その15 )( 登録画面作成4 ) で書きましたが、登録画面で surfaceArea と population は入力できる値と Bean Validation の入力チェックが合っていないので修正します。
- logback.xml で出力方法の設定をきちんと書いていなかったので、設定を書きます。
上記の対応後に、Windows で本番稼働させるためのディレクトリ作成、bat ファイル作成、サービス登録、jar ファイル配置、動作確認して完了にする予定です。
ソースコード
countryListJpa.html
<!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="utf-8"/> <meta http-equiv="X-UA-Compatible" content="IE=edge"/> <meta name="viewport" content="width=device-width, initial-scale=1"/> <title>検索/一覧画面</title> <!-- Bootstrap core CSS --> <link href="/css/bootstrap.min.css" rel="stylesheet"/> <!-- HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries --> <!--[if lt IE 9]> <script src="/js/html5shiv.min.js"></script> <script src="/js/respond.min.js"></script> <![endif]--> <!-- Custom styles for this template --> <style> <!-- body { padding-top: 70px; } .navbar-brand { font-size: 24px; } .panel-heading { padding: 5px 10px; } .form-group { margin-bottom: 5px; } .pagination { margin: 10px 0; } .panel { margin-bottom: 10px; } .table { margin-bottom: 10px; } --> </style> </head> <body> <div id="header" th:include="common/header :: header (active='countryListJpa')"></div> <div class="container"> <div class="panel panel-default"> <div class="panel-heading"> <div class="panel-title">検索条件</div> </div> <div class="panel-body"> <form id="searchForm" method="post" action="/countryListJpa" th:action="@{/countryListJpa}" th:object="${countryListForm}" class="form-horizontal"> <div class="form-group" th:classappend="${#fields.hasErrors('*{code}')} ? 'has-error' : ''"> <label for="code" class="control-label col-sm-2">Code</label> <div class="col-sm-10"> <div class="row"><div class="col-sm-2"><input type="text" name="code" id="code" maxlength="3" class="form-control input-sm" th:field="*{code}"/></div></div> <div class="row" th:if="${#fields.hasErrors('*{code}')}"><div class="col-sm-10"><p class="form-control-static text-danger"><small th:errors="*{code}"></small></p></div></div> </div> </div> <div class="form-group" th:classappend="${#fields.hasErrors('*{name}')} ? 'has-error' : ''"> <label for="name" class="control-label col-sm-2">Name</label> <div class="col-sm-10"> <div class="row"><div class="col-sm-6"><input type="text" name="name" id="name" maxlength="52" class="form-control input-sm" th:field="*{name}"/></div></div> <div class="row" th:if="${#fields.hasErrors('*{name}')}"><div class="col-sm-10"><p class="form-control-static text-danger"><small th:errors="*{name}"></small></p></div></div> </div> </div> <div class="form-group" th:classappend="${#fields.hasErrors('*{continent}')} ? 'has-error' : ''"> <label for="continent" class="control-label col-sm-2">Continent</label> <div class="col-sm-10"> <div class="row"><div class="col-sm-3"><input type="text" name="continent" id="continent" class="form-control input-sm" th:field="*{continent}"/></div></div> <div class="row" th:if="${#fields.hasErrors('*{continent}')}"><div class="col-sm-10"><p class="form-control-static text-danger"><small th:errors="*{continent}"></small></p></div></div> </div> </div> <div class="form-group" th:classappend="${#fields.hasErrors('*{localName}')} ? 'has-error' : ''"> <label for="localName" class="control-label col-sm-2">LocalName</label> <div class="col-sm-10"> <div class="row"><div class="col-sm-5"><input type="text" name="localName" id="localName" maxlength="45" class="form-control input-sm" th:field="*{localName}"/></div></div> <div class="row" th:if="${#fields.hasErrors('*{localName}')}"><div class="col-sm-10"><p class="form-control-static text-danger"><small th:errors="*{localName}"></small></p></div></div> </div> </div> <div class="text-center"> <button type="submit" value="検索" class="btn btn-primary">検索</button> <button type="reset" value="クリア" class="btn btn-default js-searchForm-clear">クリア</button> </div> </form> </div> </div> <div th:if="${page} != null"> <div id="pagenation" th:include="common/pagenation :: pagenation (url='/countryListJpa', page=${page}, ph=${ph})"></div> <table class="table table-condensed table-bordered table-striped"> <tr class="info"> <th>Code</th> <th>Name</th> <th>Continent</th> <th>LocalName</th> </tr> <tr th:each="country : ${page.content}"> <td th:text="${country.code}">ABW</td> <td th:text="${country.name}">Aruba</td> <td th:text="${country.continent}">North America</td> <td th:text="${country.localName}">Aruba</td> </tr> </table> <div id="pagenation" th:include="common/pagenation :: pagenation (url='/countryListJpa', page=${page}, ph=${ph})"></div> <form id="pagenationForm" method="post" action="#" th:action="@{#}" th:object="${countryListForm}"> <input type="hidden" name="code" id="code" th:value="*{code}"/> <input type="hidden" name="name" id="name" th:value="*{name}"/> <input type="hidden" name="continent" id="continent" th:value="*{continent}"/> <input type="hidden" name="localName" id="localName" th:value="*{localName}"/> </form> </div> </div> <!-- Bootstrap core JavaScript ================================================== --> <!-- Placed at the end of the document so the pages load faster --> <script src="/js/jquery.min.js"></script> <script src="/js/bootstrap.min.js"></script> <!-- IE10 viewport hack for Surface/desktop Windows 8 bug --> <script src="/js/ie10-viewport-bug-workaround.js"></script> <script type="text/javascript"> <!-- $(document).ready(function() { $('#code').focus(); $('.js-searchForm-clear').click(function(){ $('#searchForm') .find('input') .val(''); $('#code').focus(); }); $('.js-pagenation').each(function(){ $(this).click(function(){ $('#pagenationForm').attr('action', $(this).attr('href')); $(this).attr('href', '#'); $('#pagenationForm').submit(); }); }); }); --> </script> </body> </html>
<div id="header" th:include="common/header :: header (active='countryList')"></div>
→<div id="header" th:include="common/header :: header (active='countryListJpa')"></div>
へ変更します。<form id="searchForm" method="post" action="/countryList" th:action="@{/countryList}" th:object="${countryListForm}" class="form-horizontal">
→<form id="searchForm" method="post" action="/countryListJpa" th:action="@{/countryListJpa}" th:object="${countryListForm}" class="form-horizontal">
へ変更します。action の URL を countryListJpa へ変更しています。<div id="pagenation" th:include="common/pagenation :: pagenation (url='/countryList', page=${page}, ph=${ph})"></div>
→<div id="pagenation" th:include="common/pagenation :: pagenation (url='/countryListJpa', page=${page}, ph=${ph})"></div>
へ変更します。2箇所あります。
header.html
<!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="utf-8"/> <meta http-equiv="X-UA-Compatible" content="IE=edge"/> <meta name="viewport" content="width=device-width, initial-scale=1"/> <title>ヘッダー</title> <!-- Bootstrap core CSS --> <link href="/css/bootstrap.min.css" rel="stylesheet"/> <!-- HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries --> <!--[if lt IE 9]> <script src="/js/html5shiv.min.js"></script> <script src="/js/respond.min.js"></script> <![endif]--> <!-- Custom styles for this template --> <style> <!-- body { padding-top: 50px; } .navbar-brand { font-size: 24px; } .form-group { margin-bottom: 5px; } --> </style> </head> <body> <div th:fragment="header (active)"> <nav class="navvar navbar-inverse navbar-fixed-top" role="navigation"> <div class="container"> <div class="navbar-header"> <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar"> <span class="sr-only">ナビゲーションボタン</span> <span class="icon-bar"></span> <span class="icon-bar"></span> <span class="icon-bar"></span> </button> <div class="navbar-brand">WebApp - Basic</div> </div> <div id="navbar" class="collapse navbar-collapse"> <ul class="nav navbar-nav navbar-left"> <li th:class="${active == 'countryList'} ? 'active'"><a href="/countryList"><span class="glyphicon glyphicon-search"></span> 検索/一覧</a></li> <li th:class="${active == 'countryInput'} ? 'active'"><a href="/country/input"><span class="glyphicon glyphicon-pencil"></span> 登録</a></li> <li th:class="${active == 'countryListJpa'} ? 'active'"><a href="/countryListJpa"><span class="glyphicon glyphicon-search"></span> 検索/一覧(JPA)</a></li> </ul> <ul class="nav navbar-nav navbar-right"> <li><a href="/logout"><span class="glyphicon glyphicon-log-out"></span> ログアウト</a></li> </ul> </div> </div> </nav> </div> </body> </html>
<li th:class="${active == 'countryListJpa'} ? 'active'"><a href="/countryListJpa"><span class="glyphicon glyphicon-search"></span> 検索/一覧(JPA)</a></li>
を追加します。
CountryRepository.java
package ksbysample.webapp.basic.service; import ksbysample.webapp.basic.domain.Country; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; import org.springframework.data.repository.CrudRepository; import org.springframework.stereotype.Repository; @Repository public interface CountryRepository extends CrudRepository<Country, String>, JpaSpecificationExecutor<Country> { }
extends 節に
, JpaSpecificationExecutor<Country>
を追加します。今回は JpaSpecificationExecutor インターフェースの findAll メソッドを使用します ( 以下の画像を参照 )。
CountrySpecifications.java
package ksbysample.webapp.basic.service; import ksbysample.webapp.basic.domain.Country; import org.apache.commons.lang3.StringUtils; import org.springframework.data.jpa.domain.Specification; public class CountrySpecifications { public static Specification<Country> codeContains(String code) { return StringUtils.isEmpty(code) ? null : (root, query, cb) -> cb.like(root.get("code"), "%" + code + "%"); } public static Specification<Country> nameContains(String name) { return StringUtils.isEmpty(name) ? null : (root, query, cb) -> cb.like(root.get("name"), "%" + name + "%"); } public static Specification<Country> continentContains(String continent) { return StringUtils.isEmpty(continent) ? null : (root, query, cb) -> cb.like(root.get("continent"), "%" + continent + "%"); } public static Specification<Country> localNameContains(String localName) { return StringUtils.isEmpty(localName) ? null : (root, query, cb) -> cb.like(root.get("localName"), "%" + localName + "%"); } }
- 「参考にしたサイト」の URL のページのサンプルと IntelliJ IDEA の Lambda 変換機能を利用して Lambda で書いてみましたが、自分1人で最初から上のように書ける自信がありません。簡潔だけど、分かりやすいかどうかと言えば自分にはまだまだ Lambda は分かりにくいですね。。。
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.data.jpa.domain.Specifications; import org.springframework.stereotype.Service; import java.util.Collections; import java.util.List; import static ksbysample.webapp.basic.service.CountrySpecifications.*; @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<>(countryList, pageable, count); } public Page<Country> findCountryJpa(CountryListForm countryListForm, Pageable pageable) { return countryRepository.findAll( Specifications .where(codeContains(countryListForm.getCode())) .and(nameContains(countryListForm.getName())) .and(continentContains(countryListForm.getContinent())) .and(localNameContains(countryListForm.getLocalName())) , pageable); } public void save(CountryForm countryForm) { Country country = new Country(); BeanUtils.copyProperties(countryForm, country); countryRepository.save(country); } }
import static ksbysample.webapp.basic.service.CountrySpecifications.*;
を追加します。findCountryJpa
メソッドを追加します。- またコミット時に警告が出ていたので
findCountry
メソッド内のreturn new PageImpl<Country>(countryList, pageable, count);
→return new PageImpl<>(countryList, pageable, count);
へ変更します。
CountryListJpaController.java
package ksbysample.webapp.basic.web; import ksbysample.webapp.basic.domain.Country; import ksbysample.webapp.basic.helper.PagenationHelper; import ksbysample.webapp.basic.service.CountryService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.web.PageableDefault; 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.annotation.RequestMapping; @Controller @RequestMapping("/countryListJpa") public class CountryListJpaController { @Autowired private CountryService countryService; private static final int DEFAULT_PAGEABLE_SIZE = 5; @RequestMapping public String index(@Validated CountryListForm countryListForm , BindingResult bindingResult , @PageableDefault(size = DEFAULT_PAGEABLE_SIZE, page = 0) Pageable pageable , Model model) { if (bindingResult.hasErrors()) { return "countryListJpa"; } Page<Country> page = countryService.findCountryJpa(countryListForm, pageable); PagenationHelper ph = new PagenationHelper(page); model.addAttribute("page", page); model.addAttribute("ph", ph); return "countryListJpa"; } }
履歴
2015/03/22
初版発行。