かんがるーさんの日記

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

Spring Boot でログイン画面 + 一覧画面 + 登録画面の Webアプリケーションを作る ( その7 )( 検索/一覧画面 ( MyBatis-Spring版 ) 作成2 )

概要

Spring Boot でログイン画面 + 一覧画面 + 登録画面の Webアプリケーションを作る ( その6 )( 検索/一覧画面 ( MyBatis-Spring版 ) 作成 ) の続きです。

  • 今回の手順で確認できるのは以下の内容です。
    • 検索/一覧画面 ( MyBatis-Spring版 ) の作成、確認
    • 今回はページネーションを実装します。Spring Data JPA でページネーションを実装するのと同じように Pageable クラス、Page クラスを使用するようにしてみます。

ソフトウェア一覧

参考にしたサイト

手順

1.0.x-makecountrylist2 ブランチの作成

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

CountryListForm.java の変更

  1. src/main/java/ksbysample/webapp/basic/web の下の CountryListForm.java をエディタで開き、リンク先の内容に変更します。

CountryMapper.xml, CountryMapper.java の変更

  1. src/main/resources/ksbysample/webapp/basic/service の下の CountryMapper.xml をエディタで開き、リンク先の内容に変更します。

  2. src/main/java/ksbysample/webapp/basic/service の下の CountryMapper.java をエディタで開き、リンク先の内容に変更します。

CountryService.java の findCountryメソッドの変更

  1. src/main/java/ksbysample/webapp/basic/service の下の CountryService.java をエディタで開き、リンク先の内容に変更します。

PagenationHelper.java の作成

画面のページネーションの処理のためのヘルパークラスを作成します。

  1. src/main/java/ksbysample/webapp/basic の下に helper パッケージを作成します。

  2. 作成した helper パッケージの下に PagenationHelper.java を作成します。作成後、リンク先の内容に変更します。

CountryListController.java の変更

  1. src/main/java/ksbysample/webapp/basic/web の下の CountryListController.java をエディタで開き、リンク先の内容に変更します。

pagenation.html の作成

画面のページネーションの共通部分を作成します。

  1. src/main/resources/templates/common の下に pagenation.html を作成します。作成後、リンク先の内容に変更します。

countryList.html の変更

  1. src/main/resources/templates の下の countryList.html をエディタで開き、リンク先の内容に変更します。

動作確認

  1. Gradle tasks View から bootRun タスクを実行します。

  2. ブラウザで http://localhost:8080/countryList にアクセスして検索/一覧画面を表示して動作を確認します。

  3. Ctrl+F2 を押して Tomcat を停止します。

commit する

  1. commit します。

    f:id:ksby:20150124171437p:plain

  2. 「Code Analysis」ダイアログが表示されますので、「Review」ボタンをクリックします。

  3. 「Code Analysis」に "Attribute th:... is not allowed here" の Warning が表示されていますので、エディタ上で 「Add th:... to custom html attributes」 を選択して追加して解消します。

  4. 再度 commit します。「Code Analysis」ダイアログが表示され「Review」ボタンをクリックすると Code Analysis View に以下の画像の Warning が表示されていますが、これらは無視します。

    f:id:ksby:20150124172350p:plain

  5. 再度 commit し、「Code Analysis」ダイアログが表示されたら「Commit」ボタンをクリックします。

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

  1. IntelliJ IDEA から GitHub へ Push します。

  2. GitHub で Pull Request を作成します。

  3. IntelliJ IDEA で 1.0.x へ切り替えた後、1.0.x-makecountrylist2 を merge します。

  4. commitした後、GitHub へ Push します。

  5. ローカル及び GitHub の 1.0.x-makecountrylist2 を削除します。

最後に

  • テストクラスは後で作成します。ページネーションの実装方法は結構時間がかかったのと、Spring Security を試してみたい気持ちが強いので、テストクラスの作成は後回しにします。
  • PagenationHelper クラスはとりあえず動けばいいで作りました。後でもっとまともなものに出来たらいいな。。。
  • テーブルの各カラムの幅が固定になっていないのも直したいけど一旦放置です。

ソースコード

CountryListForm.java

package ksbysample.webapp.basic.web;

import lombok.Data;

import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;

@Data
public class CountryListForm {

    @Size(max = 3, message = "{error.size.max}")
    private String code;

    @Size(max = 52, message = "{error.size.max}")
    private String name;

    @Pattern(regexp = "^(|Asia|Europe|North America|Africa|Oceania|Antarctica|South America)$", message = "{countryListForm.continent.pattern}")
    private String continent;

    @Size(max = 45, message = "{error.size.max}")
    private String localName;

    private long page;

    private long size;

}
  • メンバー変数 page, size を追加します。

CountryMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="ksbysample.webapp.basic.service.CountryMapper">

    <select id="selectCountryCount" parameterType="countryListForm" resultType="long">
        select  count(*)
        from    country co
        <include refid="selectCountryWhere"/>
    </select>

    <select id="selectCountry" parameterType="countryListForm" resultType="Country">
        select  co.Code
                , co.Name
                , co.Continent
                , co.Region
                , co.SurfaceArea
                , co.IndepYear
                , co.Population
                , co.LifeExpectancy
                , co.GNP
                , co.GNPOld
                , co.LocalName
                , co.GovernmentForm
                , co.HeadOfState
                , co.Capital
                , co.Code2
        from    country co
        <include refid="selectCountryWhere"/>
        order by co.Code
        <if test="countryListForm.size != null and countryListForm.page != null">
            limit ${countryListForm.page * countryListForm.size}, ${countryListForm.size}
        </if>
    </select>

    <sql id="selectCountryWhere">
        <where>
            <if test="countryListForm.code != null and countryListForm.code != ''">
                co.Code like '%${countryListForm.code}%'
            </if>
            <if test="countryListForm.name != null and countryListForm.name != ''">
                and co.Name like '%${countryListForm.name}%'
            </if>
            <if test="countryListForm.continent != null and countryListForm.continent != ''">
                and co.Continent like '%${countryListForm.continent}%'
            </if>
            <if test="countryListForm.localName != null and countryListForm.localName != ''">
                and co.LocalName like '%${countryListForm.localName}%'
            </if>
        </where>
    </sql>

</mapper>
  • <select id="selectCountryCount" ...</select> の部分を追加します。
  • SQL の where 句の部分を共通化するために <select id="selectCountryCount" ...<select id="selectCountry" ... の where句の部分を <sql id="selectCountryWhere"> ... </sql> に定義します。各 SQL の where 句の記述は <include refid="selectCountryWhere"/> に入れ替えます。
  • <select id="selectCountry" ...SQL文の order by の後に <if ...</if> の部分を追加します。

CountryMapper.java

package ksbysample.webapp.basic.service;

import ksbysample.webapp.basic.domain.Country;
import ksbysample.webapp.basic.web.CountryListForm;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public interface CountryMapper {

    public long selectCountryCount(@Param("countryListForm") CountryListForm countryListForm);

    public List<Country> selectCountry(@Param("countryListForm") CountryListForm countryListForm);

}

CountryService.java

package ksbysample.webapp.basic.service;

import ksbysample.webapp.basic.domain.Country;
import ksbysample.webapp.basic.web.CountryListForm;
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 java.util.Collections;
import java.util.List;

@Service
public class CountryService {

    @Autowired
    private CountryMapper countryMapper;

    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;
    }

}
  • findCountry の引数に pageable を追加し、戻り値を Page へ変更します。
  • 内部の処理を 件数を取得する → データ一覧を取得する → Page クラスのインスタンスを生成する、に変更します。

PagenationHelper.java

package ksbysample.webapp.basic.helper;

import lombok.Getter;

@Getter
public class PagenationHelper {

    private final int MAX_DISP_PAGE = 5;

    private boolean hiddenPrev;
    private boolean hiddenNext;

    private boolean hiddenPage2;
    private boolean hiddenPage3;
    private boolean hiddenPage4;
    private boolean hiddenPage5;

    private boolean activePage1;
    private boolean activePage2;
    private boolean activePage3;
    private boolean activePage4;
    private boolean activePage5;

    private int page1PageValue;

    public PagenationHelper(int number, int size, int totalPages) {
        this.hiddenPrev = (number == 0);
        this.hiddenNext = ((totalPages == 0) || (number == totalPages - 1));

        this.hiddenPage2 = (totalPages <= 1);
        this.hiddenPage3 = (totalPages <= 2);
        this.hiddenPage4 = (totalPages <= 3);
        this.hiddenPage5 = (totalPages <= 4);

        this.activePage1 = (number == 0);
        this.activePage2 = ((MAX_DISP_PAGE - 3 <= totalPages) && (number + 1 == MAX_DISP_PAGE - 3));
        this.activePage3 = (((totalPages == MAX_DISP_PAGE - 2) && (number + 1 == MAX_DISP_PAGE - 2))
                || ((MAX_DISP_PAGE - 2 < totalPages) && (MAX_DISP_PAGE - 2 <= number + 1) && (number + 1 < totalPages - 1)));
        this.activePage4 = ((0 < number) && (((totalPages == MAX_DISP_PAGE - 1) && (number + 1 == MAX_DISP_PAGE - 1))
                || ((MAX_DISP_PAGE - 1 < totalPages) && (number + 1 == totalPages - 1))));
        this.activePage5 = ((0 < number) && (MAX_DISP_PAGE <= totalPages) && (number + 1 == totalPages));

        if (totalPages <= MAX_DISP_PAGE) {
            this.page1PageValue = 0;
        } else {
            if (number + 1 <= MAX_DISP_PAGE - 2) {
                this.page1PageValue = 0;
            } else if ((MAX_DISP_PAGE - 1 <= number + 1) && (number + 1 <= totalPages - 2)) {
                this.page1PageValue = number - 2;
            } else {
                this.page1PageValue = totalPages - MAX_DISP_PAGE;
            }
        }
    }

}

CountryListController.java

package ksbysample.webapp.basic.web;

import ksbysample.webapp.basic.domain.Country;
import ksbysample.webapp.basic.helper.PageHelper;
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;

import java.util.List;

@Controller
@RequestMapping("/countryList")
public class CountryListController {

    @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 "countryList";
        }

        countryListForm.setSize(pageable.getPageSize());
        countryListForm.setPage(pageable.getPageNumber());
        Page<Country> page = countryService.findCountry(countryListForm, pageable);
        PageHelper ph = new PageHelper(page.getNumber(), page.getSize(), page.getTotalPages());

        model.addAttribute("page", page);
        model.addAttribute("ph", ph);

        return "countryList";
    }

}
  • 定数 DEFAULT_PAGEABLE_SIZE を追加します。
  • index メソッドに引数 pageable を追加します。page, size の URL パラメータが未指定の場合の値を @PageableDefault アノテーションで指定します。
  • データ取得の部分を上記のように変更します。

pagenation.html

<!DOCTYPE html SYSTEM "http://www.thymeleaf.org/dtd/xhtml1-strict-thymeleaf-spring4-4.dtd">
<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>xxxxxxxx</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="pagenation (url, page, ph)">
        <div class="text-center">
            <ul class="pagination">
                <li th:attr="style=${ph.hiddenPrev} ? 'visibility:hidden'"><a class="js-pagenation" th:href="@{${url}(page=${page.number - 1},size=${page.size})}">&#171;</a></li>
            </ul>

            <ul class="pagination">
                <li th:class="${ph.activePage1} ? 'active'">
                    <a class="js-pagenation" th:href="@{${url}(page=${ph.page1PageValue},size=${page.size})}" th:text="${ph.page1PageValue + 1}">1</a>
                </li>
                <li th:class="${ph.activePage2} ? 'active'" th:attr="style=${ph.hiddenPage2} ? 'visibility:hidden'">
                    <a class="js-pagenation" th:href="@{${url}(page=${ph.page1PageValue + 1},size=${page.size})}" th:text="${ph.page1PageValue + 2}">2</a>
                </li>
                <li th:class="${ph.activePage3} ? 'active'" th:attr="style=${ph.hiddenPage3} ? 'visibility:hidden'">
                    <a class="js-pagenation" th:href="@{${url}(page=${ph.page1PageValue + 2},size=${page.size})}" th:text="${ph.page1PageValue + 3}">3</a>
                </li>
                <li th:class="${ph.activePage4} ? 'active'" th:attr="style=${ph.hiddenPage4} ? 'visibility:hidden'">
                    <a class="js-pagenation" th:href="@{${url}(page=${ph.page1PageValue + 3},size=${page.size})}" th:text="${ph.page1PageValue + 4}">4</a>
                </li>
                <li th:class="${ph.activePage5} ? 'active'" th:attr="style=${ph.hiddenPage5} ? 'visibility:hidden'">
                    <a class="js-pagenation" th:href="@{${url}(page=${ph.page1PageValue + 4},size=${page.size})}" th:text="${ph.page1PageValue + 5}">4</a>
                </li>
            </ul>

            <ul class="pagination">
                <li th:attr="style=${ph.hiddenNext} ? 'visibility:hidden'"><a class="js-pagenation" th:href="@{${url}(page=${page.number + 1},size=${page.size})}">&#187;</a></li>
            </ul>
        </div>
    </div>


</body>
</html>

countryList.html

<!DOCTYPE html SYSTEM "http://www.thymeleaf.org/dtd/xhtml1-strict-thymeleaf-spring4-4.dtd">
<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='countryList')"></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="/countryList" th:action="@{/countryList}" 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 id="pagenation" th:include="common/pagenation :: pagenation (url='/countryList', 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='/countryList', page=${page}, ph=${ph})"></div>

        <form id="pagenationForm" method="post" 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>

    <!-- 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>
  • <button type="reset" value="クリア" class="btn btn-default">クリア</button><button type="reset" value="クリア" class="btn btn-default js-searchForm-clear">クリア</button> に変更します。
  • <div class="text-center"><ul class="pagination"></ul></div> の部分を <div id="pagenation" th:include="common/pagenation :: pagenation (url='/countryList', page=${page}, ph=${ph})"></div> に変更します。table の上下の2箇所とも変更します。
  • <tr th:each="country : ${countryList}"><tr th:each="country : ${page.content}"> に変更します。
  • ページネーションで使用する <form id="pagenationForm" ...> ... </form> を追加します。
  • $('.js-searchForm-clear').click(function(){ ... } を追加します。
  • $('.js-pagenation').each(function(){ ... } を追加します。

履歴

2015/01/24
初版発行。