본문 바로가기
Study/실전! QueryDSL

Day 3. QueryDSL 중급 문법

반응형

본 포스트는 인프런 - 실전! QueryDSL의 강의를 듣고 공부한 내용입니다

https://www.inflearn.com/course/Querydsl-%EC%8B%A4%EC%A0%84

 

실전! Querydsl - 인프런 | 강의

Querydsl의 기초부터 실무 활용까지 한번에 해결, 본 강의는 자바 백엔드 개발의 실전 코스를 완성하는 마지막 강의 입니다. 스프링 부트와 JPA 실무 완전 정복 로드맵을 우선 확인해주세요. 로드

www.inflearn.com


1. 프로젝션과 결과 반환

  • 프로젝션
    • SELECT절에서 어떤 것을 가져올지 대상을 지정하는 것
    • 대상이 한개의 타입
      ...
      
      List<String> actual = queryFactory
          .select(member.username)
          .from(member)
          .fetch();
          
      ...​

      • 반환되는 타입이 한개 ( String type )인 경우는 컬렉션의 제네릭 타입을 명확하게 지정할 수 있음
      • 한개의 Q-Type 클래스도 같은 방식으로 사용할 수 있음
      • 대상이 두개 이상의 타입을 갖는 경우 튜플 혹은 DTO로 조회해야한다
  • 튜플 조회
    • 프로젝션 대상 타입이 둘 이상일 경우 사용
    • 한번에 여러 타입을 담아, 꺼내서 쓸 수 있다
    • com.querydsl.core.Tuple
      • 튜플은 Repository 계층에서까지만 쓰는 것이 좋은 것 같다, Service, Controller 계층까지 넘어가는 것은 좋은 설계가 아닌 것 같음
      • QueryDSL의 core 패키지란 뜻은 QueryDSL의 세부 기술이라는 뜻으로 infrastructure 계층, Repository를 넘어서서 다른 계층까지 침투하는 것은 좋지 않다
    • 예제 코드
          
      // Tuple
      @Test
      void tupleTest() {
          // when
          List<Tuple> actual = jpaQueryFactory
              .select(member.userName, member.age)
              .from(member)
              .fetch();
      
          // then
          actual.forEach(el -> {
              String userName = el.get(member.userName);
              Integer age = el.get(member.age);
              System.out.println("userName => " + userName + ", age => " + age);
          });
      }​


      • com.querydsl.core.types.Expression, 즉 select절에 넣어줬던 Q-Type의 Expression를 키로 결과를 저장하고 있다
      • 꺼내올때도 저장했던 키 그대로 넣어주면, get 메서드를 통해 결과값을 가져올 수 있다
  • DTO 조회
    • 기존의 JPQL에서 DTO를 사용하려면 new Operation을 통해 실제 모든 패키지 경로를 SELECT절에 넣어줘야했다
    • 하지만 이와다르게 QueryDSL은 결과를 DTO로 반환할 때 프로퍼티 접근, 필드 직접 접근, 생성자 사용이라는 3가지 방식을 전부 지원한다
    • 프로퍼티 접근
      • com.querydsl.core.types.Projections 이용
      • Projections.bean()을 사용하여 지정해준 타입의 setter()로 인젝션하겠다고 명시
      • setter()를 사용하겠다는 것(프로퍼티로 접근)은 기본 생성자로 newInstance를 한 후에 호출을 하겠다는 것으로 프로퍼티 접근을 사용하려면 기본 생성자가 있어야한다
      • 예제 코드
        // 프로퍼티 접근
        @Test
        void findDtoBySetterTest() {
        
            // when
            List<MemberDto> actual = jpaQueryFactory
                .select(Projections.bean(MemberDto.class,
                                         member.userName,
                                         member.age))
                .from(member)
                .fetch();
        
            actual.forEach(System.out::println);
        }
    • 필드 직접 접근
      • Projections.fields()을 사용하여 지정해준 타입의 필드에 바로 값을 넣겠다는 뜻
      • bean() 메서드와 동일하게 기본 생성자는 필수이지만 Setter() 메서드는 필요없다
      • 매칭되는 프로퍼티가 없으면 에러가 나는게 아니라 무시된다!! ( 필드에 NULL값 ) 조심해야한다
      • 예제 코드
            
        // 필드 주입
        @Test
        void findDtoByFieldsTest() {
        
            // when
            List<MemberDto> actual = jpaQueryFactory
                .select(Projections.fields(MemberDto.class,
                                           member.userName,
                                           member.age))
                .from(member)
                .fetch();
        
            actual.forEach(System.out::println);
        }​
    • 생성자 사용
      • Projections.constructor()를 이용하여 정의된 생성자를 사용하는 방식
      • 반드시 파라미터의 순서와 타입이 제공되는 생성자의 파라미터 순서, 타입과 일치해야한다
      • Setter()나 기본 생성자는 필요없다
      • 예제 코드
            
        // 생성자 사용
        @Test
        void findDtoByConstructorTest() {
        
            // when
            List<MemberDto> actual = jpaQueryFactory
                .select(Projections.constructor(MemberDto.class,
                                                member.userName,
                                                member.age))
                .from(member)
                .fetch();
        
            actual.forEach(System.out::println);
        }​

        • member.userName(String), member.age(Integer)의 순서와 타입으로 받는 Contructor가 MemberDto에 정의되어 있어야함
    • 추가 사항: Q-Type 클래스의 속성명과 주입하려는 DTO의 속성명이 다른 경우는?
      • 파라미터로 들어가는 Q-Type Class 속성에 as() 메서드를 이용해서 별칭을 적어주면 된다
      • 예제 코드
        // 별칭 지정
        @Test
        void findUserDtoTest() {
        
            // when
            List<UserDto> actual = jpaQueryFactory
                .select(Projections.fields(UserDto.class,
                                           member.userName.as("name"),
                                           member.age.as("fakeAge")))
                .from(member)
                .fetch();
        
            actual.forEach(System.out::println);
        }
    • 추가사항: 서브 쿼리의 결과가 DTO의 속성으로 주입하기
      • 서브쿼리, com.querydsl.core.types.ExpressionsUtils 이용
      • 예제 코드
            
        // 서브쿼리 이용
        @Test
        void usingSubQueryTest() {
            QMember memberSub = new QMember("memberSub");
        
            List<UserDto> actual = jpaQueryFactory
                .select(Projections.fields(UserDto.class,
                                           member.userName.as("name"),
                                           ExpressionUtils.as(jpaQueryFactory
                                                                  .select(memberSub.age.max())
                                                                  .from(memberSub), "fakeAge")))
                .from(member)
                .fetch();
        
            actual.forEach(System.out::println);
        }​
  • @QueryProjection 사용
    • com.querydsl.core.annotations.QueryProjection
    • 생성자에 @QueryProjection을 선언하여 DTO 또한 Q-Type 클래스가 생성되게 한다 ( Gradle 컴파일 필요 )
    • 예제 코드
          
      // Q-Type QueryProjection
      @Test
      void findDtoByQueryProjectionTest() {
          List<MemberDto> actual = jpaQueryFactory
              .select(new QMemberDto(member.userName, member.age))
              .from(member)
              .fetch();
      
          actual.forEach(System.out::println);
      }​

      • MemberDto 생성자에 @QueryProjection을 선언하여 QMemberDto를 생성했습니다
      • 생성자 사용 방식(Projections.constructor(...)) vs @QueryProjection 차이
        • 생성자 사용 방식은 런타임 시점에서 맞는 생성자를 찾고 없으면 오류를 낸다
        • @QueryProjection으로 생성된 Q-Type Class는 컴파일 시점에서 오류를 낼 수 있다 ( 생성된 생성자만 사용하므로 )
      • 실무상 단점
        • Q-Type을 생성해야한다, @QueryProjection을 선언해야함
        • DTO가 구체적인 외부 기술인 QueryDSL의 의존성을 가지고 있게된다 ( 선택을 해야할듯... )

 

2. 동적 쿼리

  • 쿼리를 캐싱하지 않고 동적으로 SQL을 생성하는 것을 동적 쿼리라고 합니다 ( 조건에 따라 WHERE 변경등 )
  • BooleanBuilder 사용 방법
    • com.querydsl.core.BooleanBuilder
    • 예제 코드
          
      // BooleanBuilder
      @Test
      void booleanBuilderTest() {
          String usernameParam = "memberA";
          Integer ageParam = 10;
      
          List<Member> result = selectMember1(usernameParam, ageParam);
          assertThat(result)
              .hasSize(1);
      }
      
      private List<Member> selectMember1(String usernameParam, Integer ageParam) {
          BooleanBuilder booleanBuilder = new BooleanBuilder();
          if (Objects.nonNull(usernameParam)) {
              booleanBuilder.and(member.userName.eq(usernameParam));     
          }
      
          if (Objects.nonNull(ageParam)) {
              booleanBuilder.and(member.age.eq(ageParam));
          }
      
          return jpaQueryFactory
              .selectFrom(member)
              .where(booleanBuilder)
              .fetch();
      }​

      • BooleanBuilder에서 지원하는 메서드를 통해 조건별로 쿼리를 생성할 수 있다
      • BooleanBuilder끼리도 연결할 수 있다
  • Where 다중 파라미터 사용
    • jpaQueryFactory의 where() 메서드는 기본적으로 들어오는 파라미터(Predicate)들을 and 조건으로 연결한다
    • 단 null일때는 그냥 무시한다 ( 이것을 이용해 동적 쿼리를 만들 수 있다 )
    • 예제 코드
          
      // 다중 where 조건
      @Test
      void whereTest() {
          String usernameParam = "memberA";
          Integer ageParam = null;
      
          List<Member> result = selectMember2(usernameParam, ageParam);
          assertThat(result).hasSize(1);
      }
      
      private List<Member> selectMember2(String usernameParam, Integer ageParam) {
          return jpaQueryFactory
              .selectFrom(member)
              .where(usernameEq(usernameParam), ageEq(ageParam)) // 기본적으로 and조건, null은 무시
              .fetch();
      }
      
      private Predicate usernameEq(String usernameParam) {
          if (Objects.isNull(usernameParam)) {
              return null;
          }
          return member.userName.eq(usernameParam);
      }
      
      private Predicate ageEq(Integer ageParam) {
          if (Objects.isNull(ageParam)) {
              return null;
          }
          return member.age.eq(ageParam);
      }​

      • 3항 연산자를 통해 한줄로 처리하는 방식도 권장한다
      • 각 조건별 메서드(usernameEq, ageEq)의 반환형을 Predicate로 했는데 조합을 위해 BooleanExpression으로 반환하는 것이 좋다 
      • 장점
        • 만들어둔 조건절 메서드는 다른 쿼리에서도 재활용할 수 있다는 장점이 있다
        • 쿼리 자체의 가독성이 좋아진다 ( 메서드로 추출하여 네이밍이 가능함 )
        • 다양하게 조합이 가능하다 ( BooleanExpression 사용 )

 

3. 수정, 삭제 배치 쿼리

  • 수정과 삭제에 대해 건건히 쿼리를 날리는 것이 아닌 벌크 연산을 통해 한번에 쿼리를 날릴 수 있습니다
  • 수정
    • JPA는 기본적으로 Update에 관해 변경감지가 동작하여 알아서 수정한다 ( 스냅샷과 비교 후 )
    • fetch() 메서드를 통해 결과를 가져오는 것이 아닌 execute() 메서드를 통해 실행시킨다
    • QueryDSL을 이용하여 벌크 연산 처리
          
      // Bulk Update
      @Test
      void bulkUpdateTest() {
          // when
          long lowCount = jpaQueryFactory
              .update(member)
              .set(member.userName, "비회원")
              .where(member.age.lt(28))
              .execute();
      
          // then
          assertThat(lowCount).isEqualTo(2);
      }​

      • 벌크 연산은 영속성 컨텍스트의 내용을 무시하고, DB에 바로 쿼리가 날라간다 ( 주의 사항 )
            
        // Bulk Update 주의사항
        @Test
        @Commit
        void bulkUpdateTest() {
        
            // given
            // 3 memberA 10
            // 4 memberB 20
            // 5 memberC 30
            // 6 memberD 40
        
            // when
            long lowCount = jpaQueryFactory
                .update(member)
                .set(member.userName, "비회원")
                .where(member.age.lt(28))
                .execute();
        
            // then
            assertThat(lowCount).isEqualTo(2);
        
            // 3 비회원 10
            // 4 비회원 20
            // 5 memberC 30
            // 6 memberD 40
        
            // when
            List<Member> members = jpaQueryFactory
                .selectFrom(member)
                .fetch();
        
            members.forEach(System.out::println);
        }​

        • id, username, age 순의 데이터 4개가 insert되어있다는 가정하에 위와 같은 코드를 실행시켜봤다
        • 28살 미만의 모든 Member.username을 "비회원"으로 변경하는 벌크 Update를 실행 후 다시 전체 Member 테이블을 조회해봤다 ( 기존의 Member 데이터 4개는 같은 영속성 컨텍스트 내에서 insert 됐다고 가정한다 )
        • 그렇다면 최종 forEach로 출력되는 결과는 무엇일까?
        •  
          • 신기하게도 DB의 값은 정상적으로 변했지만 SELECT 쿼리로 실행된 결과는 변경되지 않았다
          • 이유는 영속성 컨텍스트의 특징에 있다
          • 영속성 컨텍스트는 같은 ID 값의 동일한 엔티티가 이미 1차 캐시에 존재할 경우 SELECT되어 결과가 조회되더라도 늦게 들어온 데이터를 버린다 ( 덮어쓰지 않는다, 영속성 컨텍스트의 기존 데이터가 항상 우선권을 갖는다 )
          • 위와 같은 현상을 REPEATABLE READ(같은 쿼리를 여러번 실행해도 같은 결과를 내는 것)라고 한다
          • 해결책
            • 벌크 연산을 수행하면 항상 영속성 컨텍스트를 날려줘라
            • entityManager.flush();
            • entityManager.clear();
            • Spring Data JPA, @Modifying
  • 수정 ( 특정 값을 연산 )
    • 예제 코드
              
      // Update
      long execute = jpaQueryFactory
          .update(member)
          .set(member.age, member.age.add(1))
          .execute();
      • Q-Type 프로퍼티의 메서드(add, multiply ...)를 이용한다
    • SQL Query
          
      # JPQL
      /* 
      update
          Member member1 
      set
          member1.age = member1.age + ?1 
      */ 
      
      # SQL Query
      update
          member 
      set
          age=age+?​
  • 삭제
    • 예제 코드
          
      // delete
      @Test
      void bulkDeleteTest() {
          long execute = jpaQueryFactory
              .delete(member)
              .where(member.age.gt(18))
              .execute();
      }​
    • SQL Query
      # JPQL
      /*
      delete from
          Member member1 
      where
          member1.age > ?1 
      */ 
      
      # SQL Query
      delete from
          member 
      where
          age>?​

 

4. SQL function 호출하기

  • 기본적으로 Dialect에 등록되어있는 SQL 함수만 사용 가능하다
  • 사용 가능한 함수(registerFunction())는 org.hibernate.dialect 패키지안 각 Dialect에 맞는 클래스에 정의되어있다 ( H2Dialect, 
  • com.querydsl.core.types.dsl.Expressions 사용
  • 일반적인 Ansi 표준 문법들은 어느정도 Q-Type에 정의되어있긴하다 ( lowercase 등등 )
  • 예제 코드 ( H2 Database만 사용 가능 )
        
    // only H2
    @Test
    void replaceTest() {
        List<String> actual = jpaQueryFactory
            .select(Expressions.stringTemplate(
                "function('replace', {0}, {1}, {2})",
                member.userName,
                "member",
                "M"))
            .from(member)
            .fetch();
    
        actual.forEach(System.out::println);
    }​
728x90
반응형