JPQLのクエリ結果をHibernate ResultTransformerを使ってPOJOへマッピングする

# 概要

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 "エイリアス名"というようにエイリアスを付けます。カラム名とエイリアス名が同じ場合でも省略はできません |

```java
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

```java
org.hibernate.Query.list
```

JPA

```java
javax.persistence.Query.getResultList
```

**補足. 実際に実行されるSQL**

```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)クラス**

```java:ItemDto
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テーブル**

アイテムが属するカテゴリデータを持つテーブル

```sql: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テーブル**

アイテムデータを持つテーブル

```sql: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テーブル**

アイテムの在庫を持つテーブル、アイテムに対して複数件存在する。

```sql: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エンティティ**

```java: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エンティティ**

```java: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です。

```java:StandardType
package com.example.domain;

public enum StandardType {
    UNKNOWN,
    A, B, C, D, E, F, G, H, I, J;
}
```

**ItemStockエンティティ**

```java: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;
}
```