실전! Querydsl 정리

곽지용

Updated:

Intro

안녕하세요? 곽지용 사원입니다.
김영한씨의 ‘실전! Querydsl 정리’ 강의를 듣고나서
꼭 필요했던 부분이나 알아두면 좋은 부분을 정리해보았습니다.

[참고]
강의는 MacOS, IntelliJ 환경이지만
글작성자는 Windows, VSCode 환경에서 따라해보았습니다.

https://www.inflearn.com/course/Querydsl-실전

1. 프로젝트 환경설정

1-1. 프로젝트 생성

  • 스프링 부트 스타터(https://start.spring.io/) 에서 간단하게 생성 가능합니다.
    • SpringBootVersion: 2.2.2 //현재 설정 불가 *2022-01 기준 2.6.2 버전이 최신
    • groupId: study
    • artifactId: data-jpa
    • dependency : Spring Web, jpa, h2, lombok

      스프링 부트 스타터에서 추가할 수 있는 dependency 중에 Querydsl이 없습니다.
      때문에 추가 설정이 필요합니다.

  • 부트 2.2.2버전 Querydsl 추가 설정
    • build.gradle
      plugins {
          id 'org.springframework.boot' version 2.2.2.RELEASE'
          id 'io.spring.dependency-management' version '1.0.8.RELEASE'
          //querydsl 추가
          id "com.ewerk.gradle.plugins.querydsl" version "1.0.10"
          id 'java'
      }
      group = 'study'
          version = '0.0.1-SNAPSHOT'
          sourceCompatibility = '1.8'
          configurations {
          compileOnly {
              extendsFrom annotationProcessor
          }
      }
      repositories {
          mavenCentral()
      }
      dependencies {
          implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
          implementation 'org.springframework.boot:spring-boot-starter-web'
          //querydsl 추가
          implementation 'com.querydsl:querydsl-jpa'
          compileOnly 'org.projectlombok:lombok'
          runtimeOnly 'com.h2database:h2'
          annotationProcessor 'org.projectlombok:lombok'
          testImplementation('org.springframework.boot:spring-boot-starter-test') {
              exclude group: org.junit.vintage, module: junit-vintage-engine'
          }
      }
      test {
          useJUnitPlatform()
      }
      //querydsl 추가 시작
      def querydslDir = "$buildDir/generated/querydsl"
      querydsl {
          jpa = true
          querydslSourcesDir = querydslDir
      }
      sourceSets {
          main.java.srcDir querydslDir
      }
      configurations {
          querydsl.extendsFrom compileClasspath
      }
      compileQuerydsl {
          options.annotationProcessorPath = configurations.querydsl
      }
      //querydsl 추가 끝
    

    현재는 버전이 안맞아서 위 방법대로는 적용이 되지않았습니다.

  • 2.6.2버전기준 Querydsl 설정

      //querydsl 추가
      buildscript {
          dependencies {
              classpath("gradle.plugin.com.ewerk.gradle.plugins:querydsl-plugin:1.0.10")
          }
      }
    
      plugins {
          id 'org.springframework.boot' version '2.6.2'
          id 'io.spring.dependency-management' version '1.0.11.RELEASE'
          id 'java'
      }
    
      group = 'study'
      version = '0.0.1-SNAPSHOT'
      sourceCompatibility = '11'
    
      //apply plugin: 'io.spring.dependency-management'
      apply plugin: "com.ewerk.gradle.plugins.querydsl"
    
      configurations {
          compileOnly {
              extendsFrom annotationProcessor
          }
      }
    
      repositories {
          mavenCentral()
      }
    
      dependencies {
          implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
          implementation 'org.springframework.boot:spring-boot-starter-web'
          //querydsl 추가
          implementation 'com.querydsl:querydsl-jpa'
          implementation 'com.querydsl:querydsl-apt'
          //sql로그
          implementation 'com.github.gavlyukovskiy:p6spy-spring-boot-starter:1.8.0'
          compileOnly 'org.projectlombok:lombok'
          runtimeOnly 'com.h2database:h2'
          annotationProcessor 'org.projectlombok:lombok'
          testImplementation 'org.springframework.boot:spring-boot-starter-test'
      }
    
      test {
          useJUnitPlatform()
      }
    
      //querydsl 추가
      //def querydslDir = 'src/main/generated'
      def querydslDir = "$buildDir/generated/querydsl"
    
      querydsl {
          library = "com.querydsl:querydsl-apt"
          jpa = true
          querydslSourcesDir = querydslDir
      }
    
      sourceSets {
          main {
              java {
                  srcDirs = ['src/main/java', querydslDir]
              }
          }
      }
    
      compileQuerydsl{
          options.annotationProcessorPath = configurations.querydsl
      }
    
      configurations {
          querydsl.extendsFrom compileClasspath
      }
    

1-2. Gradle 빌드

  • Gradle IntelliJ 사용법
    • Gradle > Tasks > build > clean
    • Gradle > Tasks > other > compileQuerydsl
  • Gradle VSCode 사용법
    1. Gradle Extension Pack 플러그인 설치
    2. 사이드바의 Gradle 선택
    3. 프로젝트 선택 > Tasks > build > clean
    4. 프로젝트 선택 > Tasks > other > compileQuerydsl

작성자는 VSCode를 Ide로 사용했습니다.
VSCode 작업영역에 gradlew.bat 파일이 보이도록 폴더를 배치해야 사이드바에 Gradle 버튼이 활성화됩니다.

2. JPQL과 Querydsl 비교

select * from Member Where username = ‘member1’ 을 나타낸 코드입니다.

  • JPQL

      @Test
      public void startJPQL() {
          String qlString =
              "select m from Member m " +
              "where m.username = :username";
          Member findMember = em.createQuery(qlString, Member.class)
              .setParameter("username", "member1")
              .getSingleResult();
          assertThat(findMember.getUsername()).isEqualTo("member1");
      }
    
  • Querydsl

      @Test
      public void startQuerydsl() {
          JPAQueryFactory queryFactory = new JPAQueryFactory(em);
          QMember m = new QMember("m");
          Member findMember = queryFactory
              .select(m)
              .from(m)
              .where(m.username.eq("member1"))
              .fetchOne();
          assertThat(findMember.getUsername()).isEqualTo("member1");
      }
    

    Querydsl은 쿼리가 아니라 코드로만 작성할 수 있습니다.

  • SQL이 코드로만 작성되어 생기는 이점

    • JPQL은 어플리케이션 로딩시점에 에러를 잡을 수 있는게 장점이었다면 Querydsl은 컴파일 시점에서 문법 오류를 잡을 수 있습니다.
    • 메서드 체이닝으로 이루어져있기 때문에 조립하기 쉽게 되어있고 당연히 동적쿼리 작성에 유리합니다.
    • 자바코드의 장점을 그대로 가져옵니다. (중복코드를 재사용, 자동완성 등)

      결론적으로 생산성은 늘고, 에러체크는 더 빨라집니다.

3. 동적쿼리

자바 코드로 구성되어 있기 때문에 동적 쿼리도 간단해집니다.

3-1. BooleanBuilder

private List<Member> searchMember1(String usernameCond, Integer ageCond) {
    BooleanBuilder builder = new BooleanBuilder();
    if (usernameCond != null) {
        builder.and(member.username.eq(usernameCond));
    }
    if (ageCond != null) {
        builder.and(member.age.eq(ageCond));
    }
    return queryFactory
                .selectFrom(member)
                .where(builder)
                .fetch();
}

3-2. Where 다중 파라미터 사용

private List<Member> searchMember2(String usernameCond, Integer ageCond) {
return queryFactory
            .selectFrom(member)
            .where(usernameEq(usernameCond), ageEq(ageCond))
            .fetch();
}

private BooleanExpression usernameEq(String usernameCond) {
    return usernameCond != null ? member.username.eq(usernameCond) : null;
}
private BooleanExpression ageEq(Integer ageCond) {
    return ageCond != null ? member.age.eq(ageCond) : null;
}

QueryFactory의 Where 메소드에는 파라미터로 Predicate(서술어) 타입을 받습니다.
BooleanBuilder와 BooleanExpression은 Predicate 인터페이스의 구현체입니다.

4. 서브 쿼리

JPAExpressions 클래스를 이용해서 서브쿼리를 구현가능합니다.

  • Where 서브쿼리

      QMember memberSub = new QMember("memberSub");
      List<Member> result = 
      queryFactory
          .selectFrom(member)
          .where(member.age.eq(
              JPAExpressions
                  .select(memberSub.age.max())
                  .from(memberSub)
              )
          )
          .fetch();
    
  • Select 서브쿼리

      List<Tuple> fetch = queryFactory
          .select(member.username,
              JPAExpressions
                  .select(memberSub.age.avg())
                  .from(memberSub)
          ).from(member)
          .fetch();
    

from절의 서브쿼리는 JPA에서 지원하지않기 때문에 Querydsl도 사용불가능합니다.
필요한경우 조인으로 해결하거나 안된다면 네이티브 쿼리를 사용해야 합니다.

5. Case문

  • 일반적인 case문 Q엔티티에 바로 접근해서 사용

      List<String> result = queryFactory
                              .select(member.age
                                  .when(10).then("열살")
                                  .when(20).then("스무살")
                                  .otherwise("기타")
                              )
                              .from(member)
                              .fetch();
    
  • CaseBuilder를 이용한 Case문

      NumberExpression<Integer> rankPath = new CaseBuilder()
              .when(member.age.between(0, 20)).then(2)
              .when(member.age.between(21, 30)).then(1)
              .otherwise(3);
      List<Tuple> result = queryFactory
              .select(member.username, member.age, rankPath)
              .from(member)
              .orderBy(rankPath.desc())
              .fetch();
    

    CaseBuilder를 이용해 BooleanBuilder처럼 재사용가능한 코드로 만들 수 있다.

6. Expressions

Expressions 클래스는 ANSI 표준의 다양한 함수와 표현식을 지원합니다.
(String, Number, Enum, Constant, Date 등등)
기존 sql 표현식때문에 쿼리를 사용할 필요가 없어졌습니다.

  • stringTemplate 예제
      Tuple result = queryFactory
                          .select(
                              Expressions.stringTemplate("function('replace', {0}, {1}, {2})", member.username, "member", "M"),
                              Expressions.stringTemplate("DATE_FORMAT({0}, {1})", member.startTime, "%Y-%m-%d").as("date")
                          )
                          .from(member)
                          .fetchFirst();
    

7. QuerydslRepositorySupport

스프링 데이터가 제공하는 페이징을 Querydsl로 편리하게 운용하도록 도와줍니다.

  • getQuerydsl().applyPagination()사용 예제

      JPQLQuery<StoreVo> query = jpaQueryFactory        
                      .select(Projections.fields(StoreVo.class,
                              store.id
                              , store.name
                              , store.address
                      ))
                      .from(store)
                      .where(store.name.eq(name));
    
              long totalCount = query.fetchCount();            
              List<StoreVo> results = getQuerydsl().applyPagination(pageable, query).fetch();
    

    문제는 해당 기능으로 페이징시 스프링 데이터 Sort 기능이 정상 동작하지 않습니다.
    정렬이 필요할 땐 직접 구현해야합니다.

Leave a comment