티스토리 뷰
1. 참고 페이지
Querydsl 공식 사이트
Querydsl - Unified Queries for Java
Querydsl Reference Guide
Querydsl 5.0.0 API
Querydsl 관련 소스
2. Querydsl 특징
(1) Querydsl의 특징
Spring Data JPA가 기본적으로 제공해주는 CRUD 메서드 및 쿼리 메서드 기능을 사용하더라도, 원하는 조건의 데이터를 수집하기 위해서는 필연적으로 JPQL을 작성하게 됩니다. 간단한 로직을 작성하는데 큰 문제는 없으나, 복잡한 로직의 경우 개행이 포함된 쿼리 문자열이 상당히 길어집니다.
JPQL 문자열에 오타 혹은 문법적인 오류가 존재하는 경우, 정적 쿼리라면 어플리케이션 로딩 시점에 이를 발견할 수 있으나 그 외는 런타임 시점에서 에러가 발생합니다.
이러한 문제를 어느 정도 해소하는데 기여하는 프레임워크가 바로 QueryDSL입니다. QueryDSL은 정적 타입을 이용해서 SQL 등의 쿼리를 생성해주는 오픈소스 프레임워크입니다.
(2) QueryDSL의 장점
- 문자가 아닌 코드로 쿼리를 작성함으로써, 컴파일 시점에 문법 오류를 쉽게 확인할 수 있다.
- 자동 완성 등 IDE의 도움을 받을 수 있다.
- 동적인 쿼리 작성이 편리하다.
- 물론 한계가 있어서 통계성 쿼리 등은 natvie 쿼리 등으로 해결해야한다.
- 쿼리 작성 시 제약 조건 등을 메서드 추출을 통해 재사용할 수 있다.
(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