복잡한 쿼리를 동작하기 위해서 여러 방법이 있다. 앞서 작성한 @Query 를 이용하는 방법, Specification을 사용하는 방법이 있다. 이 두 방식은 프로젝트가 복잡해지면 처리가 어렵다고 하며, QueryDSL을 사용해야 한다고 한다. 해당 방법은 C#의 linqQuery와 비슷하다.
기존 Jpa 를 이용하는 Repository에, QueryDsl을 이용하는 리파지토리를 추가하여 붙인다고 생각하면 된다.
- 전반적인 프로세스
- gradle 설정
- build 를 통해 Q클래스 생성
- JPAQueryFactory를 Bean으로 등록하기 위한 Configuration 클래스 생성
- QueryDSL을 사용하는 Repository interface 생성
- Repository Interface를 구현하는 Impl 클래스 생성
- JPAQueryFactory 를 이용해서 함수 작성
- Repository 어노테이션을 붙이지 않는다.
- Impl 이라는 명칭으로 클래스를 만들어야 자동 매칭 한다.
- 기존 Repository Interface 에서 QueryDSL을 사용하는 Repository interface 를 extent 를 통해 확장 (Jpa, QueryDsl 모두 확장)
- Service 에서 JpaRepository, QueryDSL 모두 상속받는 repository를 주입받아 이용
- gradle 설정
- 요약하자면, QueryDsl 용 인터페이스가 따로 있고, 이를 구현하는 구현체 클래스가 한 세트이다. 기존 Jpa 용 Repository 인터페이스에 QueryDsl 을 확장하여, Service 에서는 양쪽 모두 사용 할 수 있게 하는것이다.
- 설치
- build.gradle 의 dependencies 에 다음을 추가한다.
- 다만, 공식버전은 업데이트가 멈춰 있다. 추후 지원을 생각한다면, OpenFeign 에서 제공하는 querydsl 을 이용하는게 권장된다
implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
implementation "jakarta.persistence:jakarta.persistence-api"
annotationProcessor "com.querydsl:querydsl-apt:5.0.0:jakarta"
annotationProcessor "jakarta.annotation:jakarta.annotation-api"
annotationProcessor "jakarta.persistence:jakarta.persistence-api"
- 이후 clean 하고 나서 build를 하면 Q클래스들이 생성된다.
- 자동으로 @Entity 가 붙은 클래스를 탐색하여 해당 클래스와 연계되는 Q클래스를 생성한다
- 위치는 build -> generated -> sources -> AnnotationProcessor -> Java -> 패키지명 에 생성된다.
- 만일 entity 클래스 명이 TestEntity 라면 QTestEntity 라고 생성된다.
- JPAQueryFactory를 Bean으로 등록하기 위한 Configuration 클래스 생성
- EntityManager를 인젝션 받아야 하고, 이를 이용해 JPAQueryFactory 생성 후 다른곳에서 인젝션 받을 수 있게 Bean으로 등록한다.
package com.sprint.mission.querydsl_practice.config;
import com.querydsl.jpa.impl.JPAQueryFactory;
import jakarta.persistence.EntityManager;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@RequiredArgsConstructor
@Configuration
public class DslConfig {
private final EntityManager entityManager;
@Bean
JPAQueryFactory jpaQueryFactory() {
return new JPAQueryFactory(entityManager);
}
}
- QueryDSL을 사용하는 Repository interface 생성
package com.sprint.mission.querydsl_practice.repository;
import com.sprint.mission.querydsl_practice.dto.ExamRequest;
import com.sprint.mission.querydsl_practice.dto.ExamResponse;
public interface ExamRepositoryDsl {
ExamResponse findById(ExamRequest dto);
}
- Repository Interface를 구현하는 Impl 클래스 생성
- JPAQueryFactory 를 이용해 마치 linq 쿼리와 흡사한 방식으로 쿼리를 작성한다.
- 주의점은 Q클래스를 임포트 해야 한다는점을 잊지 말자 (즉, Q클래스가 생성되어 있어야 한다)
- 여기서는 QExamEntity
package com.sprint.mission.querydsl_practice.repository;
import com.querydsl.jpa.impl.JPAQueryFactory;
import com.sprint.mission.querydsl_practice.dto.ExamRequest;
import com.sprint.mission.querydsl_practice.dto.ExamResponse;
import com.sprint.mission.querydsl_practice.entity.ExamEntity;
import com.sprint.mission.querydsl_practice.entity.QExamEntity;
import java.time.Instant;
import java.util.List;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
public class ExamRepositoryImpl implements ExamRepositoryDsl {
private final JPAQueryFactory jpaQueryFactory;
private final QExamEntity qExamEntity = QExamEntity.examEntity;
@Override
public ExamResponse findById(ExamRequest dto) {
List<ExamEntity> list = jpaQueryFactory.selectFrom(qExamEntity)
.where(qExamEntity.createdAt.after(Instant.now()))
.fetch();
ExamResponse examResponse = ExamResponse.<ExamEntity>builder().content(list).build();
return examResponse;
}
}
아래는 조건문 등을 따로 별도의 메서드로 빼놓아, 동적 쿼리를 이용하는 방법의 예 이다.
조건이 많아 if 문으로 여러개 처리할 경우 BooleanBuilder를 리턴
public List<User> search(UserSearchCondition condition) {
return queryFactory
.selectFrom(user)
.where(userSearchCondition(condition))
.fetch();
}
/* ================= private methods ================= */
private BooleanBuilder userSearchCondition(UserSearchCondition condition) {
BooleanBuilder builder = new BooleanBuilder();
if (condition.getEmail() != null) {
builder.and(user.email.eq(condition.getEmail()));
}
if (condition.getActive() != null) {
builder.and(user.active.eq(condition.getActive()));
}
builder.and(notDeleted());
return builder;
}
private BooleanExpression notDeleted() {
return user.deleted.isFalse();
}
조건 여러개를 각각 조합하고 싶은 경우 BooleanExpression 이용
public List<User> findForAdmin(String email) {
return queryFactory
.selectFrom(user)
.where(
notDeleted(),
isActive(),
emailEq(email)
)
.fetch();
}
/* ================= private methods ================= */
private BooleanExpression notDeleted() {
return user.deleted.isFalse();
}
private BooleanExpression isActive() {
return user.active.isTrue();
}
private BooleanExpression emailEq(String email) {
return email == null ? null : user.email.eq(email);
}
- 조건문에 호출하는(?) 메소드 이름에 유의 해야 한다. 아래는 사용되는 메소드 정리이다 (GPT 복붙)
QueryDSL 메서드설명SQL 대응예제
| eq(value) | 같음 | = | qMember.age.eq(30) → age = 30 |
| ne(value) | 같지 않음 | != | qMember.name.ne("Alice") → name != ‘Alice’ |
| gt(value) | 큼 | > | qMember.age.gt(20) → age > 20 |
| goe(value) | 크거나 같음 | >= | qMember.age.goe(20) → age >= 20 |
| lt(value) | 작음 | < | qMember.age.lt(30) → age < 30 |
| loe(value) | 작거나 같음 | <= | qMember.age.loe(30) → age <= 30 |
| contains(str) | 포함 | LIKE ‘%str%’ | qMember.name.contains("Al") |
| containsIgnoreCase(str) | 대소문자 무시 포함 | ILIKE ‘%str%’ (DB별) | qMember.name.containsIgnoreCase("al") |
| startsWith(str) | 시작 | LIKE ‘str%’ | qMember.name.startsWith("A") |
| endsWith(str) | 끝 | LIKE ‘%str’ | qMember.name.endsWith("x") |
| isNull() | NULL | IS NULL | qMember.name.isNull() |
| isNotNull() | NOT NULL | IS NOT NULL | qMember.name.isNotNull() |
| isEmpty() | 비어있음 | 컬렉션 SIZE = 0 | qMember.roles.isEmpty() |
| isNotEmpty() | 비어있지 않음 | 컬렉션 SIZE > 0 | qMember.roles.isNotEmpty() |
| between(start, end) | 범위 | BETWEEN start AND end | qMember.age.between(20, 30) |
| after(date) | 이후 | > | qMember.birthDate.after(startDate) |
| before(date) | 이전 | < | qMember.birthDate.before(endDate) |
| and(other) | AND | AND | cond1.and(cond2) |
| or(other) | OR | OR | cond1.or(cond2) |
| not() | NOT | NOT | cond1.not() |
| contains(element) | 컬렉션 포함 | MEMBER OF 컬렉션 | qMember.roles.contains(role) |
| containsAll(elements) | 컬렉션 모두 포함 | 모든 요소 MEMBER OF 컬렉션 | qMember.roles.containsAll(list) |
- 기존 Repository Interface 에서 QueryDSL을 사용하는 Repository interface 를 extent 를 통해 확장
package com.sprint.mission.querydsl_practice.repository;
import com.sprint.mission.querydsl_practice.entity.ExamEntity;
import org.springframework.data.jpa.repository.JpaRepository;
public interface ExamRepository extends JpaRepository<ExamEntity,Long>, ExamRepositoryDsl {
}
- Service 에서는 Jpa와 QueryDsl을 모두 extend 하는 repository를 인젝션 받아 사용한다.
package com.sprint.mission.querydsl_practice.service;
import com.sprint.mission.querydsl_practice.dto.ExamRequest;
import com.sprint.mission.querydsl_practice.dto.ExamResponse;
import com.sprint.mission.querydsl_practice.repository.ExamRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class ExamService {
private final ExamRepository examRepository;
public ExamResponse findById(ExamRequest dto) {
return examRepository.findById(dto);
}
....
}
'Backend > SpringBoot' 카테고리의 다른 글
| 실행시 DB에 데이터 넣기 (1) | 2024.11.19 |
|---|---|
| AOP (함수 실행 intercept) (0) | 2024.10.30 |
| DB 연결 설정 (0) | 2024.10.29 |
| 테스트 코드 작성과 TDD (2) | 2024.10.29 |
| Query 어노테이션(JPQL) 및 JPA의 Specification (0) | 2024.10.25 |