[JPA] QueryDSL 간단 (with Gradle, VSCode)
Goals
- QueryDSL 개요
- VSCode에서 Gradle로 QueryDSL 설정 방법
- 조회 방법
- 동적 쿼리를 위한 BooleanBuilder
QueryDSL 개요
보통 JPQL builder로써 JPA Criteria와 QueryDSL이 자주 비교된다. Criteria는 문자가 아닌 코드로 JPQL을 작성하므로 문법 오류를 컴파일 단계에서 잡을 수 있고, IDE의 자동완성 기능의 도움도 받을 수 있다. 하지만 그 구조가 너무 복잡하고 어렵다. 그에 반해 Criteria와 비슷한 장점을 지니면서도 쉽고 간결하며, 그 모양도 쿼리와 비슷하게 개발할 수 있는 프로젝트가 QueryDSL이다.
아래는 Criteria를 사용해서, searchCriteria
라는 검색 조건에 따라 두 테이블을 조인하는 동적 쿼리를 생성하는 코드다.
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Employee> query = cb.createQuery(Employee.class);
Root<Employee> emp = query.from(Employee.class);
Join<Employee, Department> dept = emp.join(Employee_.department);
List<Predicate> predicates = new ArrayList<>();
if (searchCriteria.getFirstName() != null) {
predicates.add(cb.equal(emp.get(Employee_.firstName), searchCriteria.getFirstName()));
}
if (searchCriteria.getLastName() != null) {
predicates.add(cb.equal(emp.get(Employee_.lastName), searchCriteria.getLastName()));
}
if (searchCriteria.getDepartmentName() != null) {
predicates.add(cb.equal(dept.get(Department_.name), searchCriteria.getDepartmentName()));
}
query.where(predicates.toArray(new Predicate[0]));
TypedQuery<Employee> typedQuery = em.createQuery(query);
List<Employee> results = typedQuery.getResultList();
위 코드를 QueryDSL로 바꿔 보겠다.
JPAQueryFactory queryFactory = new JPAQueryFactory(em);
QEmployee emp = QEmployee.employee;
QDepartment dept = QDepartment.department;
JPAQuery<Employee> query = queryFactory.selectFrom(emp)
.leftJoin(emp.department, dept);
if (searchCriteria.getFirstName() != null) {
query.where(emp.firstName.eq(searchCriteria.getFirstName()));
}
if (searchCriteria.getLastName() != null) {
query.where(emp.lastName.eq(searchCriteria.getLastName()));
}
if (searchCriteria.getDepartmentName() != null) {
query.where(dept.name.eq(searchCriteria.getDepartmentName()));
}
List<Employee> results = query.fetch();
차이를 느낄 수 있을 것이다. Criteria보다 QueryDSL을 사용한 코드가 훨씬 직관적이고 깔끔해 보인다. QueryDSL은 코드 자체가 쿼리를 표현하기 때문이다. 따라서 QueryDSL은 JPA Criteria에 비해 코드 가독성과 유지 보수성이 높다.
VSCode에서 Gradle로 QueryDSL 설정 방법
- Spring Boot 3.0.5
- JDK 17
- QueryDSL 5.0.0
build.gradle
buildscript {
ext {
queryDslVersion = "5.0.0"
}
}
plugins {
id 'java'
id 'war'
id 'org.springframework.boot' version '3.0.5'
id 'io.spring.dependency-management' version '1.1.0'
}
group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '17'
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-web'
// QeueryDsl
implementation "com.querydsl:querydsl-jpa:${queryDslVersion}:jakarta"
annotationProcessor "com.querydsl:querydsl-apt:${queryDslVersion}:jakarta"
annotationProcessor "jakarta.annotation:jakarta.annotation-api"
annotationProcessor "jakarta.persistence:jakarta.persistence-api"
// lombok
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
runtimeOnly 'mysql:mysql-connector-java:8.0.32'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
providedRuntime 'org.springframework.boot:spring-boot-starter-tomcat'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
tasks.named('test') {
useJUnitPlatform()
}
// querydsl에서 사용할 경로 설정
def querydslDir = "$buildDir/generated/querydsl"
// JPA 사용 여부와 사용할 경로를 설정
querydsl {
jpa = true
querydslSourcesDir = querydslDir
}
// build 시 사용할 sourceSet 추가
sourceSets {
main.java.srcDir querydslDir
}
// querydsl이 compileClassPath 를 상속하도록 설정
configurations {
querydsl.extendsFrom compileClasspath
}
// querydsl 컴파일시 사용할 옵션 설정
compileQuerydsl{
options.annotationProcessorPath = configurations.querydsl
}
- querydsl-jpa : QueryDSL JPA 라이브러리
- querydsl-apt : 쿼리 타입(Q)을 생성할 때 필요한 라이브러리
설정이 완료됐으면 쿼리 타입(Q) 클래스를 생성해 보겠다. 프로젝트 루트 경로에서 아래의 명령어를 입력한다.
- 빌드 명렁어 :
./gradlew compileQuerydsl
BUILD SUCCESSFUL
이란 메세지가 뜨면 성공적으로 쿼리 타입(Q) 클래스가 빌드된 것이다.
build/ 폴더에서 직접 확인해 볼 수 있다.
+ 추가로 아래와 같은 에러가 뜰 수 있다.
Deprecated Gradle features were used in this build, making it incompatible with Gradle 8.0.
이는 Spring Boot와 QueryDSL의 버전 차이 때문에 설정들이 조금씩 달라서 생기는 문제다. 아래의 글들을 참고해서 해결하길 바란다.
- https://codingjalhaja.com/springboot/vscode-querydsl-setting/
- https://velog.io/@juhyeon1114/Spring-QueryDsl-gradle-설정-Spring-boot-3.0-이상
- http://honeymon.io/tech/2020/07/09/gradle-annotation-processor-with-querydsl.html
조회 방법
먼저 QueryDSL의 간단한 사용 예시 코드를 보겠다.
@Test
public void queryDSL() {
JPAQuery<Guestbook> query = new JPAQuery<>(em);
QGuestbook guestbook = QGuestbook.guestbook;
List<Guestbook> guestbooks = query.from(guestbook)
.where(guestbook.writer.eq("user1"))
.orderBy(guestbook.title.desc())
.fetch();
System.out.println(guestbooks);
}
QueryDSL을 사용하기 위해서는 먼저 2가지 사용 조건이 필요하다.
com.querydsl.jpa.impl.JPAQuery
객체 생성을 위해EntityManager
를 생성자에 넘겨줌- 쿼리 타입(Q) 생성(기본 인스턴스)
이것 만으로 쿼리를 Java 코드로 아주 쉽게 작성할 수 있게 됐다. 위 코드만 봐도 어떤 JPQL이 생성될 지 예측이 가능할 것이다.
Hibernate가 생성할 JPQL
검색 조건 쿼리
com.querydsl.jpa.impl.JPAQuery
는 다양한 클래스를 상속받고 있다. 어떤 메서드들을 지원하는 지 확인하고 싶다면 공식 문서 API를 참고하면 되지만, 간단하게 생각해서 우리가 알고 있는 대부분의 SQL문은 메서드로 사용할 수 있다.
검색 조건 쿼리를 코드로 작성할 때 다음과 같은 순서를 따르면 좋다.
from
: 쿼리 소스를 추가한다. 첫 번째 인자는 메인 소스가 되고, 나머지는 변수로 취급한다.where
: 쿼리 필터를 추가한다. 가변 인자나and
/or
메서드를 이용해서 필터를 추가한다.groupBy
: 가변인자 형식의 인자를 기준으로 그룹을 추가한다.having
:Predicate
표현식을 이용해서 "group by" 그룹핑의 필터를 추가한다.orderBy
: 정렬 표현식을 이용해서 정렬 순서를 지정한다. 숫자나 문자열에 대해서는asc()
나desc()
를 사용.limit
,offset
,restrict
: 결과의 페이징을 설정한다. limit은 최대 결과 개수, offset은 결과의 시작 행, restrict는 limit과 offset을 함께 정의한다.
(출처 : http://querydsl.com/static/querydsl/3.6.3/reference/ko-KR/html_single/#d0e532)
위 예시를 다시 보자.
List<Guestbook> guestbooks = query.from(guestbook)
.where(guestbook.writer.eq("user1"))
.orderBy(guestbook.title.desc())
.fetch();
writer
값이 “user1”title
컬럼을 기준으로 내림차순 정렬
만약, 여러 조건을 넣고 싶으면 where
안에 and
나 or
을 사용할 수 있다.
List<Guestbook> guestbooks = query.from(guestbook)
.where(guestbook.writer.eq("user1").and(guestbook.gno.gt(100)))
.orderBy(guestbook.title.desc())
.fetch();
참고로 where()
안에는 com.querydsl.core.types.Predicate
인터페이스 타입을 파라미터 값으로 넣어줘야 한다.
간단히 말하면 Boolean 결과 값을 반환하는 Expression이다.
결과 조회
com.mysema.query.Projectable
에 정의되어 있는 결과 조회 API
fetch()
: 조회 대상이 여러 건일 때List
반환fetchOne()
: 조회 대상이 1건일 경우(1건 이상일 경우 에러). 없으면null
반환fetchFirst()
: 조회 대상이 1건이든 1건 이상이든 무조건 첫 번째 1건만 반환. 없으면null
반환fetchResults()
: 전체 데이터 조회를 위한count
쿼리를 한 번 더 실행하여 전체 데이터 수를 같이 조회
(참고 : http://querydsl.com/static/querydsl/5.0.0/apidocs/)
( + fetchResults()
를 사용하면 안되는 이유 : https://velog.io/@nestour95/QueryDsl-fetchResults가-deprecated-된-이유)
페이징과 정렬
정렬은 orderBy()
를 사용하고, 파라미터 값으로 쿼리 타입이 제공하는 asc()
, desc()
를 정렬 조건 필드에 붙여서 넘겨주면 된다.
페이징은 offset()
과 limit()
을 함께 사용한다.
List<Guestbook> guestbooks = query.from(guestbook)
.where(guestbook.writer.eq("user1").and(guestbook.gno.gt(100)))
.orderBy(guestbook.title.desc())
.limit(5)
.offset(5)
.fetch();
guestbooks.forEach((g) -> System.out.println(g));
위의 경우 한 페이지에 5개의 행을 가져오는데, 그 중 페이지 번호가 5인 데이터를 가져온다.
동적 쿼리를 위한 BooleanBuilder
com.querydsl.core.BooleanBuilder
를 사용하면 특정 조건에 따른 동적 쿼리를 편리하게 생성할 수 있다.
예를 들어 검색 조건에 따라 쿼리가 달라지는 경우를 생각해 보자. 사용자는 제목 만으로 검색을 할 수도 있고, 제목과 내용을 함께 검색할 수도 있다.
@Test
public void booleanBuilderTest() {
String type = "tc"; // 검색 조건
String keyword = "test"; // 검색 키워드
JPAQuery<Guestbook> query = new JPAQuery<>(em);
QGuestbook guestbook = QGuestbook.guestbook;
BooleanBuilder conditionBuilder = new BooleanBuilder();
// 제목 포함
if(type.contains("t")) {
conditionBuilder.and(guestbook.title.contains(keyword));
}
// 내용 포함
if(type.contains("c")) {
conditionBuilder.and(guestbook.content.contains(keyword));
}
// 작성자 포함
if(type.contains("w")) {
conditionBuilder.and(guestbook.writer.contains(keyword));
}
List<Guestbook> guestbooks = query.from(guestbook)
.where(conditionBuilder)
.orderBy(guestbook.title.desc())
.fetch();
guestbooks.forEach((g) -> System.out.println(g));
}
생성된 JPQL을 보면, 검색 조건으로 title
과 content
가 특정 문자열을 포함하도록 했고, 이를 and
연산자로 묶었다.
References
- 자바 ORM 표준 JPA 프로그래밍 - 김영한
- 코드로 배우는 스프링 부트 웹 프로젝트 - 구멍가게 코딩단
- QueryDSL 홈페이지
- QueryDSL 4.1.3 API DOCS
- QueryDSL 5.0.0 API DOCS