概要
JPAで実装した検索処理のクエリ結果をエンティティクラスではなく非エンティティクラス(POJO)へマッピングしたい場合、HibernateのResultTransformerを使用すると簡単に実装することができます。
このことは、参考に挙げているページ([The best way to map a projection query to a DTO (Data Transfer Object) with JPA and Hibernate] (https://vladmihalcea.com/2017/08/29/the-best-way-to-map-a-projection-query-to-a-dto-with-jpa-and-hibernate/amp/))で知りました。この記事はそのページを参考に実際にコードを書いて動作を確認した記録になります。とくに目新しいことは書いていないので、詳細をお知りになりたい場合は参考ページをご確認ください。
環境
- Windows10 Professional
- Java 1.8.0_144
- Spring Boot 1.5.6
- Spring Data JPA 1.11.6
- Hibernate 5.0.1 Final
- MySQL 5.6.25
参考
- [The best way to map a projection query to a DTO (Data Transfer Object) with JPA and Hibernate] (https://vladmihalcea.com/2017/08/29/the-best-way-to-map-a-projection-query-to-a-dto-with-jpa-and-hibernate/amp/)
- [Why you should use the Hibernate ResultTransformer to customize result set mappings] (https://vladmihalcea.com/2017/04/03/why-you-should-use-the-hibernate-resulttransformer-to-customize-result-set-mappings/)
- [Hibernate JavaDoc (5.0.12.Final) Package org.hibernate.transform] (http://docs.jboss.org/hibernate/orm/5.0/javadocs/index.html?org/hibernate/transform/Transformers.html)
- [High-Performance Java Persistence] (https://leanpub.com/high-performance-java-persistence/read)
- [JPA : How to convert a native query result set to POJO class collection] (https://stackoverflow.com/questions/13012584/jpa-how-to-convert-a-native-query-result-set-to-pojo-class-collection)
サンプルコード
検索処理
実装のポイント
No | ポイント |
---|---|
(1) | エンティティマネージャが生成するQueryのインスタンスを、unwrapメソッドで指定する型になるようにします |
(2) | aliasToBeanメソッドで指定するクラスへカラムのエイリアス名を使ってマッピングします |
(3) | カラムにAS "エイリアス名"というようにエイリアスを付けます。カラム名とエイリアス名が同じ場合でも省略はできません |
package com.example.domain.dao;
import com.example.domain.dto.ItemDto;
import com.example.domain.entity.Category;
import org.hibernate.Query;
import org.hibernate.transform.Transformers;
import org.hibernate.type.BooleanType;
import org.hibernate.type.LocalDateTimeType;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import java.time.LocalDateTime;
import java.util.List;
@Component
public class ItemDao {
@PersistenceContext
private EntityManager entityManager;
// 実装ポイント (3)
private static final String ITEM_STOCK_QUERY =
"SELECT item.id AS itemId, " +
" item.name AS itemName, " +
" item.price AS price, " +
" item.salesFrom AS salesFrom, " +
" item.salesTo AS salesTo, " +
" item.standardType AS standardType, " +
" item.category.name as categoryName, " +
" stocks.stock AS stock " +
"FROM com.example.domain.entity.Item AS item " +
"INNER JOIN item.itemStocks AS stocks " +
"WHERE item.category = :category " +
"AND item.delFlag = :delFlag " +
"AND item.salesFrom <= :currentDate " +
"AND item.salesTo >= :currentDate " +
"AND stocks.stock > 0 " +
"AND stocks.delFlag = :delFlag "
;
@Transactional
public List<ItemDto> findItemStock(final Category category, final LocalDateTime current) {
Query query = entityManager.createQuery(ITEM_STOCK_QUERY)
// 実装ポイント (1)
.unwrap(org.hibernate.Query.class)
// 実装ポイント (2)
.setResultTransformer(Transformers.aliasToBean(ItemDto.class))
.setParameter("category", category)
.setParameter("delFlag", Boolean.FALSE, BooleanType.INSTANCE)
.setParameter("currentDate", current, LocalDateTimeType.INSTANCE)
.setComment("item stock list");
System.out.println(query.getClass().getCanonicalName());
// → org.hibernate.internal.QueryImpl
//noinspection unchecked
return query.list();
}
}
留意点
Hiberanateの実装に依存するコードになります。例えば検索結果を返すメソッドは下記のように異なります。
Hibernate
org.hibernate.Query.list
JPA
javax.persistence.Query.getResultList
補足. 実際に実行されるSQL
select
item0_.id as col_0_0_,
item0_.name as col_1_0_,
item0_.price as col_2_0_,
item0_.sales_from as col_3_0_,
item0_.sales_to as col_4_0_,
item0_.standard_type as col_5_0_,
category2_.name as col_6_0_,
itemstocks1_.stock as col_7_0_
from
item item0_
inner join
item_stock itemstocks1_
on item0_.id=itemstocks1_.item_id,
category category2_
where
item0_.category_id=category2_.id
and item0_.category_id=?
and item0_.del_flag=?
and item0_.sales_from<=?
and item0_.sales_to>=?
and itemstocks1_.stock>0
and itemstocks1_.del_flag=?
検索結果をマッピングするPOJO(DTO)クラス
package com.example.domain.dto;
import com.example.domain.StandardType;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.time.LocalDateTime;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class ItemDto implements Serializable {
private static final long serialVersionUID = 7803523681004958221L;
// アイテムID
private Long itemId;
// アイテム名
private String itemName;
// 価格
private Integer price;
// 販売開始日
private LocalDateTime salesFrom;
// 販売終了日
private LocalDateTime salesTo;
// 規格
private StandardType standardType;
// 在庫数
private Integer stock;
// カテゴリ名
private String categoryName;
}
テーブル定義
サンプルコードで使用するテーブルです。
categoryテーブル
アイテムが属するカテゴリデータを持つテーブル
CREATE TABLE category (
id INT NOT NULL AUTO_INCREMENT COMMENT 'カテゴリID',
name VARCHAR(60) NOT NULL COMMENT 'カテゴリ名',
del_flag TINYINT(1) NOT NULL DEFAULT 0 COMMENT '1:論理削除',
create_at DATETIME NOT NULL DEFAULT NOW(),
update_at DATETIME NOT NULL DEFAULT NOW(),
PRIMARY KEY (id)
)
ENGINE = INNODB
DEFAULT CHARSET = UTF8
COMMENT = 'カテゴリマスター';
itemテーブル
アイテムデータを持つテーブル
CREATE TABLE item (
id INT NOT NULL AUTO_INCREMENT COMMENT 'アイテムID',
name VARCHAR(90) NOT NULL COMMENT 'アイテム名',
price INT NOT NULL COMMENT '価格',
sales_from DATE NOT NULL COMMENT '販売開始日',
sales_to DATE NOT NULL COMMENT '販売終了日',
standard_type INT NOT NULL COMMENT '規格タイプ',
category_id INT NOT NULL COMMENT 'カテゴリID',
del_flag TINYINT(1) NOT NULL DEFAULT 0 COMMENT '1:論理削除',
create_at DATETIME NOT NULL DEFAULT NOW(),
update_at DATETIME NOT NULL DEFAULT NOW(),
PRIMARY KEY (id)
)
ENGINE = INNODB
DEFAULT CHARSET = UTF8
COMMENT = 'アイテムマスター';
item_stockテーブル
アイテムの在庫を持つテーブル、アイテムに対して複数件存在する。
CREATE TABLE item_stock (
id INT NOT NULL AUTO_INCREMENT COMMENT 'アイテム在庫ID',
stock INT NOT NULL DEFAULT 0 COMMENT '在庫数',
item_id INT NOT NULL COMMENT 'アイテムID',
del_flag TINYINT(1) NOT NULL DEFAULT 0 COMMENT '1:論理削除',
create_at DATETIME NOT NULL DEFAULT NOW(),
update_at DATETIME NOT NULL DEFAULT NOW(),
PRIMARY KEY (id)
)
ENGINE = INNODB
DEFAULT CHARSET = UTF8
COMMENT = 'アイテム在庫テーブル';
エンティティクラス
Categoryエンティティ
package com.example.domain.entity;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.persistence.*;
import java.io.Serializable;
import java.time.LocalDateTime;
@Entity
@Table(name="category")
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class Category implements Serializable {
private static final long serialVersionUID = -69562357755603227L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name="name", nullable = false)
private String name;
@Column(name="del_flag", nullable = false)
private Boolean delFlag;
@Column(name="create_at", nullable = false)
private LocalDateTime createAt;
@Column(name="update_at", nullable = false)
private LocalDateTime updateAt;
}
Itemエンティティ
package com.example.domain.entity;
import com.example.domain.StandardType;
import lombok.*;
import javax.persistence.*;
import java.io.Serializable;
import java.time.LocalDateTime;
import java.util.List;
@Entity
@Table(name="item")
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@ToString(exclude = {"itemStocks"})
@EqualsAndHashCode(exclude = {"itemStocks"})
public class Item implements Serializable {
private static final long serialVersionUID = -3153084093423004609L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name="name", nullable = false)
private String name;
@Column(name="price", nullable = false)
private Integer price;
@Column(name="sales_from", nullable = false)
private LocalDateTime salesFrom;
@Column(name="sales_to", nullable = false)
private LocalDateTime salesTo;
@Enumerated(EnumType.ORDINAL)
@Column(name="standard_type", nullable = false)
private StandardType standardType;
@JoinColumn(name = "category_id", nullable = false)
@ManyToOne
private Category category;
@Column(name="del_flag", nullable = false)
private Boolean delFlag;
@Column(name="create_at", nullable = false)
private LocalDateTime createAt;
@Column(name="update_at", nullable = false)
private LocalDateTime updateAt;
@OneToMany(mappedBy = "item", cascade = CascadeType.ALL)
private List<ItemStock> itemStocks;
}
StandardType
アイテムの規格タイプを定義するenumです。
package com.example.domain;
public enum StandardType {
UNKNOWN,
A, B, C, D, E, F, G, H, I, J;
}
ItemStockエンティティ
package com.example.domain.entity;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.persistence.*;
import java.io.Serializable;
import java.time.LocalDateTime;
@Entity
@Table(name="item_stock")
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class ItemStock implements Serializable {
private static final long serialVersionUID = -7012583289204822258L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name="stock", nullable = false)
private Integer stock;
@JoinColumn(name = "item_id", nullable = false)
@ManyToOne
private Item item;
@Column(name="del_flag", nullable = false)
private Boolean delFlag;
@Column(name="create_at", nullable = false)
private LocalDateTime createAt;
@Column(name="update_at", nullable = false)
private LocalDateTime updateAt;
}