Spring DATA JPAを使うと、ほとんどSQLを書かなくてもよくなるようです。
・【Spring Data JPA】自動実装されるメソッドの命名ルール - Qiita
⇧ そのぶん、メソッドには命名規則があるみたいです。
⇩ Spring DATA JPA 以外にも、いろいろなクエリーの実装方法があるみたい。
・Spring Data JPA でのクエリー実装方法まとめ - Qiita
で、今回、Spring DATA JPAでSQLの「WHERE句」を動的に変えられないかということにチャレンジしました。
・Spring Data JPA の Specificationでらくらく動的クエリー - Qiita
⇧ こちらのサイトで詳しく載ってます。
Specification
Specificationは何らかの検索条件を表すインターフェイスで、実装クラスではCriteriaAPIを使用して検索条件を実装する。 Specification を使用するには、先ず Repositoryインターフェイスを JpaSpecificationExecutor を継承した形にする。
ということで、さっそく作っていきましょう。
MySQLでテーブル作成
データベースは前回までに作っていた、「sample」データベースを使っていきます。
mysql -u root -p
データベースの確認。
SHOW DATABASES;
使用するデータベースの選択し、テーブル作成。
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) );
テーブルが作成されました。
何件かデータを追加しておきます。
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);
データ確認。
SELECT * FROM items;
コマンドプロンプトでの作業はこれで終了なので閉じてしまって問題ないです。
EclipseでSpring スターター・プロジェクト作成
Eclipseを起動し、「ファイル(F)」>「新規(N)」>「Spring スターター・プロジェクト」を選択。
「名前」を入力し、「ワーキング・セット」に追加する場合はチェック。「次へ(N)>」をクリック。
「コア」>「Lombok」、「SQL」>「JPA」、「SQL」>「MySQL」、「テンプレートエンジン」>「Thymeleaf」、「Web」>「Web」にチェックし、「次へ(N)>」をクリック。
「完了(F)」をクリック。
ファイルなどを設定、作成
最初に、「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)」>「フォルダー」 をクリック。
「フォルダー名(N):」を入力し、「完了(F)」をクリック。
この流れで、フォルダーを4つ作成します。(「repository」「service」「entity」「controller」で作成しました。)
まずは、「repository」フォルダーを選択した状態で右クリックし、「新規(W)」>「インターフェース」を選択。
「名前(M):」を入力し、「拡張インターフェイス(I):」の「追加(A)...」をクリック。
「インターフェースを選択してください(C):」のテキストエリアに「Jpa」と入力して出てくる候補から、「JpaRepository」「JpaSpecificationExecutor」の2つを選択して、「OK」。
「拡張インターフェイス(I):」が追加されたら、「完了(F)」。
エラーが出ますが、後でファイルの内容を編集していきます。
続いて、「service」フォルダを選択した状態で右クリックし、「新規(W)」>「クラス」を選択。
「名前(M):」を入力し、「完了(F)」。
「service」フォルダには、同じ流れでもう1ファイル作成します。
続いて、「entity」フォルダを選択した状態で右クリックし、「新規(W)」>「クラス」を選択。
「名前(M):」を入力し、「インターフェイス(I):」の「追加(A)...」をクリック。
「Serializable - java-io」を選択し、「OK」。
「インターフェイス(I):」が追加されてるのを確認し、「完了(F)」をクリック。
続いて、「controller」フォルダを選択した状態で右クリックし、「新規(W)」>「クラス」をクリック。
「名前(M):」を入力し、「完了(F)」をクリック。
最後に、View側を担うhtmlファイルを作っていきます。
「src/main/resources」>「templates」を選択した状態で右クリックし、「新規(W)」>「その他(O)...」を選択。
「Web」>「HTMLファイル」を選択して、「次へ(N)>」をクリック。
「ファイル名(M):」を入力し、「次へ(N)>」をクリック。
「完了(F)」をクリック。
最終的な「パッケージ・エクスプローラー」の構成はこんな感じ。
ファイルの編集
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<item> { }
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<item> itemIdContains(String itemId) { return StringUtils.isEmpty(itemId) ? null : new Specification<item>() { @Override public Predicate toPredicate(Root<item> root, CriteriaQuery<?> query, CriteriaBuilder cb) { return cb.like(root.get("itemId"), itemId); } }; } // 商品名を含むものを検索 public static Specification<item> itemNameContains(String itemName) { return StringUtils.isEmpty(itemName) ? null : new Specification<item>() { @Override public Predicate toPredicate(Root<item> root, CriteriaQuery<?> query, CriteriaBuilder cb) { return cb.like(root.get("itemName"), "%" + itemName + "%"); } }; } // 価格(最低価格)以上のもの public static Specification<item> priceGreaterThanEqual(BigDecimal priceFrom) { return StringUtils.isEmpty(priceFrom) ? null : new Specification<item>() { @Override public Predicate toPredicate(Root<item> root, CriteriaQuery<?> query, CriteriaBuilder cb) { return cb.greaterThanOrEqualTo(root.get("price"), priceFrom); } }; } // 価格(最高価格)以下のもの public static Specification<item> priceLessThanEqual(BigDecimal priceTo) { return StringUtils.isEmpty(priceTo) ? null : new Specification<item>() { @Override public Predicate toPredicate(Root<item> 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<item> 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", required=false) BigDecimal priceTo) { mov.setViewName(VIEW); mov.addObject("itemId", itemId); mov.addObject("itemName", itemName); mov.addObject("priceFrom", priceFrom); mov.addObject("priceTo", priceTo); List<item> 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 アプリケーション」を選択。
『http://localhost:8080/item』にアクセスし、何も入力しないで「検索」をクリックすると、全件取得。
「価格帯」で絞ったり、
「商品名」「価格帯」で絞ったりすることができました。
かなり大雑把なつくりになってしまっているので、改良していかねばなりませんが、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<item> 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))); } }