티스토리 뷰

728x90
반응형

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의 버전 차이 때문에 설정들이 조금씩 달라서 생기는 문제다. 아래의 글들을 참고해서 해결하길 바란다.

 

조회 방법


먼저 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: 가변인자 형식의 인자를 기준으로 그룹을 추가한다.
  • havingPredicate 표현식을 이용해서 "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 안에 andor을 사용할 수 있다.

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 인터페이스 타입을 파라미터 값으로 넣어줘야 한다.

http://querydsl.com/static/querydsl/4.1.3/apidocs/com/querydsl/core/support/QueryBase.html#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을 보면, 검색 조건으로 titlecontent가 특정 문자열을 포함하도록 했고, 이를 and 연산자로 묶었다.

 

References

728x90
반응형
댓글