[TIL] 23.02.03 QueryDSL 사용법

hyewon jeong·2023년 2월 3일
0

TIL

목록 보기
82/138

QueryDSL

Spring Data JPA가 기본적으로 제공해주는 CRUD 메서드 및 쿼리 메서드 기능을 사용하더라도, 원하는 조건의 데이터를 수집하기 위해서는 필연적으로 JPQL을 작성하게 됩니다. 간단한 로직을 작성하는데 큰 문제는 없으나, 복잡한 로직의 경우 개행이 포함된 쿼리 문자열이 상당히 길어집니다. JPQL 문자열에 오타 혹은 문법적인 오류가 존재하는 경우, 정적 쿼리라면 어플리케이션 로딩 시점에 이를 발견할 수 있으나 그 외는 런타임 시점에서 에러가 발생합니다.

이러한 문제를 어느 정도 해소하는데 기여하는 프레임워크가 바로 QueryDSL입니다. QueryDSL은 정적 타입을 이용해서 SQL 등의 쿼리를 생성해주는 프레임워크입니다.

기능

  • QueryDSL의 Predicate 인터페이스로 조건문을 여러개를 구성하여 따로 관리할 수 있다.
    • findOne(Predicate), findAll(Predicate) 주로 이 2개 메소드가 사용된다.
      • findOne = Optional 리턴
      • findAll = List | Page | Iterable | Slice 리턴
  • Type Safe 기능
    • 조건문 구성시에 사용되는 객체, 필드 조건이 실제 타입과 일치한지 체크해준다.

장점

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

원리

  • QueryDSL 의존성을 추가하면 SpringData에 의해 QueryDslPredicateExecutor 인터페이스가 추가된다.
    • QueryDslPredicateExecutor 는 Repository가 QueryDsl 을 실행할 수 있는 인터페이스를 제공하는 역할을 합니다.

사용 방법

  • Spring 2.X 버전까지는 QueryDSL 빌드 Task 를 따로 설정해줘야 했습니다.
    • Spring 2.X 버전 연동설정 queryDSL 의존성, 소스 디렉토리 설정, annotation 프로세서 설정, Task 정의를 해줘야 합니다.
      plugins {
          ...
          id "com.ewerk.gradle.plugins.querydsl" version "1.0.10"
      }
      
      compileQuerydsl{
          options.annotationProcessorPath = configurations.querydsl
      }
      
      configurations {
          ...
          querydsl.extendsFrom compileClasspath
      }
      
      def querydslSrcDir = 'src/querydsl/generated'
      
      querydsl {
          library = "com.querydsl:querydsl-apt"
          jpa = true
          querydslSourcesDir = querydslSrcDir
      }
      
      sourceSets {
          main {
              java {
                  srcDirs = ['src/main/java', querydslSrcDir]
              }
          }
      }
      
      project.afterEvaluate {
          project.tasks.compileQuerydsl.options.compilerArgs = [
                  "-proc:only",
                  "-processor", project.querydsl.processors() +
                          ',lombok.launch.AnnotationProcessorHider$AnnotationProcessor'
          ]
      }
      
      dependencies {
          implementation("com.querydsl:querydsl-jpa") // querydsl
          implementation("com.querydsl:querydsl-apt") // querydsl
          ...
      }

QueryDSL gradle 빌드 스크립트 원리

  • Gradle 빌드시에 QueryDSL은 프로젝트 내의 @Entity 어노테이션을 선언한 클래스를 탐색하고,  JPAAnnotationProcessor를 사용해 Q클래스를 생성합니다.
  • querydsl-apt가 @Entity 및 @Id 등의 매핑정보 Annotation을 알 수 있도록 javax.persistence과 javax.annotation 을 annotationProcessor에 함께 추가합니다.
  • annotationProcessor는 Java 컴파일러 플러그인으로서, 컴파일 단계에서 어노테이션을 분석 및 처리함으로써 추가적인 파일을 생성합니다.
  • 개발 환경에서 생성된 Q클래스를 인지할 수 있도록 /build/generated 디렉토리를 프로젝트의 sourceSet에 추가합니다.
  • 이로써, IDE 에서도 생성된 Q클래스 파일을 인식하여 개발할 수 있습니다.

    Q 클래스

  • 설정 및 빌드를 마친 이후, 다음과 같이 Java 파일을 컴파일합니다.

  • $projectDir/build/generated 디렉토리 하위에 Entity로 등록한 클래스들이 Q라는 접두사가 붙은 형태로 생성되었습니다.
    이러한 클래스들을 Q 클래스 혹은 Q(쿼리) 타입이라고 합니다.

QueryDSL로 쿼리를 작성할 때, Q 클래스를 사용함으로써 쿼리를 Type-Safe하게 작성할 수 있습니다.
Gradle 설정을 통해 $projectDir/src/main/java의 프로덕션 코드에서 Q 클래스를 import해 사용할 수 있습니다.

📌 Spring 3.X 버전부터는 의존성만 추가하면 빌드에 자동으로 포함되서 실행됩니다.

  • Spring 3.X 버전 연동설정
    의존성 만 추가하면 됩니다.
        
 dependencies {
        		....
        
 // 9. QueryDSL 적용을 위한 의존성 (SpringBoot3.0 부터는 jakarta 사용해야함)
 implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jakarta"
 annotationProcessor "jakarta.annotation:jakarta.annotation-api"
 annotationProcessor "jakarta.persistence:jakarta.persistence-api"
        }

💡 QueryDSL .Join 이 없는 대신 조건이 많은 쿼리 (가능🙆‍♀️)

  • 해당 채널에서 메세지가 있는 쓰레드 목록 조회 기능 만들기
    1. Channel 조건으로 메세지 본문이 있는 Thread 목록 조회 쿼리수행

      💁‍♂️ 이처럼 Join 없이 조건이 많이 추가될수록 **QuerydslPredicateExecutor** 를 활용할 수 있다.

    • 사실 현업에서는 요건 잘 안쓰고 JPAQueryFactory 를 주로쓰긴 합니다.
    • 구현 코드 스니펫
      @Service
      public class ThreadServiceImpl implements ThreadService {
      
        @Autowired
        ThreadRepository threadRepository;
      
        @Override
        public List<Thread> selectNotEmptyThreadList(Channel channel) {
          var thread = QThread.thread;
      
          // 메세지가 비어있지 않은 해당 채널의 쓰레드 목록
          var predicate = thread
      				.channel.eq(channel)
              .and(thread.message.isNotEmpty());
          var threads = threadRepository.findAll(predicate);
      
          return IteratorAdapter.asList(threads.iterator());
        }
      
        @Override
        public Thread insert(Thread thread) {
          return threadRepository.save(thread);
        }
      }
      @Test
        void getNotEmptyThreadList() {
          // given
          var newChannel = Channel.builder().name("c1").type(Type.PUBLIC).build();
          var savedChannel = channelRepository.save(newChannel);
          var newThread = Thread.builder().message("message").build();
          newThread.setChannel(savedChannel);
          threadService.insert(newThread);
      
          var newThread2 = Thread.builder().message("").build();
          newThread2.setChannel(savedChannel);
          threadService.insert(newThread2);
      
          // when
          var notEmptyThreads = threadService.selectNotEmptyThreadList(savedChannel);
      
          // then 메세지가 비어있는 newThread2 는 조회되지 않는다.
          assert !notEmptyThreads.contains(newThread2);
        }

💡 QueryDSL 적용할 수 없는 예

모든 채널에서 내가 멘션된 쓰레드 목록조회기능 만들기
1. Mention 엔티티 생성
2. User- Mention - Thread 다대다 연관관계 설정
3. User, Mention조건으로 Thread조회 쿼리수행

✅ 1. User Class에 mention 연관관계 설정

  @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
  Set<CMention> mentions = new LinkedHashSet<>();

✅ 2. Thread Class에 mention 연관관계 설정

  @OneToMany(mappedBy = "thread", cascade = CascadeType.ALL, orphanRemoval = true)
  Set<Mention> mentions = new LinkedHashSet<>();
  //연관관계 편의 메서드 
  public void addMention(User user){
Mention mention =  Mention.builder().user(user).thread(this).build(); 
//양방향이므로 양쪽에다 생성한 멘션을 추가해줘야함 this.mentions.add(mention); 
user.getMentions.add(mention); 
  }

✅ 3. Thread Service 구현 . @QueryDsl 이용법

ThreadRepository에 QuerydslPredicateExecutor 구현

public interface ThreadRepository extends JpaRepository<Thread, Long>, QuerydslPredicateExecutor<Thread>  {

}

Thread Service에 QueryDsl 사용할 predicate(조건 설정)

@Service
 public class ThreadServiceImpl implements ThreadService{
 
 @Autowired
 ThreadRepository threadRepository;
 
 @Override
 public List<Thread> getMentiondThreadList(User user){
  var thread = Quthread.thread;
 //모든 채널에서 내가 멘션된 스레드 목록 조회 기능 만들기 
  var predicate = thread.mention.any().user.eq(user)
  var threads = threadRepository.findAll(predicate); 

 return IteratorAdapter.asList(threads.iterator()); 

❤️ findAll(predicate) @QueryDsl 이용한 구현

  1. findAll(predicate)할려면 우선 스레드를 만들어 줘야한다.
 Var thread = QThread.thread; 받아오고
  1. predicate 선언
Var predicate = thread.mention.any().user.eq(user)
  • 스레드에 맨션 아무거나 이 유저와 같아야한다. = 내가 맨션된 스레드를 가져오라는 의미
  1. var threads = threadRepository.findAll(predicate); //여기에 넣어주면 predicate가 조건문으로 바뀐다. 그래서 조건에 맞는 스레드를 찾아옴

  2. Return threads; 는 리스트가 아니여서 리턴 할수 없다 .
    // 여러가지 이터레이터블을 리스트를 바꾸는게 있다.

  • return IteratorAdapter.asList(threads.iterator());

    이 경우 테스트 했을때 에러 발생한다.

    var mentiondThreads = threadService.getMentiondThreadList(savedUser);
** 📌 그 이유는?? 멘션이 매핑이 되어 있는데 QueryDsl의 경우  QuerydslPredicateExecutor이 조인연산이 되지 않기 때문에 에러가 발생한다.  **
  
 그래서 jpaRepository 기능으로 구현 해야함
  

var MentiondThreads = savedUser.getMentions().stream.map(Mention::getThread).toList();

📌즉, QuerydslPredicateExecutor이 조인연산에 사용 할 수 없다.

IteratorAdapter.asList(threads.iterator())

Iterator adapters

다른 Iterator(a.k.a 반복자)를 받아 새로운 반복자를 반환하는 함수를 iterator adapter라고 부른다.

Iterator adpaters 클래스에 속한 asList()는 명세에 보면 반복자 Iterator iter 로 매개변수를 받으면 iterator()의 값을 리스트로 반환시켜준다.

Iterator ( ) ?

https://velog.io/@wonizizi99/Design-Pattern-java-Iterator

참고
자바 ORM 표준 JPA 프로그래밍
자바 ORM 표준 JPA 프로그래밍
equals와 hashCode는 왜 같이 재정의해야 할까?

profile
개발자꿈나무

0개의 댓글