はじめに (対象読者・この記事でわかること)

この記事は、Spring BootでWebアプリを作っていて「とりあえずDBは後回しにして、画面だけページングを動かしてみたい」という方を対象にしています。
JPAやMyBatis、データベース設定なしで、メモリ上のリストをThymeleafでページング表示する方法をお伝えします。
記事を読み終えると、SpringのPageablePageImplを使って、DBを介さない簡易ページングが即座に実装できるようになります。

前提知識

  • Java 17以降の文法が読める
  • Spring Boot 3系のプロジェクトがspring-boot-starter-webspring-boot-starter-thymeleafで動作している
  • Thymeleafの基本的な変数表示(${})を知っている

なぜDBなしページングが欲かるのか

プロトタイプや社内ツールでは「最初は静的データで動作確認したい」という場面がよくあります。
DBを立ち上げるまでもなく、コントローラに適当なリストを用意するだけでページャを動かしたい。そのとき、Springが提供するPageableインターフェースを使えば、後からDBに置き換えてもコントローラはほぼ変更不要です。

メモリリストをPageableで切り出す実装手順

ステップ1:Pageableをコントローラで受ける

spring-boot-starter-data-commons(Spring Boot 3系ならデフォルトで入っています)を使えば、コントローラの引数にPageableを書くだけでページ番号・サイズが自動バインドされます。
サンプルコード:

Java
@Controller @RequestMapping("/items") public class ItemController { private final List<Item> allItems = IntStream.rangeClosed(1, 123) .mapToObj(i -> new Item(i, "アイテム" + i)) .toList(); // 静的データ @GetMapping public String list(Pageable pageable, Model model) { int start = (int) pageable.getOffset(); int end = Math.min(start + pageable.getPageSize(), allItems.size()); Page<Item> page = new PageImpl<>( allItems.subList(start, end), pageable, allItems.size() ); model.addAttribute("page", page); return "items/list"; } }

PageImplはSpring Dataが提供するPageインターフェースの単純な実装で、既存リストと合計件数を渡すだけでページ情報を構築できます。

ステップ2:Thymeleafでページャを描画

Thymeleafには<nav>タグとPageオブジェクトのメソッドを組み合わせるだけで、Bootstrap互換のページャが書けます。
templates/items/list.html

Html
<table class="table"> <tr th:each="item : ${page.content}"> <td th:text="${item.id}">1</td> <td th:text="${item.name}">アイテム1</td> </tr> </table> <nav th:if="${page.totalPages > 1}"> <ul class="pagination"> <li class="page-item" th:classappend="${page.first} ? 'disabled'"> <a class="page-link" th:href="@{/items(page=${page.number - 1},size=${page.size})}" tabindex="-1">前へ</a> </li> <li th:each="i : ${#numbers.sequence(0, page.totalPages - 1)}" class="page-item" th:classappend="${i == page.number} ? 'active'"> <a class="page-link" th:href="@{/items(page=${i},size=${page.size})}" th:text="${i + 1}">1</a> </li> <li class="page-item" th:classappend="${page.last} ? 'disabled'"> <a class="page-link" th:href="@{/items(page=${page.number + 1},size=${page.size})}">次へ</a> </li> </ul> </nav>

th:href="@{/items(page=${i},size=${page.size})}"でページ番号とサイズをクエリパラメータに追加します。
Pageableのデフォルトサイズ変更はapplication.propertiesspring.data.web.pageable.default-page-size=20のように設定できます。

ハマりどころ:Pageableのページ番号が0始まり

Thymeleafでページ番号を表示する際、人間にとって1番目は0ではなく1なので+1するのを忘れると「0ページ」が表示されてしまいます。
また、PageImplのコンストラクタに渡すoffset計算もpageable.getOffset()で済ませればミスがありません。

解決策:ユーティリティメソッドで隠蔽

ページャのロジックを毎回書くのは面倒なので、Thymeleafの@ControllerAdvice@ModelAttributeとしてページ情報を詰めるか、Thymeleafの#mvc.uriComponentsBuilderを使ったカスタムダイヤレクトを用意してしまうとスッキリします。

まとめ

本記事では、Spring Boot + ThymeleafでDBを使わずにPageableを使ったページングを実装する方法を解説しました。

  • Pageableをコントローラ引数に追加するだけでページ番号・サイズが取得できる
  • PageImplに既存リストと総件数を渡せば、あとはThymeleafでpage.totalPagesなどが使える
  • ページャのリンクはth:href="@{/items(page=${i})}"で簡単に生成できる

このテクニックを使えば、本番前の画面確認や社内ツールのプロトタイプで「とりあえず動くページャ」を五分で作れます。
次回は、同じコードベースをJPAに接続して「動的フィルタ+ページング」に拡張する方法を紹介します。

参考資料

  • Spring Data Commons 公式ドキュメント(Pageable, PageImpl)
  • Thymeleaf 公式ガイド「Constructing well-formed URLs」
  • 実装サンプルGitHubリポジトリ(随時更新)