Spring DATA JPAでデータ検索 その2

2016.05.18

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

前回の続きとなります。
複合主キーの場合と、テーブル結合した検索方法ついてです。
前回よりも少しだけ面倒ではございますが、難しくはないと思います。

テーブルの作成

前回作ったデータベースに、顧客テーブルと売上テーブルを追加します。

CREATE TABLE customer_mst (
  customer_id varchar(10) NOT NULL,
  customer_name varchar(256) NOT NULL,
  age tinyint(3) unsigned NOT NULL,
  gender tinyint(1) unsigned NOT NULL,
  address varchar(256) DEFAULT NULL,
  PRIMARY KEY (customer_id)
);
CREATE TABLE sales_trn (
  sales_id varchar(10) NOT NULL,
  sales_detail_no int(11) NOT NULL,
  customer_id varchar(10) NOT NULL,
  goods_id varchar(10) NOT NULL,
  quantity int(11) DEFAULT NULL,
  amount decimal(9,2) DEFAULT NULL,
  sales_date datetime DEFAULT NULL,
  PRIMARY KEY (sales_id, sales_detail_no)
);

適当に検索用のデータも入れておきましょう。

実装

複合主キーを持つテーブルを扱う

顧客テーブルの検索は前回と同じような手順で作れるはずなので割愛します。
売上テーブの検索はsales_id、sales_detail_noの複合主キーとなっていますので一手間必要となります。
以下の用に@Idアノテーションを複数つけるだけでは、アプリケーション起動時に例外が発生します。

Sales.java

package jp.classmethod.entity;

import java.math.BigDecimal;

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

import lombok.Getter;
import lombok.Setter;

@Entity
@Table(name="sales_trn")
public class Sales {
	
	@Id
	@Column(name="sales_id")
	@Getter
	@Setter
	private String salesId;
	
	@Id
	@Column(name="sales_detail_no")
	@Getter
	@Setter
	private int salesDetailNo;
	
	@NotNull
	@Getter
	@Setter
	private String customerId;
	
	@NotNull
	@Getter
	@Setter
	private String goodsId;
	
	@Getter
	@Setter
	private int quantity;
	
	@Getter
	@Setter
	private BigDecimal amount;
	
}

Spring JPAのEntityで複合主キーを扱いたい場合、別途キーをまとめたクラスを作成する必要があります。
フィールド名はEntityのフィールド名と同じにして、Serializable実装クラスとして作成します。

SalesKeyId.java

package jp.classmethod.entity;

import java.io.Serializable;

import lombok.Getter;
import lombok.Setter;

public class SalesKeyId implements Serializable {
	
	@Getter
	@Setter
	private String salesId;
	
	@Getter
	@Setter
	private int salesDetailNo;
	
}

作成したら、以下のように、Entityに@IdClassアノテーションを付けてあげてください。

@Entity
@Table(name="sales_trn")
@IdClass(value=SalesKeyId.class)
public class Sales {

検索用のRepositoryのジェネリクスの第二引数には、先ほど作成したIdクラスを指定します。

SalesRepository.java

package jp.classmethod.repository;

import jp.classmethod.entity.Sales;
import jp.classmethod.entity.SalesKeyId;

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

public interface SalesRepository extends JpaRepository<Sales, SalesKeyId> {}

これだけできれば、前回と同じようにデータの取得が簡単にできます。

テーブルの結合

上で作成した売上テーブルからデータを取得して、画面に表示するのですが、
顧客名、商品名は顧客テーブル、商品テーブルと結合して取得します。
結合の方法なのですが、以下のような形で、売上テーブルのEntityに顧客テーブルのEntityと商品テーブルのEntityをフィールドとして持たせてあげればよいです。
テーブルのリレーションによっては@OneToOneアノテーションだけではなく、@OneToManyとか@ManyToOneとか使えるようです。
@JoinColumnアノテーションで結合するカラム名を指定してあげます。

	@Getter
	@Setter
	@OneToOne
	@JoinColumn(name="customer_id", insertable=false, updatable=false)
	private Customer customer;
	
	@Getter
	@Setter
	@OneToOne
	@JoinColumn(name="goods_id", insertable=false, updatable=false)
	private Goods goods;

検索画面やJava側の処理は、前回とほぼ同じような感じになりますが、
ソースコードは残しておきます。

sales.html

<!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">
      <table>
        <tr><td>売上ID : <input type="text" class="form-control" id="sales_id" name="salesId" th:value="${salesId}"/></td></tr>
        <tr><td>顧客ID : <input type="text" class="form-control" id="customer_id" name="customerId" th:value="${customerId}"/></td></tr>
        <tr><td>商品ID : <input type="text" class="form-control" id="goods_id" name="goodsId" th:value="${goodsId}"/></td></tr>
        <tr><td><input type="submit" value="検索"/></td></tr>
      </table>
    </form>
      <table border="1">
        <tr>
          <td>売上ID</td>
          <td>売上明細番号</td>
          <td>顧客ID</td>
          <td>顧客名</td>
          <td>商品ID</td>
          <td>商品名</td>
          <td>数量</td>
          <td>売上金額</td>
          <td>売上日時</td>
        </tr>
        <tr th:each="data : ${result}">
          <td th:text="${data.salesId}"/>
          <td th:text="${data.salesDetailNo}"/>
          <td th:text="${data.customerId}"/>
          <td th:text="${data.customer.customerName}"/>
          <td th:text="${data.goodsId}"/>
          <td th:text="${data.goods.goodsName}"/>
          <td th:text="${data.quantity}"/>
          <td th:text="${#numbers.formatInteger(data.amount, 3, 'COMMA')}"/>
          <td th:text="${#dates.format(data.salesDate, 'yyyy/MM/dd HH:mm')}"/>
        </tr>
      </table>
  </body>
</html>

Thymeleaf メモ
#numbers.formatInteger(data.amount, 3, 'COMMA')}"で数字を3桁ずつカンマ区切りしてくれる。
#dates.format(data.salesDate, 'yyyy/MM/dd HH:mm') 日付型をフォーマットしてくれる。

SalesRepository.java

package jp.classmethod.repository;

import jp.classmethod.entity.Sales;
import jp.classmethod.entity.SalesKeyId;

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

public interface SalesRepository extends JpaRepository<Sales, SalesKeyId> {}

SalesRepositoryCustomImpl.java

package jp.classmethod.repository.impl;

import java.util.List;

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

import jp.classmethod.entity.Sales;
import jp.classmethod.repository.SalesRepositoryCustom;

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

@Component
public class SalesRepositoryCustomImpl implements SalesRepositoryCustom {
	
	@Autowired
	EntityManager manager;
	
	@SuppressWarnings("unchecked")
	@Override
	public List<Sales> search(String salesId, String customerId, String goodsId) {
		StringBuilder sql = new StringBuilder();
		sql.append("SELECT s From Sales s WHERE ");
		boolean andFlg = false;
		boolean salesIdFlg = false;
		boolean customerIdFlg = false;
		boolean goodsIdFlg = false;
		if (!"".equals(salesId) && salesId != null) {
			sql.append(" s.salesId = :salesId ");
			salesIdFlg = true;
			andFlg = true;
		}
		if (!"".equals(customerId) && customerId != null) {
			if (andFlg) sql.append(" AND ");
			sql.append(" s.customerId = :customerId ");
			customerIdFlg = true;
			andFlg = true;
		}
		if (!"".equals(goodsId) && goodsId != null) {
			if (andFlg) sql.append(" AND ");
			sql.append(" s.goodsId = :goodsId ");
			goodsIdFlg = true;
			andFlg = true;
		}
		Query query = manager.createQuery(sql.toString());
		if (salesIdFlg) query.setParameter("salesId",  salesId);
		if (customerIdFlg) query.setParameter("customerId",  customerId);
		if (goodsIdFlg) query.setParameter("goodsId", goodsId);
		return query.getResultList();
	}
	
}

SalesService.java

package jp.classmethod.service;

import java.util.List;

import jp.classmethod.entity.Sales;
import jp.classmethod.repository.SalesRepository;
import jp.classmethod.repository.SalesRepositoryCustom;

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

@Service
public class SalesService {
	
	@Autowired
	SalesRepository repository;
	@Autowired
	SalesRepositoryCustom repositoryCustom;
	
	public List<Sales> search(String salesId, String customerId, String goodsId) {
		List<Sales> result;
		if ("".equals(salesId) && "".equals(customerId) && "".equals(goodsId)) {
			result = repository.findAll();
		} else {
			result = repositoryCustom.search(salesId, customerId, goodsId);
		}
		return result;
	}
	
}

SalesController.java

package jp.classmethod.controller;

import java.util.List;

import jp.classmethod.entity.Sales;
import jp.classmethod.service.SalesService;

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("/sales")
public class SalesController {
	
	private static final String VIEW = "/sales";
	
	@Autowired
	private SalesService service;
	
	@RequestMapping(method = RequestMethod.GET)
	public String index() {
		return VIEW;
	}
	
	@RequestMapping(method = RequestMethod.POST)
	public ModelAndView search(ModelAndView mav
			, @RequestParam("salesId") String salesId, @RequestParam("customerId") String customerId
			, @RequestParam("goodsId") String goodsId) {
		mav.setViewName(VIEW);
		mav.addObject("salesId", salesId);
		mav.addObject("customerId", customerId);
		mav.addObject("goodsId", goodsId);
		List<Sales> result = service.search(salesId, customerId, goodsId);
		mav.addObject("result", result);
		return mav;
	}
	
}

動かしてみる

アプリケーションを起動し、http://localhost:8080/salesに接続する。

springjpa2016051801 springjpa2016051802

実際に検索条件に合わせて、検索ができることが確認できればOKです。

色々と調べながら作ってはみたのですが、様々なアノテーションがあってSpring Data JPAは奥が深そうです。 とはいえ、慣れてしまえば難しくはないような感じもしますので、もう少し触ってみて慣れていきたいと思います。

参考URL

Spring Bootで簡単な検索アプリケーションを開発する - Qiita
Spring Data JPAを使用したプログラミング方法
テーブルの結合 - シュンツのつまづき日記
Thymeleafの機能メモ - Qiita
JPA関連アノテーションの基本として-その2- - A Memorandum