Spring DATA JPAでデータ検索

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

GWも終わり、夏が近づいてまいりましたね。
今回は、Spring Data JPAを使ってデータ検索してみました。
ちょこっとだけハマったところもあったので、個人的なメモとして残しておきます。

開発環境構築

Spring Bootの開発環境ですが、私の場合はマイEclipseにプラグインを入れました。
参考サイト

テーブル作成

Spring DATA JPAとSpring Bootを使用して、検索を行ってみます。
とりあえず検索用のデータがないといけないので、サンプルデータを作成します。
商品テーブルを作成してデータを適当に入れておきます。

CREATE TABLE goods_mst (
  goods_id varchar(10) NOT NULL,
  goods_name varchar(256) NOT NULL,
  price decimal(9,0) NOT NULL,
  PRIMARY KEY (goods_id)
);

※DBはMySQLを使用しました。

プロジェクト作成

Eclipseで新規Spring Starter Projectを作成します。
依存関係は次のものを選択しました。
springjpa2015051101
私の環境だと、プロジェクト作成時にJREシステム・ライブラリーがダブっていて、エラーが発生してました。
springjpa2015051102
build.gradleの4行目を削除したところ、ダブらなくなりました。

eclipse {
	classpath {
		 containers.remove('org.eclipse.jdt.launching.JRE_CONTAINER')
		 containers 'org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8'
	}
}

DB接続するため、src/main/resources以下のapplication.propertiesにDB設定を記述します。

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

propertiesファイルより、ymlファイルの方が個人的には見やすい感じがして好きなので、私はapplication.ymlに変更しました

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

実装

ますは、検索処理を動かすための画面を作成します。

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
  <head>
    <title>商品検索</title>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
  </head>
  <body>
    <form method="post" name="form1">
      <table>
        <tr><td>商品ID : <input type="text" class="form-control" id="goods_id" name="goodsId" th:value="${goodsId}"/></td></tr>
        <tr><td>商品名 : <input type="text" class="form-control" id="goods_name" name="goodsName" th:value="${goodsName}"/></td></tr>
        <tr>
          <td>価格帯 : <input type="text" class="form-control" id="price_from" name="priceFrom" th:value="${priceFrom}"/>
          ~ <input type="text" class="form-control" id="price_to" name="priceTo" th:value="${priceTo}"/></td>
        </tr>
        <tr><td><input type="submit" value="検索"/></td></tr>
      </table>
    </form>
    <div th:if="${resultSize > 0}"><label th:text="${resultSize}"></label>件</div>
    <table border="1" th:if="${resultSize > 0}">
      <tr>
        <td>商品ID</td>
        <td>商品名</td>
        <td>価格</td>
      </tr>
      <tr th:each="data : ${result}">
        <td th:text="${data.goodsId}"/>
        <td th:text="${data.goodsName}"/>
        <td th:text="${data.price}"/>
      </tr>
    </table>
  </body>
</html>

Entityの中身は以下のようになります。

package jp.classmethod.entity;

import java.math.BigDecimal;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
import javax.validation.constraints.NotNull;

import lombok.Getter;
import lombok.Setter;

@Entity
@Table(name="goods_mst")
public class Goods {
	
	@Id
	@Column(name="goods_id")
	@Getter
	@Setter
	private String goodsId;
	
	@Column(name="goods_name")
	@Getter
	@Setter
	private String goodsName;
	
	@NotNull
	@Getter
	@Setter
	private BigDecimal price;
	
}

あとは、JPARepositoryを継承したinterfaceを作成すれば、これを使うだけで最低限の検索ができちゃいます。
interfaceを作るだけで検索できるのが簡単で素敵ですね。
ジェネリクスの第一引数には、Entity、第二引数にはEntityでIdアノテーションを付けたプロパティの型(主キーの型)を指定してあげます。

package jp.classmethod.repository;

import jp.classmethod.entity.Goods;

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

public interface GoodsRepository extends JpaRepository<Goods, String> {
}

画面を動かすための、ControllerとServiceを作成します。

package jp.classmethod.service;

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

import jp.classmethod.entity.Goods;
import jp.classmethod.repository.GoodsRepository;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class GoodsService {
	
	@Autowired
	GoodsRepository repository;
	
	public List<Goods> search(String goodsId, String goodsName, BigDecimal priceFrom, BigDecimal priceTo) {
		List<Goods> result = repository.findAll();
		return result;
	}
	
}
package jp.classmethod.controller;

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

import jp.classmethod.entity.Goods;
import jp.classmethod.service.GoodsService;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.ComponentScan;
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;

@ComponentScan
@Controller
@RequestMapping("/goods")
public class GoodsController {
	
	private static final String VIEW = "/goods";
	
	@Autowired
	public GoodsService service;
	
	@RequestMapping(method = RequestMethod.GET)
	public String index() {
		return VIEW;
	}
	
	@RequestMapping(method = RequestMethod.POST)
	public ModelAndView login(ModelAndView mav
			, @RequestParam("goodsId") String goodsId, @RequestParam("goodsName") String goodsName
			, @RequestParam("priceFrom") BigDecimal priceFrom, @RequestParam("priceTo") BigDecimal priceTo) {
		mav.setViewName(VIEW);
		mav.addObject("goodsId", goodsId);
		mav.addObject("goodsName", goodsName);
		mav.addObject("priceFrom", priceFrom);
		mav.addObject("priceTo", priceTo);
		List<Goods> result = service.search(goodsId, goodsName, priceFrom, priceTo);
		mav.addObject("result", result);
		mav.addObject("resultSize", result.size());
		return mav;
	}
	
}

ここまで出来れば、アプリケーションを起動して、http://localhost:8080/goodsにアクセス、検索処理が動かせるはずです。

springjpa2015051103

とはいえこのままだと、画面上の検索条件が活きていないので、検索条件に合わせて検索ができるようにします。
色々と実装方法がありますが、画面に入力された条件により、クエリを動的に生成してみましょう。

自分で新たなRepositoryインターフェースを作成して、その実装クラスも作ります。

package jp.classmethod.repository;

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

import jp.classmethod.entity.Goods;

public interface GoodsRepositoryCustom {
	public List<Goods> search(String goodsId, String goodsName, BigDecimal priceFrom, BigDecimal priceTo);
}
package jp.classmethod.repository.impl;

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

import javax.persistence.EntityManager;
import javax.persistence.Query;

import jp.classmethod.entity.Goods;
import jp.classmethod.repository.GoodsRepositoryCustom;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class GoodsRepositoryCustomImpl implements GoodsRepositoryCustom {
	
	@Autowired
	EntityManager manager;
	
	@SuppressWarnings("unchecked")
	@Override
	public List<Goods> search(String goodsId, String goodsName, BigDecimal priceFrom, BigDecimal priceTo) {
		StringBuilder sql = new StringBuilder();
		sql.append("SELECT g From Goods g WHERE ");
		boolean andFlg = false;
		boolean goodsIdFlg = false;
		boolean priceFromFlg = false;
		boolean priceToFlg = false;
		if (!"".equals(goodsId) && goodsId != null) {
			sql.append(" g.goodsId LIKE :goodsId ");
			goodsIdFlg = true;
			andFlg = true;
		}
		boolean goodsNameFlg = false;
		if (!"".equals(goodsName) && goodsName != null) {
			if (andFlg) sql.append(" AND ");
			sql.append("g.goodsName LIKE :goodsName ");
			goodsNameFlg = true;
			andFlg = true;
		}
		if (priceFrom != null) {
			if (andFlg) sql.append(" AND ");
			sql.append("g.price >= :priceFrom ");
			priceFromFlg = true;
			andFlg = true;
		}
		if (priceTo != null) {
			if (andFlg) sql.append(" AND ");
			sql.append("g.price <= :priceTo ");
			priceToFlg = true;
			andFlg = true;
		}
		Query query = manager.createQuery(sql.toString());
		if (goodsIdFlg) query.setParameter("goodsId", "%" + goodsId + "%");
		if (goodsNameFlg) query.setParameter("goodsName", "%" + goodsName + "%");
		if (priceFromFlg) query.setParameter("priceFrom", priceFrom);
		if (priceToFlg) query.setParameter("priceTo", priceTo);
		return query.getResultList();
	}
	
}

個人的ハマりポイントなのですが、最初はこんな感じで書いてうまくいくと思ったのですが、駄目でした。

sql.append(" g.goodsId LIKE % :goodsId % ");
if (goodsNameFlg) query.setParameter("goodsName", goodsName);

あいまい検索する際の"%"はパラメータをセットするときにつけるように、気を付けるようにします。

あとは、新しく作ったRepositoryを使えるようにServiceの修正をします。 検索条件がすべて未入力だった場合は全件検索を行うようにしてあります。

package jp.classmethod.service;

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

import jp.classmethod.entity.Goods;
import jp.classmethod.repository.GoodsRepository;
import jp.classmethod.repository.GoodsRepositoryCustom;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class GoodsService {
	
	@Autowired
	GoodsRepository repository;
	@Autowired
	GoodsRepositoryCustom repositoryCustom;
	
	public List<Goods> search(String goodsId, String goodsName, BigDecimal priceFrom, BigDecimal priceTo) {
		List<Goods> result;
		if ("".equals(goodsId) && "".equals(goodsName) && priceFrom == null && priceTo == null) {
			result = repository.findAll();
		} else {
			result = repositoryCustom.search(goodsId, goodsName, priceFrom, priceTo);
		}
		return result;
	}
	
}

springjpa2015051104

検索条件に合わせて検索ができることを確認します。

終わりに

今回は単純なテーブルの検索を行いました。
次は複数テーブルを結合したデータ取得や、主キーが複数あるテーブルなど使って試してみたいと思います。