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

  • 16
    いいね
  • 0
    コメント

Spring Boot + Thymeleafで画面のページング機能を簡単に実装できます。
完成後の画面イメージ
スクリーンショット 2017-01-21 23.25.18.png

検証環境

  • Spring Boot 1.4.3
  • PostgreSQL 9.3.15

実装

まずは、Domainレイヤです。

@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クラスを使用します。

@Repository
public interface WordRepository extends CrudRepository<Word, Long>{

    public Page<Word> findAll(Pageable pageable);

}

次は、Serviceレイヤです。

@Service
public class WordService {

    @Autowired
    private WordRepository wordRepo;

    public Page<Word> getAllWord(Pageable pageable) {

        return wordRepo.findAll(pageable);
    }

}

Presentationレイヤです。

@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";
    }
}

表示件数の設定

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

@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側の実装です。

<!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="@{'/word/wordList'(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-01-21 23.52.24.png

スクリーンショット 2017-01-21 23.52.39.png

ラッパークラスの実装

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の代わりにラッパークラスを使います。

@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

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

    ...

        <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