Spring DATA JPAのSpecificationで動的クエリー

Spring DATA JPAを使うと、ほとんどSQLを書かなくてもよくなるようです。

【Spring Data JPA】自動実装されるメソッドの命名ルール - Qiita

⇧  そのぶん、メソッドには命名規則があるみたいです。

⇩  Spring DATA JPA 以外にも、いろいろなクエリーの実装方法があるみたい。

Spring Data JPA でのクエリー実装方法まとめ - Qiita

 

 で、今回、Spring DATA JPASQLの「WHERE句」を動的に変えられないかということにチャレンジしました。

Spring Data JPA の Specificationでらくらく動的クエリー - Qiita

⇧  こちらのサイトで詳しく載ってます。

Specification

Specificationは何らかの検索条件を表すインターフェイスで、実装クラスではCriteriaAPIを使用して検索条件を実装する。 Specification を使用するには、先ず Repositoryインターフェイスを JpaSpecificationExecutor を継承した形にする。

Spring Data JPA の Specificationでらくらく動的クエリー - Qiita

ということで、さっそく作っていきましょう。 

MySQLでテーブル作成

データベースは前回までに作っていた、「sample」データベースを使っていきます。

コマンドプロンプトなどでMySQLに接続。

mysql -u root -p

f:id:ts0818:20170911210624j:plain

 データベースの確認。

SHOW DATABASES;

f:id:ts0818:20170911210647j:plain

 使用するデータベースの選択し、テーブル作成。

USE sample;
CREATE TABLE items(
  item_id VARCHAR(10) NOT NULL,
  item_name VARCHAR(256) NOT NULL,
  price decimal(10, 0) NOT NULL,
  PRIMARY KEY(item_id)
);

f:id:ts0818:20170911211025j:plain

テーブルが作成されました。

f:id:ts0818:20170911211334j:plain

何件かデータを追加しておきます。

INSERT INTO items (item_id, item_name, price) VALUES('s0001', '戸隠そば', 1000);
INSERT INTO items (item_id, item_name, price) VALUES('s0002', '讃岐うどん', 1000);
INSERT INTO items (item_id, item_name, price) VALUES('s0003', 'らーめん', 600);

f:id:ts0818:20170911211914j:plain

データ確認。

    SELECT * FROM items;

f:id:ts0818:20170911211952j:plain

コマンドプロンプトでの作業はこれで終了なので閉じてしまって問題ないです。

 

EclipseでSpring スターター・プロジェクト作成

Eclipseを起動し、「ファイル(F)」>「新規(N)」>「Spring スターター・プロジェクト」を選択。

f:id:ts0818:20170911205116j:plain

「名前」を入力し、「ワーキング・セット」に追加する場合はチェック。「次へ(N)>」をクリック。

f:id:ts0818:20170911205232j:plain

「コア」>「Lombok」、「SQL」>「JPA」、「SQL」>「MySQL」、「テンプレートエンジン」>「Thymeleaf」、「Web」>「Web」にチェックし、「次へ(N)>」をクリック。

f:id:ts0818:20170911214717j:plain

「完了(F)」をクリック。

f:id:ts0818:20170911205649j:plain

 

ファイルなどを設定、作成

最初に、「sources/main/resources」>「application.properties」ファイルにデータベース接続の設定を記述。「データベース名」「username」「password」の部分はご自分の環境に合わせてください。

spring.datasource.url=jdbc:mysql://localhost/sample?useSSL=true
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driverClassName=com.mysql.jdbc.Driver

 

まずは、「src/main/java」直下のパッケージを選択した状態で右クリックし、「新規(W)」>「フォルダー」 をクリック。

f:id:ts0818:20170911212350j:plain

「フォルダー名(N):」を入力し、「完了(F)」をクリック。

f:id:ts0818:20170911212626j:plain

この流れで、フォルダーを4つ作成します。(「repository」「service」「entity」「controller」で作成しました。)

f:id:ts0818:20170911212742j:plain

まずは、「repository」フォルダーを選択した状態で右クリックし、「新規(W)」>「インターフェース」を選択。

f:id:ts0818:20170911214755j:plain

「名前(M):」を入力し、「拡張インターフェイス(I):」の「追加(A)...」をクリック。

f:id:ts0818:20170911214851j:plain

「インターフェースを選択してください(C):」のテキストエリアに「Jpa」と入力して出てくる候補から、「JpaRepository」「JpaSpecificationExecutor」の2つを選択して、「OK」。

f:id:ts0818:20170911214948j:plain

「拡張インターフェイス(I):」が追加されたら、「完了(F)」。

f:id:ts0818:20170911215201j:plain

エラーが出ますが、後でファイルの内容を編集していきます。

f:id:ts0818:20170911215346j:plain

続いて、「service」フォルダを選択した状態で右クリックし、「新規(W)」>「クラス」を選択。

f:id:ts0818:20170911220546j:plain

「名前(M):」を入力し、「完了(F)」。

f:id:ts0818:20170911220638j:plain

「service」フォルダには、同じ流れでもう1ファイル作成します。

f:id:ts0818:20170911220733j:plain

続いて、「entity」フォルダを選択した状態で右クリックし、「新規(W)」>「クラス」を選択。

f:id:ts0818:20170911220844j:plain

「名前(M):」を入力し、「インターフェイス(I):」の「追加(A)...」をクリック。

f:id:ts0818:20170911220951j:plain

「Serializable - java-io」を選択し、「OK」。

f:id:ts0818:20170911221106j:plain

インターフェイス(I):」が追加されてるのを確認し、「完了(F)」をクリック。

f:id:ts0818:20170911221145j:plain

続いて、「controller」フォルダを選択した状態で右クリックし、「新規(W)」>「クラス」をクリック。

f:id:ts0818:20170911221238j:plain

「名前(M):」を入力し、「完了(F)」をクリック。

f:id:ts0818:20170911221331j:plain

最後に、View側を担うhtmlファイルを作っていきます。

「src/main/resources」>「templates」を選択した状態で右クリックし、「新規(W)」>「その他(O)...」を選択。

f:id:ts0818:20170911221427j:plain

「Web」>「HTMLファイル」を選択して、「次へ(N)>」をクリック。

f:id:ts0818:20170911221605j:plain

「ファイル名(M):」を入力し、「次へ(N)>」をクリック。

f:id:ts0818:20170911221746j:plain

「完了(F)」をクリック。

f:id:ts0818:20170911221837j:plain

最終的な「パッケージ・エクスプローラー」の構成はこんな感じ。

f:id:ts0818:20170911221904j:plain

 

ファイルの編集

entity

Item.java

package com.example.demo.entity;

import java.io.Serializable;
import java.math.BigDecimal;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;

import lombok.Data;
@Entity
@Data
@Table(name="items")
public class Item implements Serializable {
	@Id
	@Column(name="item_id")
  private String itemId;

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

	@Column(name="price")
  private BigDecimal price;
}

repository

ItemRepository.java

package com.example.demo.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;

import com.example.demo.entity.Item;

public interface ItemRepository extends JpaRepository<Item, String>, JpaSpecificationExecutor {

}

service

ItemSpecifications.java

package com.example.demo.service;

import java.math.BigDecimal;

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;

import com.example.demo.entity.Item;
@Service
public class ItemSpecifications {

	// 商品IDを含むものを検索
	public static Specification itemIdContains(String itemId) {
		return StringUtils.isEmpty(itemId) ? null : new Specification() {
			@Override
			public Predicate toPredicate(Root root, CriteriaQuery<?> query, CriteriaBuilder cb) {
				return cb.like(root.get("itemId"), itemId);
			}
		};
	}

	// 商品名を含むものを検索
	public static Specification itemNameContains(String itemName) {
		return StringUtils.isEmpty(itemName) ? null : new Specification() {
		  @Override
		  public Predicate toPredicate(Root root, CriteriaQuery<?> query, CriteriaBuilder cb) {
		  	return cb.like(root.get("itemName"), "%" + itemName + "%");
		  }
		};
	}

	// 価格(最低価格)以上のもの
	public static Specification priceGreaterThanEqual(BigDecimal priceFrom) {
		return StringUtils.isEmpty(priceFrom) ? null : new Specification() {
			@Override
			public Predicate toPredicate(Root root, CriteriaQuery<?> query, CriteriaBuilder cb) {
				return cb.greaterThanOrEqualTo(root.get("price"), priceFrom);
			}
		};
	}

	// 価格(最高価格)以下のもの
	public static Specification priceLessThanEqual(BigDecimal priceTo) {
		return StringUtils.isEmpty(priceTo) ? null : new Specification() {
			@Override
			public Predicate toPredicate(Root root, CriteriaQuery<?> query, CriteriaBuilder cb) {
				return cb.lessThanOrEqualTo(root.get("price"), priceTo);
			}
		};
	}
}

service

ItemService.java

package com.example.demo.service;

import java.math.BigDecimal;
import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.jpa.domain.Specifications;
import org.springframework.stereotype.Service;

import com.example.demo.entity.Item;
import com.example.demo.repository.ItemRepository;

@Service
public class ItemService {

	@Autowired
	ItemRepository repository;

  public List findItems(String itemId, String itemName, BigDecimal priceFrom, BigDecimal priceTo) {
  	return repository.findAll(Specifications
  			.where(ItemSpecifications.itemIdContains(itemId))
  			.and(ItemSpecifications.itemNameContains(itemName))
  			.and(ItemSpecifications.priceGreaterThanEqual(priceFrom))
  			.and(ItemSpecifications.priceLessThanEqual(priceTo)));
  }
}

controller

ItemController.java

package com.example.demo.controller;

import java.math.BigDecimal;
import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
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.servlet.ModelAndView;

import com.example.demo.entity.Item;
import com.example.demo.service.ItemService;

@Controller
@RequestMapping("/item")
public class ItemController {

	private static final String VIEW = "item";

	@Autowired
	public ItemService service;

	@RequestMapping(method = RequestMethod.GET)
	public String index() {
		return VIEW;
	}

	@RequestMapping(method = RequestMethod.POST)
	public ModelAndView itemList(ModelAndView mov,
			@RequestParam(name="itemId", required=false) String itemId, @RequestParam(name="itemName", required=false) String itemName,
			@RequestParam(name="priceFrom", required=false) BigDecimal priceFrom, @RequestParam(name="priceTo") BigDecimal priceTo) {

		mov.setViewName(VIEW);
		mov.addObject("itemId", itemId);
		mov.addObject("itemName", itemName);
		mov.addObject("priceFrom", priceFrom);
		mov.addObject("priceTo", priceTo);
		List items = service.findItems(itemId, itemName, priceFrom, priceTo);
		mov.addObject("items", items);
		mov.addObject("itemsSize", items.size());
		return mov;
	}
}

item.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8" />
<title>検索テスト</title>
</head>
<body>
    <form method="post" name="form">
      <table>
        <tr><td>商品ID : <input type="text" id="itemId" name="itemId" th:value="${itemId}"/></td></tr>
        <tr><td>商品名 : <input type="text" id="itemName" name="itemName" th:value="${itemName}"/></td></tr>
        <tr>
          <td>価格帯 : <input type="text" id="priceFrom" name="priceFrom" th:value="${priceFrom}"/>
          ~ <input type="text" id="priceTo" name="priceTo" th:value="${priceTo}"/></td>
        </tr>
        <tr><td><input type="submit" value="検索"/></td></tr>
      </table>
    </form>
    <div th:if="${itemsSize > 0}"><label th:text="${itemsSize}"></label>件</div>
    <table border="1" th:if="${itemsSize > 0}">
      <tr>
        <td>商品ID</td>
        <td>商品名</td>
        <td>価格</td>
      </tr>
      <tr th:each="data : ${items}">
        <td th:text="${data.itemId}"/>
        <td th:text="${data.itemName}"/>
        <td th:text="${data.price}"/>
      </tr>
    </table>
</body>
</html>

サーバーで実行。「実行(R)」>「Spring Boot アプリケーション」を選択。

f:id:ts0818:20170911233220j:plain

http://localhost:8080/item』にアクセスし、何も入力しないで「検索」をクリックすると、全件取得。

f:id:ts0818:20170911233321j:plain

「価格帯」で絞ったり、

f:id:ts0818:20170911233424j:plain

「商品名」「価格帯」で絞ったりすることができました。

f:id:ts0818:20170911233502j:plain

かなり大雑把なつくりになってしまっているので、改良していかねばなりませんが、Spring DATA JPAでも動的クエリーが使えるみたいです。

「ItemService.java」で「import static ItemSpecifications.*;」ができなかったのがよく分からんですが。

 

2017年9月12日  追記

職場の先輩にアドバイスをもらい、パッケージ名までつけることでimportできました。あとは、@AutowiredでItemSpecificationsをDI(依存性注入)すればOKっぽいです。

同一パッケージだったので、パッケージ名つけなくてもimportできると思い込んでました...反省。

package com.example.demo.service;

import static com.example.demo.service.ItemSpecifications.*;

import java.math.BigDecimal;
import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.jpa.domain.Specifications;
import org.springframework.stereotype.Service;

import com.example.demo.entity.Item;
import com.example.demo.repository.ItemRepository;

@Service
public class ItemService {

	@Autowired
	ItemRepository repository;

	@Autowired
	ItemSpecifications specification;

  public List findItems(String itemId, String itemName, BigDecimal priceFrom, BigDecimal priceTo) {
  	return repository.findAll(Specifications
  			.where(itemIdContains(itemId))
  			.and(itemNameContains(itemName))
  			.and(priceGreaterThanEqual(priceFrom))
  			.and(priceLessThanEqual(priceTo)));
  }
}