QueryDSL
정적 타입을 이용해서 SQL과 같은 쿼리를 생성할 수 있도록 해주는 오픈소스 프레임워크.
QueryDSL이 제공하는 Fluent API를 이용해 코드 작성의 형식으로 쿼리를 생성할 수 있게 도와줌.
사용이유
Spring Data JPA가 기본적으로 제공해주는 CRUD 메서드 및 쿼리 메서드 기능을 사용하더라도, 원하는 조건의 데이터를 수집하기 위해서는 필연적으로 JPQL을 작성하게 됨.
간단한 로직을 작성하는데 큰 문제는 없으나, 복잡한 로직의 경우 개행이 포함된 쿼리 문자열이 상당히 길어지게 되고, JPQL 문자열에 오타 혹은 문법적인 오류가 존재하는 경우 정적 쿼리가 아닐 시 런타임 시점에서 에러가 발생함.
위 같은 문제를 해소하기 위해 QueryDSL을 사용.
QueryDSL 장점
- 문자가 아닌 코드로 쿼리를 작성함으로써, 컴파일 시점에 문법 오류를 쉽게 확인할 수 있다.
- 자동 완성 등 IDE의 도움을 받을 수 있다.
- 동적인 쿼리 작성이 편리하다.
- 쿼리 작성 시 제약 조건 등을 메서드 추출을 통해 재사용할 수 있다.
설정(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 |