무슨 이유에서인지 QueryDSL 의 쿼리 결과를 list() 로도, unique() 로도 반환할 수 없다. 원인을 파악해야 한다.

QueryDSL

QueryDSL 환경 설정을 위해 build.gradle.kts 파일에 설정을 추가한다. 추가하는 설정은 이 글 맨 아래에서 확인할 수 있다.

설정 파일

persistence.xml

<?xml version="1.0" encoding="UTF-8"?>
<!-- xml 네임스페이스와 사용할 버전을 지정한다. -->
<!-- jpa 2.1 을 사용하려면 여기에 xmlns 와 2.1 을 쓰면 된다. -->
<persistence xmlns="http://xmlns.jcp.org/xml/ns/persistence" version="2.2">

    <!-- 영속성 유닛. 일반적으로 연결할 데이터베이스당 하나의 영속성 유닛을 등록한다. -->
    <!-- 그리고 영속성 이름에는 고유한 이름을 부여해야 한다. 여기서는 jpabook. -->
    <persistence-unit name="jpabook">
        <!-- @Entity 애너테이션이 들어가는 클래스 목록 -->
        <class>com.wisdoom.Member</class>
        <class>com.wisdoom.Team</class>
        <properties>

            <!-- 필수 속성 -->
            <!-- name 이 javax.persistence 로 시작하는 속성은 JPA 표준 속성으로 특정 구현체에 종속되지 않는다. -->
            <property name="javax.persistence.jdbc.driver" value="org.h2.Driver"/>
            <property name="javax.persistence.jdbc.user" value="sa"/>
            <property name="javax.persistence.jdbc.password" value=""/>
            <property name="javax.persistence.jdbc.url" value="jdbc:h2:tcp://localhost/~/test"/>
            <!-- 특정 데이터베이스를 활용하기 위해 사용. -->
            <property name="hibernate.dialect" value="org.hibernate.dialect.H2Dialect"/>

            <!-- 옵션 -->
            <!-- DDL 을 콘솔에 출력한다. -->
            <property name="hibernate.show_sql" value="true"/>
            <!-- 애플리케이션 실행 시점에 데이터베이스 테이블을 자동으로 생성한다.
            create: 기존 테이블 삭제 후 생성; create-drop: create 속성과 동일하나, create 한 내용을 애플리케이션 종료 시에 DROP
            update: 데이터베이스 테이블과 엔티티 매핑 정보를 비교해 변경 사항만 수정
            validate: 데이터베이스 테이블과 엔티티 매핑 저보를 비교했을 때 차이가 있으면 경고를 남기고 애플리케이션을 실행하지 않는다.
            -->
<!--             <property name="hibernate.hbm2ddl.auto" value="create"/>-->

            <!--???? 기본 키 생성 전략을 사용하기 위한 설정 ????-->
            <property name="hibernate.id.new_generator_mappings" value="true"/>

            <!-- 실제로 실행되는 sql query 를 다이얼로그에 출력한다 true. -->
            <property name="hibernate.use_sql_comments" value="true"/>
            <!-- 실제로 실행되는 sql query 를 사람이 보기 좋게 출력한다 true. 보기 좋게 출력하지 않는다 false. -->
            <property name="hibernate.format_sql" value="false"/>

            <!--<property name="hibernate.hbm2ddl.auto" value="create" />-->
        </properties>
    </persistence-unit>

</persistence>

build.gradle.kts

import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
    val kotlinVersion = "1.4.32"
    kotlin("jvm") version kotlinVersion
    kotlin("plugin.allopen") version kotlinVersion
    kotlin("plugin.jpa") version kotlinVersion
    kotlin("kapt") version kotlinVersion
}

allOpen {
    annotation("javax.persistence.Entity")
}

group = "me.wisdoom"
version = "1.0-SNAPSHOT"

repositories {
    mavenCentral()
}

dependencies {
    // JPA, 하이버네이트
    implementation("org.hibernate:hibernate-entitymanager:5.4.32.Final")
    // h2 데이터베이스
    implementation("com.h2database:h2:1.4.200")

    // QueryDSL 환경 설정
    // JPAQuery class 사용 가능
    implementation("com.querydsl:querydsl-jpa:4.3.0")
    // Q Class 생성
    kapt("com.querydsl:querydsl-apt:4.3.0:jpa")

    testImplementation(kotlin("test-junit5"))
    testImplementation("org.junit.jupiter:junit-jupiter-api:5.6.0")
    testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.6.0")
}

tasks.test {
    useJUnitPlatform()
}

tasks.withType<KotlinCompile>() {
    kotlinOptions.jvmTarget = "1.8"
}

QueryDSL 사용 예제 - 쿼리 타입 직접 지정 (“m”)

fun main() {
    // QueryDSL
    val emf = Persistence.createEntityManagerFactory("jpabook")
    val em = emf.createEntityManager()
    val query = JPAQueryFactory(em)
    val qMember = QMember("m") // JPQL 별칭(identification variable 또는 alias)이 m
    val members = query.from(qMember)
        .where(qMember.name.eq("수호"))
        .orderBy(qMember.name.desc())
    println(members)
}

QueryDSL 사용 예제 - 기본 인스턴스 사용

fun main() {
    // QueryDSL
    val emf = Persistence.createEntityManagerFactory("jpabook")
    val em = emf.createEntityManager()
    val query = JPAQueryFactory(em)
    val members = query.from(member)
        .where(member.name.eq("수호"))
        .orderBy(member.name.desc())
    println(members)
}

위의 두 사용 예제의 결과는 같으며, 아래와 같이 JPQL 쿼리가 실행된다.

select m
from Member m
where m.name = ?1
order by m.name desc

QueryDSL 사용 예제 - 동적 쿼리 생성: BooleanBuilder

fun main() {
    // QueryDSL 동적 쿼리 생성 - BooleanBuilder
    val builder = BooleanBuilder()
    builder.and(QMember.member.name.eq("수호"))
        .and(QMember.member.id.goe(1))
    println(builder)
}
member1.name = 수호 && member1.id >= 1

QueryDSL 사용 예제 - 메소드 위임: QueryDelegate
아래와 같이 클래스를 하나 정의하고, static function 을 정의한다. 코틀린에서는 companion object 를 사용한다.

class MemberExpression {

    companion object {
        @QueryDelegate(Member::class)
        fun isHigh(member: QMember, par: Int): BooleanExpression = member.id.gt(1)
    }
}

위처럼 클래스와 함수를 정의한 후 build 를 하면 Q Class 에 아래와 같이 메서드가 생성된 것을 확인할 수 있다.

@Generated("com.querydsl.codegen.EntitySerializer")
public class QMember extends EntityPathBase<Member> {
    // 생략
    public BooleanExpression isHigh(Integer par) {
        return MemberExpression.Companion.isHigh(this, par);
    }
}