【Train with Spring】5. ページネーション

# 初めに
この記事は主に同期に対してSpringを紹介するために作成されました。
なるべく楽をしながらSpringと仲良くなるのが目的ですので、網羅的な説明は極力避け、目的を達成する手段が幾つかある中から、その内の1つを学ぶ形式で進みます。
また、記事中には正確性に欠ける表現が出現しますが、これも上記と同じ理由です。

## 前回までのあらすじ
- [【Train with Spring】1. プロジェクトのセットアップ](https://qiita.com/kthksgy/items/c8ac96ffe85b38feaa16)
- [【Train with Spring】2. 画面遷移とパラメータの受け取り](https://qiita.com/kthksgy/items/364dfb0e703aede2d18e)
- [【Train with Spring】3. データベースアクセス1](https://qiita.com/kthksgy/items/ad7a62dc7d510d907fdf)
- [【Train with Spring】4. データベースアクセス2](https://qiita.com/kthksgy/items/52599fc420dc2cfe7b5f)

前回はSpring Data JPAでのリレーションの実装について学びました。

前回がかなり長めだったので、今回はページネーションについて簡単に解説します。

# ページネーション
ページネーションとは、データをページ単位で分割する処理の事です。

QiitaでもまとめブログでもGoogle検索でも何でも良いですが、そのようなサイトではデータの一覧を表示する際に、全てのデータを一度に表示せず、ページ単位で表示するようになっています。
例えば、Googleでページネーションと調べた場合は一千万件超えのページがヒットしますが、検索結果に一度に表示されるのはせいぜい10件程度です。

今回は、このページ分割処理をSpringでどう実装するのかを解説します。

## 仕組み
ページネーションでは、ページ番号(Springでは0始まり)とページサイズの2つのパラメータを基に取得するデータを選択します。
例えば、時系列順に並んでいる100個(0--99)のデータに対してページサイズが10でページ番号が2という条件でデータを取得すると、取得されるのは20番目から29番目のデータです。

# Springにおけるページネーション対応
では、Springでページネーションを実装していきます。

といっても、やる事は簡単です。

## リポジトリ
リポジトリでは、データを取得するメソッドの引数に`Pageable`型の引数を追加して、戻り値の型を`List<>`から`Page<>`に変更するだけです。

```java
package com.example.trainwithspring.repositories;

import java.util.List;
import java.util.Optional;

import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.repository.CrudRepository;

import com.example.trainwithspring.entities.User;

public interface UserRepository extends CrudRepository<User, Long> {
	Optional<User> findByName(String name);
	List<User> findByNameContaining(String name);
	// 追加
	Page<User> findByNameContaining(String name, Pageable pageable);
}
```

これで、名前での中間一致検索がページネーションに対応しました。

### ソートしながらページング
一般的に、ページングはソートと一緒に用いられます。

例えば、名前での中間一致検索をしながら、ユーザーの登録日時順でページングをしたい場合は以下のように`OrderBy〇〇Desc`を付けます。

```java
Page<User> findByNameContainingOrderByCreatedAtDesc(String name, Pageable pageable);
```

## コントローラー
コントローラーでも、マッピングされたメソッドの引数に`Pageable`型の引数を追加するだけです。

```java
package com.example.trainwithspring.controllers;

import java.util.Optional;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;

import com.example.trainwithspring.entities.User;
import com.example.trainwithspring.repositories.UserRepository;

@Controller
public class UserController {
	@Autowired // このフィールドはSpringで勝手に用意してくれという意味のおまじない
	UserRepository userRepository;
	
	/**
	 * ユーザーの名前での中間一致検索。
	 * @param name 検索する名前
	 * @param model Thymeleafに渡すデータ
	 * @param pageable ページ情報
	 * @return Thymeleafのテンプレート名
	 */
	@GetMapping("/users")
	public String users(
			@RequestParam(name = "name", required = true) String name,
			Model model,
			// Pageable型の引数を追加
			Pageable pageable) {
		// さっき定義したメソッドでユーザーを取得する
		Page<User> page = userRepository.findByNameContaining(name, pageable);
		List<User> users = page.getContent();
		// Thymeleafで前ページ／次ページへのリンクを作れるようにページをそのまま渡す
		model.addAttribute(page);
		// 利便性のためにusersからユーザー情報を参照できるようにユーザーリストも渡す
		model.addAttribute(users);
		return "user-list";
	}
}
```

これで完成です。

### `Pageable`
Pageableは、ページ番号とページサイズを保持するためのクラスです。

コントローラーのメソッドの引数に指定すると、リクエストに存在する`page`パラメータと`size`パラメータから自動的にページ情報を取得して、`pageable`に代入します。
例えばユーザー一覧の2ページ目(ページサイズ10)を見るには`http://localhost:8080/users?page=2&size=10`にアクセスをします。

もっと素朴な書き方をするとこんな感じです。このように、リクエストパラメータから`Pageable`インスタンスを作成する部分を、引数に設定すると自動化してくれます。

```java
@GetMapping("/users")
public String users(
        @RequestParam(name = "name", required = true) String name,
        Model model,
        @RequestParam(name = "page", required = false, defaultValue = "0") Integer page,
        @RequestParam(name = "page", required = false, defaultValue = "10") Integer size) {
    // リクエストパラメータを基にPageableを作成
    Pageable pageable = Pageable.ofSize(size).withPage(page);
    // さっき定義したメソッドでユーザーを取得する
    Page<User> userPage = userRepository.findByNameContaining(name, pageable);
    List<User> users = userPage.getContent();
    // Thymeleafで前ページ／次ページへのリンクを作れるようにページをそのまま渡す
    model.addAttribute("page", userPage);
    // 利便性のためにusersからユーザー情報を参照できるようにユーザーリストも渡す
    model.addAttribute(users);
    return "user-list";
}
```

## Thymeleaf
Thymeleafでは、コントローラーから受け取ったページ情報を基に、前ページや次ページへのリンクを作成します。

例えば、`http://localhost:8080/users?page=2&size=10`にアクセスした場合

- 次ページへのリンクは`http://localhost:8080/users?page=3&size=10`
- 前ページへのリンクは`http://localhost:8080/users?page=1&size=10`
- 一度に20項目表示するリンクは`http://localhost:8080/users?page=2&size=20`

のように、それぞれのリンクを動的に生成します。

`Page`オブジェクトにはページ番号、ページサイズの他に、全データ数等の追加情報も含まれているため、それらを駆使すれば「最後のページへ移動」のようなページ移動も可能になります。

# まとめ
今回はページネーションについて解説しました。

次回は未定です。書くべき事が新たに生まれたら解説します。