これは何?
こちらの記事の続きです。
本記事では、上記記事の「必要なカラムだけ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にまとめるような書き方になってます。