티스토리 뷰

JPA

Querydsl 스터디

크리드보이

1. 참고 페이지

Querydsl 공식 사이트

Querydsl - Unified Queries for Java

Querydsl Reference Guide

Querydsl Reference Guide

Querydsl 5.0.0 API

Querydsl 5.0.0 API

Querydsl 관련 소스

GitHub - lcalmsky/querydsl

 

 

2. Querydsl 특징

(1) Querydsl의 특징

Spring Data JPA가 기본적으로 제공해주는 CRUD 메서드 및 쿼리 메서드 기능을 사용하더라도, 원하는 조건의 데이터를 수집하기 위해서는 필연적으로 JPQL을 작성하게 됩니다. 간단한 로직을 작성하는데 큰 문제는 없으나, 복잡한 로직의 경우 개행이 포함된 쿼리 문자열이 상당히 길어집니다.

JPQL 문자열에 오타 혹은 문법적인 오류가 존재하는 경우, 정적 쿼리라면 어플리케이션 로딩 시점에 이를 발견할 수 있으나 그 외는 런타임 시점에서 에러가 발생합니다.

이러한 문제를 어느 정도 해소하는데 기여하는 프레임워크가 바로 QueryDSL입니다. QueryDSL은 정적 타입을 이용해서 SQL 등의 쿼리를 생성해주는 오픈소스 프레임워크입니다.

(2) QueryDSL의 장점

  1. 문자가 아닌 코드로 쿼리를 작성함으로써, 컴파일 시점에 문법 오류를 쉽게 확인할 수 있다.
  2. 자동 완성 등 IDE의 도움을 받을 수 있다.
  3. 동적인 쿼리 작성이 편리하다.
    1. 물론 한계가 있어서 통계성 쿼리 등은 natvie 쿼리 등으로 해결해야한다.
  4. 쿼리 작성 시 제약 조건 등을 메서드 추출을 통해 재사용할 수 있다.

(3) Querydsl 작동 방식

  • 기본적으로 QueryDSL은 프로젝트 내의 @Entity 어노테이션을 선언한 클래스를 탐색하고, JPAAnnotationProcessor를 사용해 Q 클래스를 생성합니다.
  • querydsl-apt가 @Entity 및 @Id 등의 애너테이션을 알 수 있도록, javax.persistence과 javax.annotation을 annotationProcessor에 함께 추가합니다.
    • annotationProcessor는 Java 컴파일러 플러그인으로서, 컴파일 단계에서 어노테이션을 분석 및 처리함으로써 추가적인 파일을 생성할 수 있습니다.
  • build 폴더는 gradle에 의해 gitignore 처리된다.
    • Qfile 등은 시스템이 자동으로 만들어주는 형태이고, 버전마다 조금씩 달라질 수 있기 때문에 git으로 형상관리를 하지는 않는다.
    • 혹시나 src/main/generated에 들어가게 된다면, 해당 파일들은 gitignore 해줘야함도 기억하자

 

(4) Querydsl 관련 라이브러리

  • apt 라는 부분이 코드 생성과 관련된 라이브러리라고 한다.
  • QHello 등의 객체를 만들어주는 역할을 한다.
  • core, jpa 부분은 실제 querydsl의 코드를 작동하게 해주는 selectFrom.fetchOne 등의 작동을 담당하는 라이브러리

 

(5) 기타

  • Querydsl fetchResults(), fetchCount() Deprecated(향후 미지원)
    • Querydsl은 향후 fetchCount() , fetchResult() 를 지원하지 않기로 결정했다.

 

3. Querydsl 설정 방법

공식 문서에는 Gradle에 대한 내용이 누락되어 있으며, 실제로 QueryDSL 설정 방법은 Gradle 및 IntelliJ 버전에 따라 상이

(1) gradle.build

**// 1. queryDsl version 정보 추가**
buildscript {
    ext {
        **queryDslVersion = "5.0.0"**
    }
}

plugins {
    id 'org.springframework.boot' version '2.6.3'
    id 'io.spring.dependency-management' version '1.0.11.RELEASE'
    **// 2. querydsl plugins 추가
    id "com.ewerk.gradle.plugins.querydsl" version "1.0.10"**
    id 'java'
}

//...

dependencies {
    **// 3. querydsl library dependencies 추가
    implementation "com.querydsl:querydsl-jpa:${queryDslVersion}"
    implementation "com.querydsl:querydsl-apt:${queryDslVersion}"**
    //...
}

test {
    useJUnitPlatform()
}

**/*
 * queryDSL 설정 추가
 */
// querydsl에서 사용할 경로 설정
def querydslDir = "$buildDir/generated/querydsl"

// JPA 사용 여부와 사용할 경로를 설정
querydsl {
    jpa = true
    querydslSourcesDir = querydslDir
}

// build 시 사용할 sourceSet 추가
// IDE의 소스 폴더에 자동으로 넣어준다.
// 개발 환경에서 생성된 Q파일들을 사용할 수 있도록 generated 디렉토리를 sourceSet에 추가해주면 개발 코드에서 생성된 Q파일에 접근할 수 있습니다.
sourceSets {
    main.java.srcDir querydslDir
}

// querydsl 컴파일시 사용할 옵션 설정
// Q파일을 생성해준다.
compileQuerydsl{
    options.annotationProcessorPath = configurations.querydsl
}

// querydsl 이 compileClassPath 를 상속하도록 설정
// 컴파일이 될때 같이 수행
configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
    querydsl.extendsFrom compileClasspath
}**

 

(2) compileQuerydsl 실행

Gradle Tasks -> compileQuerydsl 을 실행

 

또는 명령어를 이용하여 Querydsl query type 생성

./gradlew clean compileQuerydsl

 

 

(3) Querydsl Build 결과 확인

 

  • BUILD SUCCESSFUL 을 확인하였다면 build/generated/querydsl 경로에 Project Entity 들의 QClass 가 생성된 것을 확인할 수 있다.
  • 프로젝트 하위 디렉토리 중 build/generated/querydsl 여기 진입하면 아까 생성한 Item Entity 가 QItem 으로 변해있는 것을 확인할 수 있습니다.
  • $projectDir/build/generated 디렉토리 하위에 Entity로 등록한 클래스들이 Q라는 접두사가 붙은 형태로 생성되었습니다.
  • 이러한 클래스들을 Q 클래스 혹은 Q(쿼리) 타입이라고 합니다.
    • QueryDSL로 쿼리를 작성할 때, Q 클래스를 사용함으로써 쿼리를 Type-Safe하게 작성할 수 있습니다.
  • git으로 소스 코드를 관리할 땐 반드시 해당 경로를 무시하도록 처리해주셔야 합니다.
  • QItem.java 파일을 열어보면
package io.lcalmsky.querydsl.domain;

import static com.querydsl.core.types.PathMetadataFactory.*;

import com.querydsl.core.types.dsl.*;

import com.querydsl.core.types.PathMetadata;
import javax.annotation.Generated;
import com.querydsl.core.types.Path;

@Generated("com.querydsl.codegen.EntitySerializer")
public class QItem extends EntityPathBase<Item> {

    private static final long serialVersionUID = 1540314452L;

    public static final QItem item = new QItem("item");

    public final NumberPath<Long> id = createNumber("id", Long.class);

    public QItem(String variable) {
        super(Item.class, forVariable(variable));
    }

    public QItem(Path<? extends Item> path) {
        super(path.getType(), path.getMetadata());
    }

    public QItem(PathMetadata metadata) {
        super(Item.class, metadata);
    }
}

 

 

(4) Querydsl 정상 동작 테스트

그럼 Querydsl 을 이용해 정상적으로 쿼리를 수행하는지 확인해보겠습니다.

package io.lcalmsky.querydsl.domain;

import com.querydsl.jpa.impl.JPAQueryFactory;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import javax.persistence.EntityManager;
import org.springframework.transaction.annotation.Transactional;;

import static org.junit.jupiter.api.Assertions.assertEquals;

@SpringBootTest
@Transactional
class ItemTest {
    @Autowired
    EntityManager entityManager;

    @Test
    void test() {
        // given
        Item item = new Item();
        entityManager.persist(item);

        // when
        JPAQueryFactory queryFactory = new JPAQueryFactory(entityManager); // (1)
        QItem qItem = new QItem("i"); // (2)
        Item found = queryFactory.selectFrom(qItem).fetchOne(); // (3)

        // then
        assertEquals(found, item); // (4)
    }
}

(1) JPAQueryFactory를 생성합니다. 이 때 생성자로 EntityManager를 주입해줍니다.
(2) QItem 객체를 생성합니다. 생성자에는 Entity의 alias로 사용할 변수명을 입력합니다.
(3) JPQL을 작성하듯이 자바 코드로 쿼리를 작성합니다.
(4) DB에 저장된 데이터와 다시 조회해 온 데이터가 동일한지 확인합니다.

잘 동작했는지 확인하기 위해 아래 설정을 추가해줍니다.

H2 데이터베이스 가 실행되며 테이블을 직접 생성하고 포매팅된 SQL 로그를 확인할 수 있기 위함입니다.

 

spring:
  jpa:
    hibernate:
      ddl-auto: create
    properties:
      hibernate:
        format_sql: true
logging:
  level:
    org.hibernate.SQL: debug

 

테스트를 실행시킨 결과는 다음과 같습니다.

2021-07-15 19:53:33.992 DEBUG 4334 --- [           main] org.hibernate.SQL                        : 

    create table item (
       id bigint not null,
        primary key (id)
    )
// 생략
2021-07-15 19:53:35.898 DEBUG 4334 --- [           main] org.hibernate.SQL                        : 
    insert 
    into
        item
        (id) 
    values
        (?)
2021-07-15 19:53:35.906 DEBUG 4334 --- [           main] org.hibernate.SQL                        : 
    select
        item0_.id as id1_0_ 
    from
        item item0_

 

 

(5) 또 다른 예

[ Java 파일 구조 ]

 

[ 생성된 Q클래스 구조 ]

 

(6) p6spy 추가

sql문의 파라미터 출력 및 기타 기능들을 위해서 p6spy 라이브러리를 추가한다.

implementation 'com.github.gavlyukovskiy:p6spy-spring-boot-starter:1.5.8'

 

 

4. Querydsl 예제

@DisplayName("hi 내용을 포함하며 댓글이 1개 이상인 Post를 ID 내림차순으로 조회한다.")
@Test
void queryDsl_findPostsByMyCriteria_Three() {
    EntityManager entityManager = testEntityManager.getEntityManager();

    JPAQuery<Post> query = new JPAQuery<>(entityManager);
    QPost qPost = new QPost("p");

    List<Post> posts = query.from(qPost)
        .where(qPost.content.contains("hi")
            .and(qPost.comments.size().gt(0))
        ).orderBy(qPost.id.desc())
        .fetch();

    assertThat(posts).hasSize(3);
}
  • QueryDSL은 각종 풍부한 체이닝 메서드와 유틸리티 메서드 및 정적 타입(Q 클래스)을 기반으로 직관적으로 쿼리를 작성
  • JPQL을 사용해본 독자님이라면 코드가 상당히 직관적임

 

5. Check List

(1) 기본 Q-Type 활용

[ Q클래스 인스턴스를 사용하는 2가지 방법 ]

QMember qMember = new QMember("m"); // 별칭 직접 지정
QMember qMember = QMember.member; // 기본 인스턴스 사용

하지만 기본 인스턴스를 static import와 함께 사용하는 것을 권장한다.

 

(2) 결과 조회

  • fetch : 리스트 조회, 데이터 없으면 빈 리스트 반환
  • fetchOne : 단 건 조회
    • 결과가 없으면 : null
    • 결과가 둘 이상이면 : com.querydsl.core.NonUniqueResultException
  • fetchFirst : limit(1).fetchOne()
  • fetchResults : 페이징 정보 포함, total count 쿼리 추가 실행
    • group by having 카운팅에서 해당 메소드가 명확하게 동작하지 않는 이슈가 발생하여 deprecated 처리
  • fetchCount : count 쿼리로 변경해서 count 수 조회
    • group by having 카운팅에서 해당 메소드가 명확하게 동작하지 않는 이슈가 발생하여 deprecated 처리

(3) 페이징

  • 실무에서 페이징 쿼리를 작성할 때, 데이터를 조회하는 쿼리는 여러 테이블을 조인해야 하지만, count 쿼리는 조인이 필요 없는 경우도 있다.
  • 그런데 이렇게 자동화된 count 쿼리는 원본 쿼리와 같이 모두 조인을 해버리기 때문에 성능이 안나올 수 있다.
  • count 쿼리에 조인이 필요없는 성능 최적화가 필요하다면, count 전용 쿼리를 별도로 작성해야 한다.
댓글
Total
최근에 올라온 글