データベース

【JPA/Querydsl】OneToManyなテーブルから必要なカラムだけDTOにマッピングする(transform,groupBy)

2023年5月21日

こちらの記事の続きです。

本記事では、上記記事の「必要なカラムだけSELECTする」に加えて、OneToManyな2テーブルに対して飛ばしたJONクエリの結果を、自作DTOにマッピングする方法をお伝えします。

クエリチューニングの一環として「N+1になっちゃうから、JOINして一回でSELECTしたい!」時に便利なTipsになってます。

ITエンジニア6年目の山根です。X(Twitter)やってます。自己紹介,お問い合わせはこちらまで!

解決したい課題

  • queryDSLで必要なカラムだけ取得したい
  • その際、@Entityクラスではなく、自分で作ったDTO的なクラスに結果をマッピングしたい
  • OneToManyなテーブルをJOINして取得したい
  • List変数を持ったDTOにマッピングしたい

そもそもqueryDSLとは

JPA+HibernateでSQLを実装する際、型安全な実装を可能にしてくれるライブラリ。

Querydsl - Unified Queries for Java

結論:transformとgroupByを使う

transformgroupByというメソッドによって、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.idbook.titlebook.authorrental.useIdrental.rentalDate
1鬼滅の刃1巻吾峠呼世晴12023/1/1
1鬼滅の刃1巻吾峠呼世晴22023/2/1
1鬼滅の刃1巻吾峠呼世晴32023/3/1

なクエリ結果をbook.idで集計(文字通りgroupBy)してMapにまとめるような書き方になってます。

サンプル実装

-データベース