0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Spring Bootでページネーションを実装する

Posted at

はじめに

Spring Bootアプリケーションで一覧表示を行う際のページ分割を実装する。

環境

開発環境は以下の通り。

Eclipse IDE Version: 2023-12 (4.30.0)
PostgreSQL 16.0,

関連記事

Spring Securityでログイン機能を実装する

Spring Bootで検索と一覧表示を行う

サンプルソース

Git Hub

アプリケーションプロパティ

関連記事と同じ。

build.gradle

関連記事と同じ。

サンプルプログラム

一覧

以下のアドレスにアクセスするとログイン画面が表示される。
http://localhost:8080
ログイン完了後に一覧画面に遷移すると表示するデータ(行数)により以下のようにページが分割される。

28ページの場合

image.png

2ページの場合

image.png

本サンプルのページネーションの仕様。

  • 一ページの表示行数は10行
  • 先頭ページ(1ページ)と最終ページを常に表示
  • ページ選択の両端に「<<」と「>>」を表示してページ番号クリック以外でもページ移動が可能
  • 4ページ括りで表示する。1~4ページのレイアウトで5ページ目は5~8ページのレイアウト
  • 先頭ページと最終ページの間に省略しているページがある場合は「・・」を表示

サンプルの解説

  • 指定されたページのデータの取得はSQLのROW_NUMBER()関数で10件ずつ取得
  • 一覧表示のHTMLにnavタグを配置してリスト項目をJavascriptで動的に作成
  • navタグの中のリスト項目タグのスタイルシートはBootstrapのページネーションを使用
  • ページ情報は一覧表示のHTMLにhidden属性で設定してコントローラーと受け渡しを行う
DTO

ページネーションで利用するのは以下の項目になる。

  • private Integer count: データ件数
  • private Integer page: 現在のページ
  • private Integer maxpage: 最大ページ数
UserForm.java
@Data
public class UserForm implements Serializable{
	private String code;
	private String name;
	private String kana;
	private String password;
	private Integer statecode;
	private Integer statuscode;
	private String divisionname;
	// 写真
	private MultipartFile imgfile;
	private String contenttype;
	private String imgString;
	
	private String searchname;
	private String searchkana;
	// 状態(複数)
	private List<Integer> statekeys;

	// 雇用形態
	private Integer statusKey;
	
	// 検索で状態と雇用形態の選択肢を保持する
	private Map<String, Map<Integer, String>> mapitems;
	private String mapstring;	
	private Map<String, Object> msg;
	
	// 検索結果
	private Integer count;
	private Integer page;
	private Integer maxpage;
	
	public UserForm() {
		code = "";
		name = "";
		kana= "";
		password = "";
		statecode = 0;
		statuscode = 0;
		divisionname = "";
		imgfile = null;
		contenttype = "";
		imgString = "";
		
		searchname = "";
		searchkana= "";
		statekeys = new ArrayList<Integer>();
		statusKey = 0;
		mapitems = new HashMap<String, Map<Integer, String>>();
		mapstring = "";
		msg = new HashMap<String, Object>();
		
		count = 0;
		page = 0;
		maxpage = 0;
	}
}
CSS

以下のスタイルはbootstrap.min.cssからコピーしている。

  • paginationはulタグ(リスト)のスタイル
  • page-itemはliタグ(リスト項目)のスタイル
  • page-linkはaタグ(リンク)のスタイル

コピーしないで以下のようにヘッダーに参照を入れるとすべてのスタイルが使える。

List.html
<head>
  <meta charset="UTF-8">
 <link rel="stylesheet" href="./css/example.css">
   <!-- Bootstrap CSS -->
  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta1/dist/css/bootstrap.min.css" rel="stylesheet"
    integrity="sha384-giJF6kkoqNQ00vy+HMDP7azOuL0xtbfIcaT9wjKHr8RbDVddVHyTfAAsrekwKmP1" crossorigin="anonymous">
  <title>list</title>
  
</head>

実際やってみるとintegrityの部分が意味不明であるのと原因不明の改行が発生したのでページネーションの部分のみを自前のCSSにコピーしている。

Bootstrap
BootstrapはWeb開発用のフロントエンドのフレームワークでその圧縮版がbootstrap.min.css。HTMLのヘッダーでリンクするとボタン、ナビゲーションバー、フォーム要素などのスタイリングが利用できる。

example.css
--省略--
.pagination{display:flex; justify-content:center;list-style:none}

.page-link{position:relative;display:block;color:#0d6efd; font-size:  medium; text-decoration:none;background-color:#fff;border:1px solid #dee2e6;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}
.page-item.active 
.page-link{z-index:3;color:#fff;background-color:#0d6efd;border-color:#0d6efd}
.page-item.disabled 
.page-link{color:#6c757d;pointer-events:none;background-color:#fff;border-color:#dee2e6}
.page-link{padding:.375rem .75rem}
--省略--
HTML
List.html
<!DOCTYPE html>
--省略--
<form class="form-center" id="form" method="post" th:object="${UserForm}" th:action="@{/search}">
	<input type="hidden" th:field=*{mapstring}>
	<input type="hidden" id="code" th:field=*{code}>
	<input type="hidden" id="page" th:field=*{page}>
	<input type="hidden" id="maxpage" th:field=*{maxpage}>

--省略--    
</form>
<nav><ul class="pagination" id="pagination"></ul></nav>
<footer class="footer">
    <p>© All rights reserved by PALM</p>
</footer>

<script th:inline="javascript">
window.onload =  function() {
	let title = /*[[${title}]]*/"title";
	let user = /*[[${session.user['fullname']}]]*/"user";
		
    let element_title = document.getElementById("title");	
	element_title.textContent = title;
    let element_user = document.getElementById("user");	
	element_user.textContent = "ユーザー:" + user;
	localStorage.clear();

   	// ページネーション
   	let pageElement = document.getElementById("page");
	let page = pageElement.value;
	const pages = 4;
	// 現在のページから4ページ括りの数(親ページ)を求める
    let sec = Math.floor((page-1)/pages) + 1;
   	let maxpageElement = document.getElementById("maxpage");
	let maxpage = maxpageElement.value;
    
    html = '';
    for (i = (sec-1) * pages + 1; i <= sec * pages && i <= maxpage; i++) {
     	if (i ==(sec-1) * pages + 1) {
			if (page > 1){ 
				// ページが1ページから始まらない場合は「<<を表示する
    			html += `<li class="page-item"><a class="page-link" href="javascript:void(0);" onclick="pageclick(${parseInt(page)-1})">&laquo;</a></li>`;
    		}
    		if (sec > 1){
				// 4ページずつ表示するセクションが1でない場合(5ページ目が先頭ページの場合)1ページ目へのリンクと「・・」を表示。
    			html += `<li class="page-item"><a class="page-link" href="javascript:void(0);" onclick="pageclick(${1})">1</a></li>`;
    			if (i != 2){
    				html += `<li class="page-item disabled"><a class="page-link" href="javascript:void(0);">・・</a></li>`;
 				}
 			}
 		}
 		if (i == page){
  			html += `<li class="page-item active"><a class="page-link" href="javascript:void(0);" onclick="pageclick(${i})">${i}</a></li>`;			 
		}else{
  			html += `<li class="page-item"><a class="page-link" href="javascript:void(0);" onclick="pageclick(${i})">${i}</a></li>`;
  		}
  		if (i == sec * pages || i == maxpage) {
			// 表示する右端  
			if (sec != Math.floor((maxpage-1)/pages) + 1){	
				// 4ページずつ表示するセクションが最終でない場合、最終ページへのリンクと「・・」を表示。
    			if (maxpage != pages+1){
    				html += `<li class="page-item disabled"><a class="page-link" href="javascript:void(0);">・・</a></li>`;
				}
    			html += `<li class="page-item"><a class="page-link" href="javascript:void(0);" onclick="pageclick(${maxpage})">${maxpage}</a></li>`;
			}
			if (page != maxpage){	  
				// ページが最終ページで終わらない場合は「>>」を表示する
				html += `<li class="page-item"><a class="page-link" href="javascript:void(0);" onclick="pageclick(${parseInt(page)+1})">&raquo;</a></li>`;
  			}
  		}
  	}
    //ナビゲーションバーの中身を設定する。
 	document.getElementById('pagination').innerHTML = html;	
}
--省略--
  • navタグ:他の文書へのナビゲーションリンクを提供するためのセクション
  • ulタグ:項目の順序なしリスト
  • liタグ:リストの項目
  • aタグ:href属性を用いてリンクを作成する

本サンプルではnavタグ内にulタグをid="pagination"で定義してwindow.onloadイベントでliタグ以下を動的に作成している。

<nav><ul class="pagination" id="pagination"></ul></nav>	

Javascriptで動的に作成たHTMLをulタグの中身に設定している。

<script th:inline="javascript">
window.onload =  function() {
    //ナビゲーションバーの中身を設定する。
 	document.getElementById('pagination').innerHTML = html;	
}
ページ番号クリック

ページ番号が変更された場合はページ番号を引数とするpageclickメソッドが起動する。
コントローラーのメソッドに渡す引数のフォームのpageプロパティにページ番号を設定して"/page"にマッピングされたpostメソッドが呼ばれる。

List.html
<script th:inline="javascript">
function pageclick(num) {
	// 検索項目を変更してからページ変更した場合は
	// 検索条件を元にもどしてから変更されたページのデータ検索を行う
    let form = document.getElementById("form");
	form.reset();
	
	let page = document.getElementById("page");
	page.value = num;
		
    form.action="/page";
    form.submit();
}  
コントローラー

ページ番号をクリックした場合は"/page"にマッピングされたpostメソッドが起動される。Postメソッドからリダイレクトで"/list"にマッピングされたGetメソッドが起動される。

UserController.java
@Controller
public class UserController {
	
	@GetMapping(value={"/list"})
    public String getList(@ModelAttribute("UserForm") UserForm form, Model model, HttpSession session) {
    	model.addAttribute("title", "一覧"); 
        setMapItems(form);
        setSearchKey(form, session);
        // 職員データの取得
        Map<String, List<Map<String, Object>>> data = new HashMap<>();
        Integer count = userService.count(form);
        form.setCount(count);
        form.setMaxpage((int) (Math.floor((count - 1) / 10) + 1));
        if (form.getPage() == 0) form.setPage(1);
        data.put("users", userService.select(form));
        model.addAttribute("data", data);
        model.addAttribute("UserForm", form);
        return "list";
    }
    
    @PostMapping(value={"/page"})
    public String postPage(UserForm form, BindingResult result, RedirectAttributes redirectAttributes, HttpSession session){
    	redirectAttributes.addFlashAttribute("UserForm", form); 
		session.setAttribute("StateKeys", form.getStatekeys()); 
		session.setAttribute("StatusKey", form.getStatusKey()); 
        return "redirect:/list";
    }
}
public String getList()メソッドの処理

引数であるformにcount(データ件数),page(現在のページ),maxpage(最大ページ)が設定されているがここでもデータ件数と最大ページを再設定している。
以下のcount()メソッドでformの検索条件を渡してデータ件数を取得してその値をformのデータ件数のプロパティに設定している。データ件数から最大ページを求めてformの最大ページのプロパティに設定している。
(10はページ当たりの行数)

Integer count = userService.count(form);
form.setCount(count);
form.setMaxpage((int) (Math.floor((count - 1) / 10) + 1));
サービス

以下のメソッドで一覧データを取得する。

public List<Map<String, Object>> select(UserForm form) {

以下でnoが指定ページの先頭行から+10行のデータを取得している。

WHERE no BETWEEN " + Integer.toString((form.getPage()-1)*10+1) + " AND " + Integer.toString(form.getPage()*10);

「no」はSELECTで定義されている。

String query = 
"SELECT"+
" ROW_NUMBER() OVER(ORDER BY a.code) no"+ 

public Integer count()メソッドでデータ件数を取得している。

public Integer count(UserForm form) {
	--省略--
    int tmp = (Integer) jdbcTemplate.queryForObject(query, Integer.class);
	form.setCount(tmp);
UserService.java
@Service
public class UserService {
	
	@Autowired
	private JdbcTemplate jdbcTemplate;
	
	public List<Map<String, Object>> select(UserForm form) {
		String query = 
		"SELECT"+
		" ROW_NUMBER() OVER(ORDER BY a.code) no"+ 
		",a.code"+
		",a.name"+
		",a.kana"+
		",b.name statename"+
		",c.name statusname"+
		",a.divisionname"+
		",a.remarks"+
		",d.image"+
		" FROM members a"+
		" LEFT JOIN name b"+
		" ON a.statecode=b.code AND b.category=2"+
		" LEFT JOIN name c"+
		" ON a.statuscode=c.code AND c.category=1"+
 		" LEFT JOIN image d"+
		" ON a.code=d.code"+
		" WHERE a.name IS NOT null";
		
		if (form.getSearchname() != null && form.getSearchname() != "") {
			query += " AND a.name LIKE '" + form.getSearchname() + "%'";
		}
		if (form.getSearchkana() != null && form.getSearchkana() != "") {
			query += " AND a.kana LIKE '" + form.getSearchkana() + "%'";
		}
		
		if (form.getStatekeys().size() != 0) {
			//状態 通常 休職 退職
			query += " AND (";
			for (int i = 0; i < form.getStatekeys().size(); i++) {
				if (i == 0) {
					query += " a.statecode = '" + form.getStatekeys().get(i) + "'";
				}else{
					query += " OR a.statecode = '" + form.getStatekeys().get(i) + "'";				
				}
			}
			query += ")";
		}

		if (form.getStatusKey() != 0) {
			query += " AND (a.statuscode = '" + form.getStatusKey() + "')";			
		}
		query += " ORDER BY code";		
		query = "SELECT no,code,name,kana,statename,statusname,divisionname,remarks,image" +
		" FROM (" + query +") WHERE no BETWEEN " + Integer.toString((form.getPage()-1)*10+1) + " AND " + Integer.toString(form.getPage()*10);

		List<Map<String, Object>> tmp = jdbcTemplate.queryForList(query);
		for (int i=0; i<tmp.size(); i++) {
			if (tmp.get(i).get("image") != null) {
				StringBuffer encoded = new StringBuffer();
				encoded.append("data:"+ form.getContenttype()+ ";base64,");
				encoded.append(Base64.getEncoder().encodeToString((byte[])tmp.get(i).get("image")));
				tmp.get(i).put("imgString",encoded.toString());
			}else {
				tmp.get(i).put("imgString",null);
			}
		}
		return tmp;
	}
	
	public Integer count(UserForm form) {
		String query = 
		"SELECT count(*)"+
		" FROM members a"+
		" LEFT JOIN name b"+
		" ON a.statecode=b.code AND b.category=2"+
		" LEFT JOIN name c"+
		" ON a.statuscode=c.code AND c.category=1"+
		" WHERE a.name IS NOT null";
		
		if (form.getSearchname() != null && form.getSearchname() != "") {
			query += " AND a.name LIKE '" + form.getSearchname() + "%'";
		}
		if (form.getSearchkana() != null && form.getSearchkana() != "") {
			query += " AND a.kana LIKE '" + form.getSearchkana() + "%'";
		}
		
		if (form.getStatekeys().size() != 0) {
			//状態 通常 休職 退職
			query += " AND (";
			for (int i = 0; i < form.getStatekeys().size(); i++) {
				if (i == 0) {
					query += " a.statecode = '" + form.getStatekeys().get(i) + "'";
				}else{
					query += " OR a.statecode = '" + form.getStatekeys().get(i) + "'";				
				}
			}
			query += ")";
		}

		if (form.getStatusKey() != 0) {
			query += " AND (a.statuscode = '" + form.getStatusKey() + "')";			
		}
		int tmp = (Integer) jdbcTemplate.queryForObject(query, Integer.class);
		return tmp;
	}
}

おわりに

ネットの記事を探りながらページネーションの機能を実装してみた。本記事に掲載している画像処理については回を改めて投稿したい。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?