こちらの記事の続きです。
本記事では、上記記事の「必要なカラムだけSELECTする」に加えて、OneToManyな2テーブルに対して飛ばしたJONクエリの結果を、自作DTOにマッピングする方法をお伝えします。
クエリチューニングの一環として「N+1になっちゃうから、JOINして一回でSELECTしたい!」時に便利なTipsになってます。
解決したい課題
- queryDSLで必要なカラムだけ取得したい
- その際、@Entityクラスではなく、自分で作ったDTO的なクラスに結果をマッピングしたい
- OneToManyなテーブルをJOINして取得したい
- List変数を持ったDTOにマッピングしたい
そもそもqueryDSLとは
JPA+HibernateでSQLを実装する際、型安全な実装を可能にしてくれるライブラリ。
Querydsl - Unified Queries for Java
結論:transformとgroupByを使う
transform
とgroupBy
というメソッドによって、List構造を持ったDTOに結果をマッピングします。
この時、List構造はMany側のテーブルの値をマッピングすることを想定します。
TABLE
book
テーブルとrental
テーブルがあります。1つの本に対してレンタルは複数回行われることが想定されるため、OneToManyになります。
以下、このようなユースケースを想定します。
- ある1つの本の情報と、その本のレンタル実績の情報を取得したい
- bookテーブルのrelase_date、rentalテーブルのreturn_deadline情報は取得不要。
- SQLはJOINを用いて1回だけ飛ばすことにしたい
CREATE TABLE book (
id VARCHAR(255) PRIMARY KEY,
title VARCHAR(255) NOT NULL,
author VARCHAR(255) NOT NULL,
release_date DATETIME NOT NULL //この行は今回取得したくない!!!
);
CREATE TABLE rental (
id VARCHAR(255) PRIMARY KEY, //この行は今回取得したくない!!!
user_id INT NOT NULL,
book_id VARCHAR(255) NOT NULL,
rental_date TIMESTAMP NOT NULL,
return_deadline DATETIME NOT NULL //この行は今回取得したくない!!!
);
TABLEクラス
import java.time.LocalDateTime
import javax.persistence.Entity
import javax.persistence.Id
import javax.persistence.JoinColumn
import javax.persistence.OneToMany
import javax.persistence.Table
@Entity
@Table(name = "book")
class BookEntity(
@Id
var id: String? = null,
val title: String? = null,
val author: String? = null,
val releaseDate: LocalDateTime? = null,
@OneToMany
@JoinColumn(name = "book_id")
val rentals: Set<RentalEntity>? = null,
)
import java.time.LocalDateTime
import javax.persistence.Entity
import javax.persistence.Id
import javax.persistence.Table
@Entity
@Table(name = "rental")
class RentalEntity(
@Id
var id: String? = null,
val userId: Int? = null,
val rentalDate: LocalDateTime? = null,
val returnDeadline: LocalDateTime? = null,
)
DTOクラス
data class BookWithRentalDto(
val id: String,
val title: String,
val author: String,
val rentals: List<RentalDto>
)
data class RentalDto(
val userId: Int,
val rentalDate: LocalDateTime,
)
クエリメソッド
select句の中にProjections.constructorメソッドを入れて、DTOにマッピングします。
import com.querydsl.core.types.Projections
import com.querydsl.jpa.impl.JPAQueryFactory
import com.sample.infra.jpa.entity.QBookEntity //自動生成されるQEntity
import com.sample.usecase.query.BookDto
val queryFactory: JPAQueryFactory = JPAQueryFactory(entityManager); //entityManagerはjavax.persistence.EntityManager
val book = QBookEntity.bookEntity //自動生成されるQEntity
val result = queryFactory
.from(book)
.leftJoin(book.rentals, rental)
.where(book.id.eq(id))
.transform(
groupBy(book.id).`as`(
Projections.constructor(
BookWithRentalDto::class.java,
book.id,
book.title,
book.author,
list(
Projections.constructor(
RentalDto::class.java,
rental.userId,
rental.rentalDate
)
)
)
)
)
val bookWithRental = result[id]
この時、変数result
の型はMap<String, List<BookWithRentalDto>>
になります。
イメージ的には
book.id | book.title | book.author | rental.useId | rental.rentalDate |
---|---|---|---|---|
1 | 鬼滅の刃1巻 | 吾峠呼世晴 | 1 | 2023/1/1 |
1 | 鬼滅の刃1巻 | 吾峠呼世晴 | 2 | 2023/2/1 |
1 | 鬼滅の刃1巻 | 吾峠呼世晴 | 3 | 2023/3/1 |
なクエリ結果をbook.idで集計(文字通りgroupBy
)してMapにまとめるような書き方になってます。