SpringBoot、Thymeleafでのページング機能の実装(Java初心者向け)

はじめに

 現在のプロジェクトでは、プログラムはJava(Spring-Boot)、フロントはthymeleaf、Bootstrapを使って開発をしています。
 開発する中で、時間を要したのがページネーション処理です。同環境での実装例も見当たらず、数日かかっての実装となりました。
 そこで、Spring-Boot、Tymeleaf環境でのページネーションの実装方法をご紹介します。同環境でページネーション処理につまずいている方に読んでいただきたいです。

環境

実装ページング機能概要

  • テーブルでData内容を表示する(3件づつ)
  • ページ数の表示、そのページへの遷移が可能
  • 現在のページの所在が分かる。
  • 「次へ」「前へ」を表出させて遷移できる。
  • 「最初」「最後」を表出させて遷移できる。
  • 表示するページ数を制限する(3ページ)

ディレクトリ構成

src/
├ java/
│ ├ sampleController.java
│ ├ sampleForm.java
│ ├ sampleDao.java
│ └ member.java
└ resourse/
└ html/
└ sample.html

1-1 Controller

package jp.sbworks.standard.web.controller;

import java.util.HashMap;
import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;

import jp.sbworks.standard.web.dao.SampleDao;
import jp.sbworks.standard.web.db.single.pojo.SSample;

@Controller
public class SampleController {

    /** 1ページの表示数 */
    private final String limit = "3";

    /** ページネーションで表示するページ数 */
    private int showPageSize = 3;

    @Autowired
    private SampleDao sampleDao;


    @RequestMapping(value = "/sample",method = RequestMethod.GET)
    @Transactional(readOnly = true)
    public String index(Model model, @RequestParam HashMap<String, String> params) throws Exception {
        // パラメータを設定し、現在のページを取得する    
        String currentPage = params.get("page");

        // 初期表示ではパラメータを取得できないので、1ページに設定
        if (currentPage == null){
            currentPage = "1";
        }
        // データ取得時の取得件数、取得情報の指定
        HashMap<String, String> search = new HashMap<String, String>();
        search.put("limit", limit);
        search.put("page", currentPage);
    
        int total = 0;
        List<SSample> list = null;
        try {
            // データ総数を取得
            total = sampleDao.getMemberListCount();
            // データ一覧を取得
            list = sampleDao.getMemberList(search);
        } catch (Exception e) {
            return "error/fatal";
        }

        // pagination処理
        // "総数/1ページの表示数"から総ページ数を割り出す
        int totalPage = (total + Integer.valueOf(limit) -1) / Integer.valueOf(limit);
        int page = Integer.valueOf(currentPage);
        // 表示する最初のページ番号を算出(今回は3ページ表示する設定)
        // (例)1,2,3ページのstartPageは1。4,5,6ページのstartPageは4
        int startPage = page - (page-1)%showPageSize;
        // 表示する最後のページ番号を算出
        int endPage = (startPage+showPageSize-1 > totalPage) ? totalPage :    startPage+showPageSize-1;
        model.addAttribute("list", list);
        model.addAttribute("total", total);
        model.addAttribute("page", page);
        model.addAttribute("totalPage", totalPage);
        model.addAttribute("startPage", startPage);
        model.addAttribute("endPage", endPage);
        return "html/sample/index";
        }
    }

1-2 DAO

package jp.sbworks.standard.web.dao;

import java.io.IOException;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.List;

import org.sql2o.Connection;
import org.sql2o.Query;
import org.sql2o.Sql2o;

import jp.sbworks.standard.web.db.single.pojo.SSample;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;


@Repository
public class SampleDao {

    @Autowired
// データベースアクセス用のライブラリ
    private Sql2o sql2o;
 
    // リスト情報を取得する 
    public List<SSample> getMemberList(HashMap<String, String> search) throws SQLException, IOException {
        // Jdbcに接続する
        Connection con = sql2o.open();
        con.getJdbcConnection().setAutoCommit(false);
        // クエリの作成
        Query query = con.createQuery("select ID as id, NAME as name, DEPARTMENT as department, DAY_OF_JOINING as dayOfJoining from SAMPLE limit :limit offset :offset;");
        int limit = Integer.valueOf(search.get("limit"));
        int page = Integer.valueOf(search.get("page")) - 1;
        // 何件情報を取得するかの指定。
        query = query.addParameter("limit", limit);
        // 何件目からの情報を取得するかの指定(※コントローラからパラメータを使って現在のページ数が分かる。それによって何件目からの情報を取得すればいいのかが分かる。)
        query = query.addParameter("offset", limit * page);

        return query.executeAndFetch(SSample.class);
    }
    
    // リスト情報件数を取得する 
    public int getMemberListCount() throws SQLException, IOException {
        Connection con = sql2o.open();
        con.getJdbcConnection().setAutoCommit(false);
        Query query =  con.createQuery("select count(1) from SAMPLE");
        return query.executeAndFetchFirst(Integer.class);
     }
}

1-3 DB

package jp.sbworks.standard.web.db.single.pojo;

import java.io.Serializable;
import java.time.LocalDate;

public class SSample implements Serializable {

    // ID 
    public Integer id;
    // 名前 
    public String name;
    // 部署 
    public Integer department;
    // 入社日
    public LocalDate dayOfJoining;
    // 登録者 
    public String regName;

}

1-4 html

<!DOCTYPE html>
<html lang="ja">
    <head>
        <script type="text/javascript" th:src="@{/webpack-bundled/Sample/bundled.js}"></script>
    </head>
<div class="table-scroll mb-2">
    <table class="table table-bordered table-sm mt-1">
        <thead>
            <tr>
                <th>社員ID</th>
                <th>名前</th>
                <th>部署</th>
                <th>入社日</th>
                <th>登録者</th>
            </tr>
        </thead>
        <tbody>
            <th:block th:each="item, status : ${list}">
                <tr>
                    <td th:text="${item.id}"></td>
                    <td th:text="${item.name}"></td>
                    <td th:text="${item.department}"></td>
                    <td th:text="${item.dayofjoining}"></td>
                    <td th:text="${item.regname}"></td>
                </tr>
            </th:block>
        </tbody>
    </table>
</div>
<!-- ここからページング処理 -->
<nav>
    <ul class="pagination pg-blue justify-content-center">
        <li th:if="${startPage} > 1" class="page-item ">
            <a class="page-link" th:href="@{'/sample?page=1'}" tabindex="-2">最初</a>
        </li>
        <li th:if="${page} > 1" class="page-item ">
            <a class="page-link" th:href="@{'/sample?page=' + ${page-1}}" tabindex="-1">前へ</a>
        </li>
        <th:block th:if="${endPage}<=0">
            <li class="page-item " th:classappend="${i == page} ? active" th:each="i : ${startPage}">
                <a class="page-link" th:href="@{'/sample?page=' + ${i}}" th:text="${i}"></a>
            </li>
        </th:block>
        <!-- StartPageからEndPageまでのページ数を表示する -->
        <th:block th:if="${endPage}>0">
            <li class="page-item " th:classappend="${i == page} ? active" th:each="i : ${#numbers.sequence(startPage, endPage)}">
                <a class="page-link" th:href="@{'/sample?page=' + ${i}}" th:text="${i}"></a>
            </li>
        </th:block>
        <li th:if="${page} < ${totalPage}" class="page-item ">
            <a class="page-link" th:href="@{'/sample?page=' + ${page+1}}">次へ</a>
        </li>
        <li th:if="${endPage} < ${totalPage}" class="page-item ">
            <a class="page-link" th:href="@{'/sample?page=' + ${totalPage}}">最後</a>
        </li>
    </ul>
</nav>

現在いるページの所在を分かりやすくするために、Bootstrapを使って色付けをしています。下記は現在表示しているページとして青色で表示する場合の処理を簡単に書きました。

<nav>
    <ul class="pagination pg-blue">
…
<li th:classappend="${i == page} ? active" >

結果

下記のように実装できました。

まとめ

ページングの処理は煩雑なりがちですが、基本的な動きはJavaで実装し、Thymeleafで条件分岐、繰り返し処理を活用することで、より可読性が高く、シンプルなプログラムとなります。
 私がつまづいた箇所は、2ページ以降のデータの取得です。コントローラでパラメータを使って現在のpage番号を取得することで、取得を始めるデータを特定でき、ページごとに正しいデータを取得することに成功しました。
 1ページに表示するデータ数や、表示ページ数の指定をプロジェクト用に沿って替えることで、活用いただければ幸いです。

author/A.Nishihara