SpringBoot + Thymeleafでページングを実現する

## 概要

- SpringBoot+Thymeleafで一覧画面をページングする
- 完成時の画面イメージ  
<img width="250" alt="image1.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/396521/e0190bd0-0f2b-616d-e059-8c850c0cfbf2.png">
<img width="250" alt="image2.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/396521/7dfd8900-ac49-561e-86d5-0c4629b85029.png">
<img width="250" alt="image3.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/396521/fd538856-90e7-a5dc-2df9-7dbb930fea0b.png">

## 検証環境

- Spring Boot : 2.2.5
- H2 Database

## 実装

### Entity

```Player.java
@Entity
@Getter
@Setter
public class Player {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;
    private String name;
    private Integer number;
    private String position;
}
```

### Repository

```PlayerRepository.java
@Repository
public interface PlayerRepository extends JpaRepository<Player, Integer> {

    public Page<Player> findAll(Pageable pageable);
}
```

- 今回はJPAを使用している
- プレゼンテーションレイヤで扱いやすいようPage型で返却する  
https://docs.spring.io/spring-data/commons/docs/current/api/org/springframework/data/domain/Page.html

### Service

```PlayerService.java
@Service
public class PlayerService {

    @Autowired
    private PlayerRepository playerRepository;

    public Page<Player> getPlayers(Pageable pageable) {
        return playerRepository.findAll(pageable);
    }
}
```

### Controller

```PlayerController.java
@Controller
@RequestMapping("/pages/players")
public class PlayerController {

    @Autowired
    private PlayerService playerService;

    @GetMapping
    public String index(Pageable pageable, Model model) {

        Page<Player> playerPage = playerService.getPlayers(pageable);

        model.addAttribute("page", playerPage);
        model.addAttribute("players", playerPage.getContent());

        return "pages/index";
    }
}
```

### Configuration

```WebConfig.java
@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {

        PageableHandlerMethodArgumentResolver resolver = new PageableHandlerMethodArgumentResolver();
        // 1ページに表示する最大件数(10件)を設定する
        resolver.setMaxPageSize(10);
        argumentResolvers.add(resolver);
    }
}
```

- 1ページの最大件数設定のところは以下と同様となる
  - `resolver.setFallbackPageable(PageRequest.of(0, 10));`  
※PageRequest.of(ページ番号, ページサイズ)

### View(Thymeleaf)

```index.html
<!-- body部 -->
<h1>INTER PLAYER</h1>
<table border="1" bordercolor="navy">
  <thead>
    <tr>
      <th>NAME</th>
      <th>NUMBER</th>
      <th>POSITION</th>
    </tr>
  </thead>
  <tbody>
    <tr th:each="player : ${players}">
      <td th:text="${player.name}"></td>
      <td th:text="${player.number}"></td>
      <td th:text="${player.position}"></td>
   </tr>
  </tbody>
</table>
<!-- 以降がページネーション部分 -->
<div>
  <ul>
    <li style="display:inline;">
      <span th:if="${page.first}">&lt;&lt;前</span>
      <a
        th:if="${!page.first}"
        th:href="@{/pages/players(page = ${page.number} - 1)}"
      >
        &lt;&lt;前
      </a>
    </li>
    <li
      th:each="i : ${#numbers.sequence(0, page.totalPages - 1)}"
      style="display:inline; margin-left:10px;"
    >
      <span th:if="${i} == ${page.number}" th:text="${i + 1}">1</span>
      <a
        th:if="${i} != ${page.number}"
        th:href="@{/pages/players(page = ${i})}"
      >
        <span th:text="${i+1}">1</span>
      </a>
    </li>
    <li style="display:inline; margin-left:10px;">
      <span th:if="${page.last}">次&gt;&gt;</span>
      <a
        th:if="${!page.last}"
        th:href="@{/pages/players(page = (${page.number} + 1))}"
      >
        次&gt;&gt;
      </a>
    </li>
  </ul>
</div>
```

### `Interface Page<T>`

- https://docs.spring.io/spring-data/commons/docs/current/api/org/springframework/data/domain/Page.html

|メソッド|詳細|
|---|---|
|getContent()|ページコンテンツをListで返却する|
|getNumber()|何ページ目であるか返却する|
|getSize()|ページサイズを返却する|
|getTotalPages()|総ページ数を返却する|
|isFirst()|最初のページであるか真偽値を返却する|
|isLast()|最後のページであるか真偽値を返却する|

## 最後に

- Pageableを渡してあげることで簡単に実現できた
- 全量取得してきてクライアント側でごにょごにょするのかと思ってたけど、今回検証したのは都度DBアクセスする方式だった
- ソート要件を追加すると変わってくるので、後に検証してみる

## 参照

- https://github.com/mtiger2k/pageableSpringBootDataJPA
- https://www.baeldung.com/spring-data-jpa-pagination-sorting

***

## 2020/3/14 追記

- 1ページの最大件数設定は、以下の方が良いかも
 - WebConfig.java は削除する
 - HandlerMethodの引数Pageableに`@PageableDefault`を付与する  
 https://docs.spring.io/spring-data/data-commons/docs/current/api/org/springframework/data/web/PageableDefault.html

```PlayerController.java
@Controller
@RequestMapping("/pages/players")
public class PlayerController {

    @Autowired
    private PlayerService playerService;

    @GetMapping
    public String index(@PageableDefault(page = 0, size = 10) Pageable pageable, Model model) {

        Page<Player> playerPage = playerService.getPlayers(pageable);

        model.addAttribute("page", playerPage);
        model.addAttribute("players", playerPage.getContent());

        return "pages/index";
    }
}
```