Backend/SpringBoot

QueryDSL

Dean83 2024. 10. 30. 11:21

복잡한 쿼리를 동작하기 위해서 여러 방법이 있다. 앞서 작성한 @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를 주입받아 이용

 

  • 요약하자면, 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