Spring Data JPAで動的にクエリを生成する（複数ワード検索）（ページング対応）

JPAでGoogle検索みたいにスペース区切りで複数ワードのAND検索がしたい。
「転職　東京」みたいな。
さらにページングにも対応したい。

色々調べていたらSpecificationに辿り着いたので、実装方法を備忘録として残しておきます。
いくつか粗があるので参考程度に。

# Entity

```java
@Data
@Entity
@Table(name="account")
public class AccountEntity implements Serializable{

	/**
	 * シリアルバージョンUID
	 */
	private static final long serialVersionUID = 1L;

	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	@Column(name="id")
	private Integer id;

	@Column(name="name")
	private String name;

	@Column(name="age")
	private Integer age;
}
```

# Repository
JpaSpecificationExecutorを継承します。

```java
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.stereotype.Repository;

@Repository
public interface AccountRepository extends JpaRepository<AccountEntity, Integer>, JpaSpecificationExecutor<AccountEntity> {
	Page<AccountEntity> findAll(Pageable pageable);
}
```

# Specification

```java
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.stereotype.Component;


@Component
public class AccountSpecification {
    /**
     * 指定文字をユーザー名に含むアカウントを検索する。
     */
    public Specification<AccountEntity> nameLike(String name) {

   	// 匿名クラス
        return new Specification<AccountEntity>() {
        	//CriteriaAPI
            @Override
            public Predicate toPredicate(Root<AccountEntity> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
            	// 「name LIKE '%name%'」を追加
            	return cb.like(root.get("name"), "%" + name + "%");
            }
        };
    }
    /**
     * 指定文字を年齢に含むアカウントを検索する。
     */
    public Specification<AccountEntity> ageEqual(String age) {

        return new Specification<AccountEntity>() {
        	//CriteriaAPI
            @Override
            public Predicate toPredicate(Root<AccountEntity> root, CriteriaQuery<?> query, CriteriaBuilder cb) {

            	// 数値変換可能かのチェック
            	try {
            		Integer.parseInt(age);

            		// 「age = 'age'」を追加
            		return cb.equal(root.get("age"), age);

            	} catch (NumberFormatException e) {
            		return null;
            	}
            }
        };
    }
}
```

## AND句・OR句で繋げる
ここでもand()やor()で繋げられますが、サービスクラスでwhere()やand()or()メソッド使った方がわかりやすいかも。

```java
            	cb.like(root.get("name"), "%" + name + "%");
            	cb.and(cb.equal(root.get("age"), age));
            	return cb.or(cb.equal(root.get("age"), age));
```

# Service
findAllの引数でwhere()やand()メソッドを呼び出して、Where句やAnd句を追加します。

```java:1語検索バージョン
@Service
public class AccountService {

	@Autowired
	AccountRepository repository;

	@Autowired
	AccountSpecification accountSpecification;

	public List<AccountEntity> searchAccount(String keyWords, Pageable pageable){

		//前後の全角半角スペースを削除
		String trimedkeyWords = keyWords.strip();

		// 全角スペースと半角スペースで区切る
		String[] keyWordArray = trimedkeyWords.split("[　 ]", 0);

		// 「Select * From account」 + 「Where name LIKE '%keyWordArray[0]%'」
		return repository.findAll(Specification
				.where(accountSpecification.ageEqual(keyWordArray[0])), pageable);
	}
}
```

「Select * From account Where(name='name' OR age='age') AND(name='name' OR age='age')」みたいにWhere句やAnd句の（）の中に入れたい時はWhere/Andメソッドの中にAnd/ORメソッドを入れ子にする。

```java:複数ワード検索バージョン
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Service;

@Service
public class AccountService {

	@Autowired
	AccountRepository repository;

	@Autowired
	AccountSpecification accountSpecification;

	public Page<AccountEntity> findAll(Pageable pageable) {
		return repository.findAll(pageable);
	}

	public Page<AccountEntity> searchAccount(String keyWords, Pageable pageable){

		//前後の全角半角スペースを削除
		String trimedkeyWords = keyWords.strip();

		// 全角スペースと半角スペースで区切る
		String[] keyWordArray = trimedkeyWords.split("[　 ]", 0);

		//todo　ここのisBlankでのnullチェックが効いてない。nullがfalseになる。
		//nullか空文字なら全検索　１語ならWhere句追加　２語以上ならAnd句追加にしたい。
		if(keyWordArray.length == 1 && StringUtils.isBlank(keyWordArray[0])) {
			return repository.findAll(pageable);

		}else if(keyWordArray.length == 1) {
			// 「Select * From account Where (name LIKE '%keyWordArray[0]%' OR age = '%keyWordArray[0]%')
			return repository.findAll(Specification
					.where(accountSpecification.nameLike(keyWordArray[0])
					.or(accountSpecification.ageEqual(keyWordArray[0]))), pageable);

		}else {
			Specification<AccountEntity> specification =
					Specification.where(accountSpecification.nameLike(keyWordArray[0])
							.or(accountSpecification.ageEqual(keyWordArray[0])));

			// 「Select * From account Where(name LIKE '%keyWordArray[0]%' OR age = '%keyWordArray[0]%') AND(name LIKE '%keyWordArray[i]%' OR age = '%keyWordArray[i]%') AND ・・・
			for(int i = 1; i < keyWordArray.length; i++) {
				specification = specification.and(accountSpecification.nameLike(keyWordArray[i])
						.or(accountSpecification.ageEqual(keyWordArray[i])));
			}
			return repository.findAll(specification, pageable);
		}
	}
}
```

# Controller

```java
@Controller
public class AccountController {
	//全表示 or 検索の判定用
	boolean isAllOrSearch;

	@Autowired
	AccountService accountService;

	//アカウント全表示
	@GetMapping("/hello")
	public String getHello( @PageableDefault(page=0, size=2)Pageable pageable, Model model) {
		isAllOrSearch = true;
		Page<AccountEntity> accountAll = accountService.findAll(pageable);

		model.addAttribute("isAllOrSearch", isAllOrSearch);
		model.addAttribute("page", accountAll);
		model.addAttribute("accountAll", accountAll.getContent());
		return "hello";
	}

	//アカウント検索
	@GetMapping("/search")
	public String getName(@RequestParam("keyWords")String keyWords, Model model, @PageableDefault(page = 0, size=2)Pageable pageable) {
		isAllOrSearch = false;
		Page<AccountEntity> accountAll = accountService.searchAccount(keyWords, pageable);

		model.addAttribute("keyWords", keyWords);
		model.addAttribute("isAllOrSearch", isAllOrSearch);
		model.addAttribute("page", accountAll);
		model.addAttribute("accountAll", accountAll.getContent());

		return "hello";
	}
}
```

# HTML
アカウント全表示用と検索用でほぼ同じページネーションを書いてしまっている。
サービスクラスでのnullチェックが上手くいかないため。

```html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
	<form action="/search" method="get">
		<input type="text" name="keyWords">
		<input type="submit">
	</form>

	<table>
		<tbody>
			<tr>
				<th>ID</th>
				<th>名前</th>
				<th>年齢</th>
			</tr>
			<tr th:each="account : ${accountAll}">
				<td th:text="${account.id}">id</td>
				<td th:text="${account.name}">名前</td>
				<td th:text="${account.age}">年齢</td>
			</tr>
		</tbody>
	</table>
	<!-- ページネーション -->
		<div th:if="${isAllOrSearch}">
			<ul>
				<li style="display: inline;"><span th:if="${page.isFirst}">&lt;&lt;先頭</span>
					<a th:if="${!page.isFirst}"
					th:href="@{/hello(page = 0)}">
						&lt;&lt;先頭 </a></li>
				<li style="display: inline; margin-left: 10px;"><span th:if="${page.isFirst}">前</span>
					<a th:if="${!page.isFirst}"
					th:href="@{/hello(page = ${page.number} - 1)}">
						前</a></li>
				<li th:if="${!page.empty}" 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="@{/hello(page = ${i})}"> <span
						th:text="${i+1}">1</span></a></li>
				<li style="display: inline; margin-left: 10px;"><span
					th:if="${page.isLast}">次</span> <a th:if="${!page.isLast}"
					th:href="@{/hello(page = (${page.number} + 1))}">
						次</a></li>
				<li style="display: inline; margin-left: 10px;"><span
					th:if="${page.last}">最後&gt;&gt;</span> <a th:if="${!page.isLast}"
					th:href="@{/hello(page = ${page.totalPages - 1})}">
						最後&gt;&gt; </a></li>
			</ul>
		</div>

		<div th:if="${!isAllOrSearch}">
			<ul>
				<li style="display: inline;"><span th:if="${page.isFirst}">&lt;&lt;先頭</span>
					<a th:if="${!page.isFirst}"
					th:href="@{'/search?keyWords=' + ${keyWords} + '&page=0'}">
						&lt;&lt;先頭 </a></li>
				<li style="display: inline; margin-left: 10px;"><span th:if="${page.isFirst}">前</span>
					<a th:if="${!page.isFirst}"
					th:href="@{'/search?keyWords=' + ${keyWords} + '&page=' + ${page.number - 1}}">
						前</a></li>
				<li th:if="${!page.empty}" 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="@{'/search?keyWords=' + ${keyWords} + '&page=' + ${i}}"> <span
						th:text="${i+1}">1</span></a></li>
				<li style="display: inline; margin-left: 10px;"><span
					th:if="${page.isLast}">次</span> <a th:if="${!page.isLast}"
					th:href="@{'/search?keyWords=' + ${keyWords} + '&page=' + ${page.number + 1}}">
						次</a></li>
				<li style="display: inline; margin-left: 10px;"><span
					th:if="${page.last}">最後&gt;&gt;</span> <a th:if="${!page.isLast}"
					th:href="@{'/search?keyWords=' + ${keyWords} + '&page=' + ${page.totalPages - 1}}">
						最後&gt;&gt; </a></li>
			</ul>
		</div>
</body>
</html>
```

# Specificationパターン
Specificationパターンとは、仕様を満たすことを目的にしたデザインパターン。
例えば人材探しで条件と候補者を分ける。
普通ならIF文とかSQLのWhere句とかで実装する。

specificationパターンでは
and() や or() メソッドで、個々の条件判断オブジェクトを、追加していく。

今回の例で言えば、条件判断の元がEntityに入ってて、andやorメソッドで条件を追加していく。
（間違っていたらごめんなさい）

# 参考
[Spring Data JPA の Specificationでらくらく動的クエリー](https://qiita.com/tag1216/items/3a408d2751a6310e2948)
[[JPA] DB検索時の条件を動的に設定する](https://qiita.com/ksh-fthr/items/44ecc0550a77f4280ea7)
[JPA Specificationで複数キーワードによる絞り込み検索](https://qiita.com/ibara1454/items/7d65d75a910dced33b50)
[Specification パターン ：複雑な ビジネスルールの表現手段](http://masuda220.jugem.jp/?eid=331)

# 終わりに
ここのところSpringBootやJPAに触れていてインターフェースの中身を覗くことが多かった。
抽象的なコードを理解するために、デザインパターンを知っておくと良さそう。
Javaのデザインパターンの名著があるらしいので買ってみよう。