Spring Boot + Thymeleafでページング機能を実装する

Spring Boot + Thymeleafで画面のページング機能を簡単に実装できます。
完成後の画面イメージ
<img width="1355" alt="スクリーンショット 2017-01-21 23.25.18.png" src="https://qiita-image-store.s3.amazonaws.com/0/123138/b43099a2-fe83-c5b7-5144-5d19d99713ae.png">


##検証環境
- Spring Boot 1.4.3
- PostgreSQL 9.3.15

##実装
###まずは、Domainレイヤです。

```java
@Entity
@Table(name="word_info")
public class Word implements Serializable {

	private static final long serialVersionUID = -870708489937857961L;
	
	@Id
	@GeneratedValue(strategy=GenerationType.TABLE, generator="seqTable")
	@TableGenerator(name="seqTable", table="seq_table", pkColumnName="seq_name", pkColumnValue="word_seq", valueColumnName="seq_value")
	@Column(name="id")
	private Long id;
	
	@Column(name="word")
	private String word;
	
	@Column(name="meaning")
	private String meaning;
	
	@Column(name="example")
	private String example;

	//get・set省略
	...
}
```

一覧を取得するために、JPAのリポジトリクラスを定義する。Listの代わりにPageクラスを使用します。

```java
@Repository
public interface WordRepository extends CrudRepository<Word, Long>{
	
	public Page<Word> findAll(Pageable pageable);

}
```

###次は、Serviceレイヤです。

```java
@Service
public class WordService {
	
	@Autowired
	private WordRepository wordRepo;
	
	public Page<Word> getAllWord(Pageable pageable) {

		return wordRepo.findAll(pageable);
	}

}
```

###Presentationレイヤです。


```java
@Controller
public class MainController {
	
	@Autowired
	private WordService wordService;
	
	@RequestMapping(value="/word/wordList", method=RequestMethod.GET)
	public String getWordList(Model model, Pageable pageable) {
		Page<Word> wordsPage = wordService.getAllWord(pageable);
		model.addAttribute("page", wordsPage);
		model.addAttribute("words", wordsPage.getContent());
		model.addAttribute("url", "/word/wordList");
		
		return "/word/wordList";
	}
}
```

###表示件数の設定
ページ単位に最大の表示件数を設定します。

```java
@Configuration
public class WebConfig extends WebMvcConfigurerAdapter {

  @Override
  public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
      PageableHandlerMethodArgumentResolver resolver = new PageableHandlerMethodArgumentResolver();
      //ページ単位に表示する件数
      resolver.setFallbackPageable(new PageRequest(0, 5));
      argumentResolvers.add(resolver);
      super.addArgumentResolvers(argumentResolvers);
  }

}
```

###最後にThymeleaf側の実装です。

```html
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
	  xmlns:th="http://www.thymeleaf.org">
	<head>
		<meta charset="UTF-8" />
		<link th:substituteby="common/header :: common_header"/>
		<title>Word List</title>
	</head>
	<body>
		<table border="1">
			<tr>
				<th>ID</th>
				<th>WORD</th>
				<th>MEANING</th>
				<th>EXAMPLE</th>
			</tr>
			<tr th:each="word:${words}">
				<td th:text="${word.id}"></td>
				<td th:text="${word.word}"></td>
				<td th:text="${word.meaning}"></td>
				<td th:text="${word.example}"></td>
			</tr>
		</table>
		
		<div th:fragment='paginationbar'>
			<ul>
				<li th:class="${page.first} ? 'disabled':''" style="display:inline">
					<span th:if="${page.first}">←先頭</span>
					<a th:if="${not page.first}" th:href="@{${url}(page=0)}">←先頭</a>
				</li>
				<li th:each='i : ${#numbers.sequence(0, page.totalPages-1)}' th:class="(${i}==${page.number})? 'active' : ''" style="display:inline">
                	<span th:if='${i}==${page.number}' th:text='${i+1}'>1</span>
               	 	<a th:if='${i}!=${page.number}' th:href="@{${url}(page=${i})}">
               	 		<span th:text='${i+1}'>1</span>
               	 	</a>
                </li>
				<li th:class="${page.last} ? 'disabled':''" style="display:inline">
					<span th:if="${page.last}">末尾➝</span>
					<a th:if="${not page.last}" th:href="@{${url}(page=(${page.totalPages}-1))}">末尾➝</a>
				</li>
			</ul>
		</div>
	</body>
</html>
```

##カスタマイズ

以下のようにページ数が多いケースには、一部のページ番号だけを表示したい場合、Pageのラッパークラスを作成し、カスタマイズを行うことが可能です。
![2017-12-08_092643.jpg](https://qiita-image-store.s3.amazonaws.com/0/123138/76a9f237-8ab5-c35c-b077-f79d876873e5.jpeg)


###ラッパークラスの実装

```java
public class PageWrapper<T> {
    public static final int MAX_PAGE_ITEM_DISPLAY = 5;
    private Page<T> page;
    private List<PageItem> items;
    private int currentNumber;
    private String url;

    public String getUrl() {
        return url;
    }

    public void setUrl(String url) {
        this.url = url;
    }

    public PageWrapper(Page<T> page, String url){
        this.page = page;
        this.url = url;
        items = new ArrayList<PageItem>();

        currentNumber = page.getNumber() + 1;

        int start, size;
        if (page.getTotalPages() <= MAX_PAGE_ITEM_DISPLAY){
            start = 1;
            size = page.getTotalPages();
        } else {
            if (currentNumber <= MAX_PAGE_ITEM_DISPLAY - MAX_PAGE_ITEM_DISPLAY/2){
                start = 1;
                size = MAX_PAGE_ITEM_DISPLAY;
            } else if (currentNumber >= page.getTotalPages() - MAX_PAGE_ITEM_DISPLAY/2){
                start = page.getTotalPages() - MAX_PAGE_ITEM_DISPLAY + 1;
                size = MAX_PAGE_ITEM_DISPLAY;
            } else {
                start = currentNumber - MAX_PAGE_ITEM_DISPLAY/2;
                size = MAX_PAGE_ITEM_DISPLAY;
            }
        }

        for (int i = 0; i<size; i++){
            items.add(new PageItem(start+i, (start+i)==currentNumber));
        }
    }

    public List<PageItem> getItems(){
        return items;
    }

    public int getNumber(){
        return currentNumber;
    }

    public List<T> getContent(){
        return page.getContent();
    }

    public int getSize(){
        return page.getSize();
    }

    public int getTotalPages(){
        return page.getTotalPages();
    }

    public boolean isFirstPage(){
        return page.isFirst();
    }

    public boolean isLastPage(){
        return page.isLast();
    }

    public boolean isHasPreviousPage(){
        return page.hasPrevious();
    }

    public boolean isHasNextPage(){
        return page.hasNext();
    }

    public class PageItem {
        private int number;
        private boolean current;
        public PageItem(int number, boolean current){
            this.number = number;
            this.current = current;
        }

        public int getNumber(){
            return this.number;
        }

        public boolean isCurrent(){
            return this.current;
        }
    }
}
```

###コントロールクラスの変更
Pageの代わりにラッパークラスを使います。

```java
@Controller
public class MainController {
	
	@Autowired
	private WordService wordService;

	@RequestMapping("/word/register")
	public String wordRegister(WordForm wordForm) {
		wordService.addWord(wordForm);
		return "/word/wordRegister";
	}
	
	@RequestMapping(value="/word/wordList", method=RequestMethod.GET)
	public String getWordList(Model model, Pageable pageable) {
		Page<Word> wordPage = wordService.getAllWord(pageable);
		PageWrapper<Word> page = new PageWrapper<Word>(wordPage, "/word/wordList");
		model.addAttribute("page", page);
		model.addAttribute("words", page.getContent());
		
		return "/word/wordList";
	}
}
```

###Thymeleaf

ページングの部分は以下のように変更します。

```html
	...

		<div th:fragment='paginationbar'>
			<ul class='pagination pagination-centered'>
				<li th:class="${page.firstPage}?'disabled':''" style="display:inline">
					<span th:if='${page.firstPage}'>←先頭</span>
					<a th:if='${not page.firstPage}' th:href='@{${page.url}(page=0,size=${page.size})}'>←先頭</a>
				</li>
				<li th:class="${page.hasPreviousPage}? '' : 'disabled'" style="display:inline">
					<span th:if='${not page.hasPreviousPage}'>«</span>
					<a th:if='${page.hasPreviousPage}' th:href='@{${page.url}(page=${page.number-2},size=${page.size})}'>«</a>
				</li>
	                
	                
				<li th:each='item : ${page.items}' th:class="${item.current}? 'active' : ''" style="display:inline">
					<span th:if='${item.current}' th:text='${item.number}'>1</span>
					<a th:if='${not item.current}' th:href='@{${page.url}(page=${item.number-1},size=${page.size})}'>
					<span th:text='${item.number}'>1</span>
					</a>
				</li>
				<li th:class="${page.hasNextPage}? '' : 'disabled'" style="display:inline">
					<span th:if='${not page.hasNextPage}'>»</span>
					<a th:if='${page.hasNextPage}' th:href='@{${page.url}(page=${page.number},size=${page.size})}'>»</a>
				</li>
				<li th:class="${page.lastPage}? 'disabled' : ''" style="display:inline">
					<span th:if='${page.lastPage}'>末尾➝</span>
					<a th:if='${not page.lastPage}' th:href='@{${page.url}(page=${page.totalPages - 1},size=${page.size})}'>末尾➝</a>
				</li>
			</ul>
		</div>
	...
```

参照：https://github.com/mtiger2k/pageableSpringBootDataJPA