検索画面で、検索条件を指定するが、検索結果が多くて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
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で動的クエリに投稿したものと同じなので割愛する。
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画面に検索実行用とページングをするための2つのformとすることで、それぞれのURLで処理できるようにしています。