본문 바로가기

항해 99/Spring

QueryDSL

QueryDSL - 래퍼런스 문서

 

QueryDSL

정적 타입을 이용해서 SQL과 같은 쿼리를 생성할 수 있도록 해주는 오픈소스 프레임워크.

QueryDSL이 제공하는 Fluent API를 이용해 코드 작성의 형식으로 쿼리를 생성할 수 있게 도와줌.

 

사용이유

Spring Data JPA가 기본적으로 제공해주는 CRUD 메서드 및 쿼리 메서드 기능을 사용하더라도, 원하는 조건의 데이터를 수집하기 위해서는 필연적으로 JPQL을 작성하게 됨.

 

 간단한 로직을 작성하는데 큰 문제는 없으나, 복잡한 로직의 경우 개행이 포함된 쿼리 문자열이 상당히 길어지게 되고, JPQL 문자열에 오타 혹은 문법적인 오류가 존재하는 경우 정적 쿼리가 아닐 시 런타임 시점에서 에러가 발생함.

 

위 같은 문제를 해소하기 위해 QueryDSL을 사용.

 

QueryDSL 장점

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

 

설정(Gradle)

plugins {
    id 'java'
    id 'org.springframework.boot' version '3.2.3'
    id 'io.spring.dependency-management' version '1.1.4'
}

group = 'com.sparta'
version = '0.0.1-SNAPSHOT'

java {
    sourceCompatibility = '17'
}

configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}

repositories {
    mavenCentral()
}

dependencies {
    // Query DSL
    implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
    annotationProcessor "com.querydsl:querydsl-apt:5.0.0:jakarta"
    annotationProcessor "jakarta.annotation:jakarta.annotation-api"
    annotationProcessor "jakarta.persistence:jakarta.persistence-api"

    // JUnit Jupiter API
    testImplementation 'org.junit.jupiter:junit-jupiter-api:5.7.0'
    // JUnit Jupiter Engine
    testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.7.0'
    // JUnit Jupiter Params (선택적)
    testImplementation 'org.junit.jupiter:junit-jupiter-params:5.7.0'
    // Mockito Core
    testImplementation 'org.mockito:mockito-core:3.6.0'
    // Mockito JUnit Jupiter
    testImplementation 'org.mockito:mockito-junit-jupiter:3.6.0'
    implementation 'io.jsonwebtoken:jjwt-api:0.11.2'
    runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.2'
    runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.2' // JSON Processor

    // JWT
    compileOnly group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.5'
    runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.5'
    runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.5'
    // json
    implementation 'org.json:json:20231013'

    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-security'
    implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
    implementation 'org.springframework.boot:spring-boot-starter-validation'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6'
    compileOnly 'org.projectlombok:lombok'
    runtimeOnly 'com.mysql:mysql-connector-j'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testImplementation 'org.springframework.security:spring-security-test'
}

tasks.named('test') {
    useJUnitPlatform()
}

//QueryDSL 플로그인 설정 START

//def generated = layout.buildDirectory.dir("generated/querydsl").get().asFile

def generatedDir =  "src/main/generated"


sourceSets {
    main {
        java {
            srcDirs = ['src/main/java']
        }
        // Mybatis 쿼리 xml 위치때문에 조정
        resources{
            srcDir "${project.projectDir}/src/main/java"
        }
    }
}

clean {
    delete file(generatedDir)
}

 

build.gradle에 추가해야 되는 항목

implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
annotationProcessor "com.querydsl:querydsl-apt:5.0.0:jakarta"
annotationProcessor "jakarta.annotation:jakarta.annotation-api"
annotationProcessor "jakarta.persistence:jakarta.persistence-api"

 

 

EntityManager 주입받기

QueryDslConfig

// QueryDsl 설정
@Configuration
public class QuerydslConfig {

    @PersistenceContext
    private EntityManager entityManager;

    @Bean
    public JPAQueryFactory jpaQueryFactory(){
        return new JPAQueryFactory(entityManager);
    }
}
  • JPAQueryFactory를 Bean으로 등록해 프로젝트 전역에서 QueryDSL을 작성할 수 있도록 설정

 

Repository / RepositoryCustom 생성

public interface RoomRepository extends JpaRepository<Room, Long>, RoomRepositoryCustom {

}
  • JpaRepository를 상속하는 기존 Repository가 CustomRepository 인터페이스를 상속하도록 설정
public interface RoomRepositoryCustom {

    Page<RoomOfList> searchAll(FieldsOfRoomList fieldsOfRoomList, Pageable pageable);
}
  • 기존의 Repository 인터페이스의 쿼리 메서드를 삭제하고, 동일한 메서드 시그니처를 새로운 커스텀 인터페이스에 정의

 

RepositoryImpl

QueryDsl에 대한 로직 작성

예제 코드

@RequiredArgsConstructor
public class RoomRepositoryImpl implements RoomRepositoryCustom{

    private final JPAQueryFactory queryFactory;
    
    @Override
    public Page<RoomOfList> searchAll(FieldsOfRoomList fieldsOfRoomList, Pageable pageable) {

        List<Room> result = queryFactory
                .selectFrom(room)
                .leftJoin(room.tags, tag1).fetchJoin()
                .where(eqInterests(
                    fieldsOfRoomList.getExercise()),
                    eqArea(fieldsOfRoomList.getArea()),
                    participateCount(fieldsOfRoomList.getParticipantCount(), fieldsOfRoomList.isContainNoAdmittance()),
                    betweenTime(fieldsOfRoomList.getStartAppointmentDate(), fieldsOfRoomList.getEndAppointmentDate(), fieldsOfRoomList.isContainTimeClosing()),
                    eqTitle(fieldsOfRoomList.getRoomTitle()),
                    eqContent(fieldsOfRoomList.getRoomContent())
                )
                .offset(pageable.getOffset())
                .limit(pageable.getPageSize())
                .orderBy(sortRoomList(pageable))
                .fetch();


        return afterTreatment(result);

    }

}
  • 커스텀 인터페이스를 구현하는 클래스에 QueryDSL 쿼리를 작성(구현 클래스의 이름은 Impl로 끝나야 함)

 

기타

@Repository
public class QueryRepository {

    private final JPAQueryFactory jpaQueryFactory;

    public QueryRepository(JPAQueryFactory jpaQueryFactory) {
        this.jpaQueryFactory = jpaQueryFactory;
    }

    public List<Post> findAllPostsInnerFetchJoin() {
        return jpaQueryFactory.selectFrom(post)
            .innerJoin(post.comments)
            .fetchJoin()
            .fetch();
    }

    public List<Orphan> findALlOrphans() {
        return jpaQueryFactory.selectFrom(orphan)
            .distinct()
            .leftJoin(orphan.parent).fetchJoin()
            .where(orphan.name.contains("abc"))
            .fetch();
    }
}
  • 특정 엔티티 타입에 구애받지 않는 자신만의 QueryDSL 관련 Repository를 정의해 사용할 수 있음

 

코드에서 QueryDSL 역할

JPAQueryFactory

  • QueryDSL 쿼리를 생성하고 실행하는 데 사용되는 핵심 클래스, 이 인스턴스를 사용하여 데이터베이스 쿼리를 구성하고 실행

selectFrom(room)

  • 쿼리의 시작점을 정의, 'room' 엔티티에서 데이터를 선택(select)하기 위한 쿼리를 시작, room은 Qroom 인스턴스일 가능성이 높음(QueryDSL에서 생성된 엔티티의 메타 모델)

leftJoin(room.tags, tag1)

  • leftJoin은 왼쪽 조인을 수행하며, room.tags와 tag1 엔티티를 조인함

.fetchJoin

  • 조인된 엔티티의 데이터를 즉시 로딩하여 성능을 최적화함, N+1 쿼리 문제를 방지하는 데 유용함

where(...)

  • 쿼리의 조건을 정의함, 인자로 주어진 조건들에 따라 결과를 필터링함, 각 조건 메서드는 특정 필드에 대한 조건을 나타내며 BooleanExpression을 반환함, 이 조건들은 and 연산으로 결합 됨

offset(pageable.getOffset())

  • 페이징을 위해 결과의 시작점을 정의함, Pageable 인터페이스에서 제공하는 getOffset 메서드를 사용하여 몇 번째 row부터 데이터를 가져올지 결정

limit(pageable.getPageSize())

  • 한 페이지에 표시할 항목의 수를 제한함, Pageable 인터페이스에서 제공하는 getPageSize 메서드를 사용하여 최대 항목 수를 정의

orderBy(sortRoomList(pageable))

  • 결과의 정렬 순서를 정의함, sortRoomList 메서드는 Pageable 객체를 인자로 받아, 정렬 조건을 구성하고 OrderSpecifier 배열을 반환할 것으로 추정, 이 배열은 쿼리의 orderBy 메서드에 사용됨

fetch()

  • 최종적으로, fetch 메서드는 위에서 정의된 조건에 맞는 데이터를 데이터베이스에서 가져옴, 결과는 List<Room> 타입으로 반환

 

QueryDSL 핵심

QueryDSL에서 where(), Predicate, BooleanExpression, 그리고 BooleanBuilder는 쿼리를 동적으로 구성할 때 사용되는 핵심 개념과 도구, 이들을 사용함으로써 복잡한 조건을 타입 안전하게 구현하고, 유연하게 쿼리를 조합할 수 있음

 

1. where()

  • where() 메서드는 쿼리의 조건을 지정하는 데 사용
  • QueryDSL 쿼리 구성에서 select, from, join 등과 함께 사용되어 결과를 필터링함
  • where 메서드는 BooleanExpression을 인자로 받아, 해당 조건에 맞는 데이터만 검색 결과로 반환하도록 함

2. Predicate

  • Predicate는 Java 8에서 도입된 함수형 인터페이스 중 하나로, 어떤 값을 입력받아 boolean 값을 반환하는 함수를 의미
  • QueryDSL에서는 이 개념이 확장되어, 데이터베이스 쿼리의 조건을 표현하는 데 사용되는 타입으로 사용됨
  • Predicate는 조건의 참/거짓을 평가하여 쿼리의 where 조건 등에 활용됨

3. BooleanExpression

  • BooleanExpression은 QueryDSL에서 조건식을 표현하는 데 사용되는 인터페이스
  • Predicate와 유사하게 boolean 결과를 나타내지만, QueryDSL의 컨텍스트 내에서 다양한 조건 조합(and, or, not) 및 비교(eq, ne, lt, gt 등) 연산을 지원함
  • BooleanExpression은 쿼리의 where 조건, 조인 조건 등에 사용되어 데이터베이스 쿼리의 결과를 동적으로 필터링하는 데 필수적인 역할을 함

 

4. BooleanBuilder

  • BooleanBuilder는 BooleanExpression을 동적으로 구성하기 위한 유틸리티 클래스
  • 여러 조건을 프로그래매틱하게 조합할 필요가 있을 때 유용하게 사용
    • 예를 들어, 사용자의 입력에 따라 쿼리의 조건을 변경해야 하는 경우, BooleanBuilder를 사용하여 조건을 조합하고, 최종적으로 구성된 BooleanExpression을 where() 메서드에 전달할 수 있음
  •  BooleanBuilder는 and(), or() 메서드를 통해 여러 BooleanExpression을 결합할 수 있으며, 이를 통해 복잡한 조건 로직을 구현할 수 있음

이러한 도구들을 활용함으로써, QueryDSL을 사용하는 개발자는 데이터베이스 쿼리를 더 유연하고 안전하게 구성할 수 있으며, 복잡한 조건을 쉽게 관리할 수 있게 됨

 

QueryResults, fetchCount() deprecated

대안

1. 결과 가져오기: 실제 페이지에 필요한 데이터만 로드합니다.

List<MyEntity> results = queryFactory
    .selectFrom(myEntity)
    .where(...)
    .limit(pageable.getPageSize())
    .offset(pageable.getOffset())
    .fetch();

 

2. 전체 개수 계산: 전체 개수를 계산하기 위해 별도의 쿼리를 실행합니다. 이때, count() 집계 함수를 사용하면 됩니다.

long total = queryFactory
    .select(new QMyEntity(myEntity.count()))
    .from(myEntity)
    .where(...)
    .fetchOne();

 

페이지에 필요한 데이터만 로드하고, 전체 항목 수도 효율적으로 계산할 수 있음

 

No sources given error 해결

  • 원인 : QueryDSL의 Query에 from 절이 없는 경우 발생

'항해 99 > Spring' 카테고리의 다른 글

REST API URI 규칙  (0) 2024.03.13
ExceptionHandler  (0) 2024.03.11
Entity와 DTO의 분리  (0) 2024.03.08
lombok 주의 사항  (0) 2024.03.06
Controller, RestController 차이  (0) 2024.03.05