HIRO Tracks

ソフトウェアエンジニアが日々学んだ知識を発信します。

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

これは何?

hiro-tracks.net

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

本記事では、上記記事の「必要なカラムだけ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を使う

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.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にまとめるような書き方になってます。

サンプル実装

[https://github.com/kannna5296/querydsl-sample/blob/queryDSL-sample/infra/src/main/kotlin/com/sample/infra/query/SampleQueryService.kt#L24-L33:embed:cite]