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

マイクロフレームワークを使ってみた-Spark 編-

本記事の概要

  • Spark Framework チュートリアルやってみた
  • Spark Framework を実際のプロジェクトに使うかを評価してみた

こんにちは。SBWorksです。

突然ですが、みなさま。マイクロフレームワークという言葉を知っていますか?

マイクロフレームワークとは

microframework is a term used to refer to minimalistic web application frameworks. It is contrasted with full-stack frameworks.

出典 https://en.wikipedia.org/wiki/Microframework

簡単に言うと、軽量でフルスタックなウェブフレームワークって感じですね。最近は、急激な環境変化に対応する為に、お互いに疎なサービスを開発し、それらの組み合わせて大きなサービスを実行するという開発がトレンドになってきています。フレームワークもその急激な変化に対応するために、軽量なフレームワークの開発が盛んに行われているようです。色々なフレームワークが開発されている中で、今回はJavaのフレームワークである、Spark Frameworkを使ってみようと思います。(詳しく知りたい人は公式ページのドキュメントを参照してください。)

Spark Framework のチュートリアルやってみた

それでは実際にSpark Frameworkを使ってみましょう。

まずは公式ページへ。

チュートリアルページへ。

今回はBasic webapp structure のチュートリアルをやってみます。

Githubにソースコードが公開されている。チュートリアルが充実してるのはありがたいですね。

javaディレクトリ配下のソースコードはこんな感じ。util + 画面毎に分かれてる感じ。

resourceディレクトリ配下のソースコードはこんな感じ。velocity使ってる。

gradleとmavenがあったので、今回はgradleでビルドしてみます。

$ cd path/to/spark-basic-structure
$ gradle build

起動してみます。

$ gradle run

立ち上がったらログイン画面(http://localhost:4567/login/)へアクセスしてみます。

ひとまず動きました。あとはUsername/Passwordを入力して(UserDao.javaを確認)、書籍一覧画面(localhost:4567/books/)へアクセスしてみると、

書籍一覧も表示できました。

Spark Framework を実プロジェクトに使うかを検証

チュートリアルを動かすことはできたので、これを実プロジェクトで使えるのかを検証していきたいと思います。

まずは本番環境にリリースすることを想定して、jarファイルを作成して起動します。

$ cd path/to/spark-basic-structure
$ gradle build
$ java -jar build/libs/spark-basic-structure-all-1.0-SNAPSHOT.jar

起動したら、先ほどと同じようにログイン画面(http://localhost:4567/login/)へアクセスしてみます。

CSSが効いていない…。ソースコードで該当箇所を調べます。どうやら下記ソースコードがgradle run で起動した時とjarで起動した時で異なるようです。

staticFileLocation("/public");

調べてみると、同じようにrunタスクで起動した時には正常に動くのに、jarで起動すると、静的ファイルが読み込めないという記事がいくつかあったので同じようにハマっている人がいるようです。
公式にissueを投げている人がいて、grade shadowでjarファイル作ったらうまくいくよと書いていたので、試してみるもうまく行きませんでした。

jarでの不具合が出ている問題が解決できないと、本番稼働できないので、今回はSpark Framework の使用は一旦見送ることにしました。

結論

  • Spark Framework でチュートリアルやってみた
  • Spark Framework を実プロジェクトでは使わないことにした

最後に

今後もマイクロフレームワークを検証していきたいと思います。

ありがとうございました!