Springbootで検索画面の検索条件を保持しつつ、ページネーションを行う

検索画面で、検索条件を指定するが、検索結果が多くて1ページに表示できないケースでは、検索結果に対してページネーションをしたいケースがあるので、それを実装してみます。

###1.環境
eclipse 4.7.2
java 8
database postgreSQL10
build.gradle

```
	compile('org.springframework.boot:spring-boot-starter-data-jpa')
	compile('org.springframework.boot:spring-boot-starter-thymeleaf')
	compile('org.springframework.boot:spring-boot-starter-web')
	runtime('org.springframework.boot:spring-boot-devtools')
	runtime('org.postgresql:postgresql')
	compileOnly('org.projectlombok:lombok')
	providedRuntime('org.springframework.boot:spring-boot-starter-tomcat')
	testCompile('org.springframework.boot:spring-boot-starter-test')
```

###2.ページネーション

他にもやり方はあると思いますが、Pageクラスと同じプロパティをもつBeanを定義してThymeleafに渡すようにしました。

(1)Bean

```Pagination
import lombok.Data;

@Data
public class Pagination {
	private boolean first;
	private boolean last;
	private int number;
	private int numberOfElements;
	private int size;
	private long totalElements;
	private int totalPages;
}
```

(2)Helper

Paginationオブジェクトを生成するクラスです。

```PagenationHelper.java
import org.springframework.data.domain.Page;

public class PagenationHelper {
	public static Pagination createPagenation(Page pageObject) {
		Pagination pagination = new Pagination();
		pagination.setFirst(pageObject.isFirst());
		pagination.setLast(pageObject.isLast());
		pagination.setNumber(pageObject.getNumber());
		pagination.setNumberOfElements(pageObject.getNumberOfElements());
		pagination.setSize(pageObject.getSize());
		pagination.setTotalElements(pageObject.getTotalElements());
		pagination.setTotalPages(pageObject.getTotalPages());
		pagination.setNumberOfElements(pageObject.getNumberOfElements());
		return pagination;
	}
}
```

###3.Entity
[SpringbootのSpecificationで動的クエリ](https://qiita.com/t-iguchi/items/e8e037e6ce85962ea75e)に投稿したものと同じなので割愛する。

###4.Repository

Pageableを使って、Pageを返します。

```
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;

public interface EmployeeRepository extends JpaRepository<Employee, String>, JpaSpecificationExecutor<Employee> {
	public Page<Employee> findAll(Specification<Employee> and, Pageable pageable);
}
```

###5.Service

Usernameでソートし5行毎にページングする設定にしてます。

```EmployeeService.java
import static com.stone.sample.employee.EmployeeSpecifications.*;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@Transactional
public class EmployeeService {

	private static final int PAGE_SIZE=5;

	@Autowired
	EmployeeRepository repository;
	public Page<Employee> search(int page, List<String> username, String empname) {
	    return repository.findAll(Specification
	        .where(usernameIn(username))
	        .and(empnameContains(empname))
	        ,PageRequest.of(page<=0?0:page, PAGE_SIZE, new Sort(
	                Sort.Direction.ASC, "username"))
	    );
	}
}
```

###6.Specification

```EmployeeSpecifications.java
import java.util.List;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.util.StringUtils;

public class EmployeeSpecifications {
    public static Specification<Employee> empnameContains(final String empname) {
        return StringUtils.isEmpty(empname) ? null : new Specification<Employee>() {
            @Override
            public Predicate toPredicate(Root<Employee> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
                return cb.like(root.get("empname"), "%" + empname + "%");
            }
        };
    }
    public static Specification<Employee> usernameIn(final List<String> usernames) {
        return usernames==null||usernames.size()==0 ? null : new Specification<Employee>() {
            @Override
            public Predicate toPredicate(Root<Employee> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
                return cb.in(root.get("username")).value(usernames);
            }
        };
    }
}
```


###7.Controller

```SpecificationController.java

import java.util.ArrayList;
import java.util.List;

import javax.servlet.http.HttpSession;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.SessionAttributes;
import org.springframework.web.servlet.ModelAndView;

import com.stone.sample.common.PagenationHelper;
import com.stone.sample.employee.Employee;
import com.stone.sample.employee.EmployeeService;
@Controller
@SessionAttributes(value = "form")
public class SpecificationController {

	@Autowired
	EmployeeService employeeService;

	@Autowired
	HttpSession session;

	private static final String SESSION_FORM_ID="searchForm";

	@RequestMapping(value="/", method = RequestMethod.GET)
	public ModelAndView get(Model model) {
		return search(new SearchForm(), model);
	}
	@RequestMapping(value="/pagenate", method = RequestMethod.GET)
	public ModelAndView pagenate(
			@RequestParam("page") int page,
			Model model) {
		SearchForm form = (SearchForm)session.getAttribute(SESSION_FORM_ID);
		form.setPage(page);
		return search(form, model);
	}

	@RequestMapping(value="/search", method = RequestMethod.POST)
	public ModelAndView search(
		  @ModelAttribute SearchForm form,
		  Model model) {
		session.setAttribute(SESSION_FORM_ID, form);
		List<String> usernames = new ArrayList<String>();
		if (!form.getUsername1().isEmpty()) usernames.add(form.getUsername1());
		if (!form.getUsername2().isEmpty()) usernames.add(form.getUsername2());
		if (!form.getUsername3().isEmpty()) usernames.add(form.getUsername3());
		Page<Employee> emps = employeeService.search(form.getPage(), usernames, form.getEmpname());
		ModelAndView mv=new ModelAndView();
		mv.setViewName("index");
		mv.addObject("form",form);
		mv.addObject("emplist",emps);
		mv.addObject("page",PagenationHelper.createPagenation(emps));
		return mv;
	}
}
```
"page"に、Paginationオブジェクトを渡します。
mv.addObject("page",PagenationHelper.createPagenation(emps));

###8.form

```SearchForm.java
import lombok.Data;
@Data
public class SearchForm {
	private int page;
	private String username1="";
	private String username2="";
	private String username3="";
	private String empname="";
}
```

###9.index.html

```
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
	<head>
		<title>Springboot</title>
		<meta charset="utf-8" />
		<style>
		  .pagination{
		    padding-left:0px;
		  }
			.pagination:after {
			  content: "";
			  display: block;
			  clear: both;
			}
			.page-item {
			  display: inline;
			  min-width: 30px;
			  text-align:center;
			  border:1px solid #EEEEEE;
			  float: left;
			}
		</style>
	</head>
	<body>
	  <h3>Specifinationで動的検索</h3>
	  <form th:action="@{/search}" th:object="${form}" method="post">
	    <div>
	    <label>ユーザID</label>
	    <input type="text" th:field="*{username1}" />
	    <input type="text" th:field="*{username2}" />
	    <input type="text" th:field="*{username3}" />
	    </div>
	    <div>
	    <label>ユーザ名</label>
	    <input type="text" th:field="*{empname}" />
	    </div>
	    <input type="submit" value="検索"/>
	  </form>
    <table border="1">
        <tr>
        <th>username</th>
        <th>empname</th>
        </tr>
        <tr th:each="emp : ${emplist}">
            <td th:text="${emp.username}"></td>
            <td th:text="${emp.empname}"></td>
        </tr>
    </table>
    <form th:action="@{/pagenate}" th:object="${page}" method="get">
    <div class="float-right">
      <div>
        <span th:text="${page.number+1}"></span>
	<span>/</span>
	<span th:text="${page.totalPages}"></span>
	<span>頁を表示</span>
      </div>
      <div th:fragment='paginationbar'>
        <ul>
          <li class="page-item">
              <span th:if="${page.first}">←先頭</span>
            <a th:if="${not page.first}" th:href="@{${url}(page=0)}">←先頭</a>
          </li>
          <li class="page-item" th:each='i : ${#numbers.sequence(0, page.totalPages-1)}'>
            <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 class="page-item">
            <span th:if="${page.last}">末尾➝</span>
            <a th:if="${not page.last}" th:href="@{${url}(page=(${page.totalPages}-1))}">末尾➝</a>           </li>
        </ul>
       </div>
     </div>
   </form>
  </body>
</html>
```

1画面に検索実行用とページングをするための２つのformとすることで、それぞれのURLで処理できるようにしています。

###10.確認

![image.png](https://qiita-image-store.s3.amazonaws.com/0/246741/55ea2d52-0018-3d0c-13b5-28ae3abd6fe0.png)