Study/실전! QueryDSL

Day 2. QueryDSL 기본 문법

주지민 2021. 9. 24. 13:02
반응형

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

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

 

실전! Querydsl - 인프런 | 강의

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

www.inflearn.com


1. 예제 도메인 모델

  • 해당 포스트를 진행하면서 사용할 도메인 모델부터 정의해보려고 합니다
  • 엔티티 ERD

  • Member Entity Code
    // Member.class
    @NoArgsConstructor(access = AccessLevel.PROTECTED)
    @Getter
    @ToString(of = {"id", "userName", "age"})
    @Entity
    public class Member {
    
        @Id
        @GeneratedValue
        @Column(name = "member_id")
        private Long id;
        private String userName;
        private int age;
    
        @ManyToOne(fetch = FetchType.LAZY)
        @JoinColumn(name = "team_id")
        private Team team;
    
        public Member(String userName) {
            this(userName, 0, null);
        }
    
        public Member(String userName, int age) {
            this(userName, age, null);
        }
    
        public Member(String userName, int age, Team team) {
            this.userName = userName;
            this.age = age;
            if (Objects.nonNull(team)) {
                changeTeam(team);
            }
        }
    
        private void changeTeam(Team team) {
            this.team = team;
            team.getMembers().add(this);
        }
    }
  • Team Entity Code
    // Team.class
    @NoArgsConstructor(access = AccessLevel.PROTECTED)
    @Getter
    @ToString(of = {"id", "name"})
    @Entity
    public class Team {
    
        @Id
        @GeneratedValue
        private Long id;
        private String name;
    
        @OneToMany(mappedBy = "team")
        private List<Member> members = new ArrayList<>();
    
        public Team(String name) {
            this.name = name;
        }
    }

 

2. JPQL vs QueryDSL

  • 간단한 테스트 코드를 통해 JPQL과 QueryDSL의 차이점을 알아보겠습니다
  • JPQL Test Code
    // JPQL Test Code
    @Test
    void startJPQL() {
    
        // given
        String expected = "memberA";
        String qlString = "SELECT m FROM Member m WHERE m.userName = :userName";
    
        // when
        Member actual = em.createQuery(qlString, Member.class)
                          .setParameter("userName", expected)
                          .getSingleResult();
    
        // then
        assertThat(actual).isNotNull();
        assertThat(actual.getUserName()).isEqualTo(expected);
    }
  • QueryDSL Test Code
    // Q Type Class compile
    ./gradlew compileQuerydsl


    // QueryDSL Test Code
    @Test
    void startQueryDsl() {
        QMember m = new QMember("m");
        
        Member actual = jpaQueryFactory
            .select(m)
            .from(m)
            .where(m.userName.eq("memberA"))
            .fetchOne();
    
        assertThat(actual).isNotNull();
        assertThat(actual.getUserName()).isEqualTo("memberA");
    }
  • 차이점
    • JPQL과 다르게 문자열을 사용하지 않고 자바 메서드 호출로 쿼리를 만들어냄
      • JPQL은 문자열 쿼리로 만들어내기 때문에 런타임 시점에서 쿼리 오류가 잡힌다 ( IDE의 도움을 받을 순 있음 )
      • QueryDSL은 자바 메서드 호출로 쿼리를 만들어 내기 때문에 컴파일 시점에서 문법적 오류가 잡힌다
    • JPQL처럼 쿼리에 ":{parameterName}"을 바인딩하지 않아도 만들어낸 Q Type 클래스의 eq, gt등의 메서드를 이용하여 바인딩된 쿼리를 만들어 낼 수 있다
      • QueryDSL은 기본적으로 파라미터를 ?로 바인딩, prepareStatement의 파라미터 바인딩 방식을 사용

 

3. Q-Type 클래스 활용

  • Q-Type 클래스를 사용하는 방법 ( 생성하는 방법 )
    • 생성자에 variable 파라미터(별칭)을 넘겨서 사용
      // using variable
      QMember m = new QMember("m");
      • 별칭은 말그래도 쿼리에서 사용하는 별칭이다 ( 같은 테이블을 조인해야하는 경우에만 별칭을 바꿔주면 된다 )
    • 내부에 만들어둔 static instance를 이용
      // Using static instance
      QMember m = QMember.member;
  • 특히나 static instance를 이용할 경우 코드를 더 간결하게 유지할 수 있다 ( static import 이용 )
  • 참고로 변환된 JPQL 쿼리를 보고 싶은 경우 jpa.properties.hibernate.use_sql_comments: true 속성을 추가해주도록 하자

 

4. 기본 문법 소개

4-1. 검색 조건 쿼리

  • 기본 테스트 코드 예제
    // test
    @Test
    void searchTest() {
        // when
        Member findMember = jpaQueryFactory
            .selectFrom(member)
            .where(member.userName.eq("memberA")
                                  .and(member.age.eq(10)))
            .fetchOne();
    
        // then
        assertThat(findMember.getUserName()).isEqualTo("memberA");
        assertThat(findMember.getAge()).isEqualTo(10);
    }

    • select ... from ... 은 대상이 같을 경우 selectFrom으로 합칠 수 있다
    • {Q-Type Class}.{속성}.{키워드} 형태로 where 조건절을 구성할 수 있으며 키워드에는 eq, between 등 다양한 기능을 제공
    • 또한 각 속성들을 and, or 등으로 묶을 수도 있다 ( 쿼리 형태랑 비슷 )
    • 키워드 종류 ( JPQL에서 지원하는 모든 것들을 메서드로 제공 )
      키워드 설명 코드 예제 쿼리
      eq 같다 member.username.eq("...") username = "..."
      ne 다르다 member.username.ne("...") username != "..."
      eq("...").not member.username.eq("...").not()
      isNotNull NULL이 아니다 member.username.isNotNull() username IS NOT NULL
      in 그룹셋 안에 포함되는지 member.username.in(...) username IN (...)
      not in 그룹셋 안에 포함되지 않는지 member.username.notIn(...) username NOT IN (...)
      between 범위안에 포함되는지 member.age.between(10, 20) age between 10 AND 20
      goe 크거나 같은지 member.age.goe(10) age >= 10
      gt 큰지 member.age.gt(10) age > 10
      loe 작거나 같은지 member.age.loe(10) age <= 10
      lt 작은지 member.age.lt(10) age < 10
      like 해당 문자열과 같은지
      * 패턴 일치 문자 사용 가능
      member.username.like("member%") username LIKE "member%"
      contains 해당 문자열이 포함되는지 member.username.contains("member") username LIKE "%member%"
      startsWith 해당 문자열로 시작하는지 member.username
      .startsWith("member")
      username LIKE "member%"
      기타 등등
    • AND 연산은 직관적으로 and method 체인을 걸 수도 있지만, where 메서드에 파라미터로 넣어도 각 파라미터가 and로 연결된다
      // 위 아래는 같은 결과
      where(member.username.eq("memberA").and(member.age.eq(10));
      
      where(member.username.eq("memberA"), member.age.eq(10));​

 

4-2. 결과 조회

  • fetch()
    • 리스트 조회, 데이터 없으면 빈 리스트 반환
    • Code
          
      // fetch method    
      @Test
      void fetchTest() {
          // when
          List<Member> members = jpaQueryFactory
              .selectFrom(member)
              .fetch();
      
          // then
          assertThat(members).isNotEmpty();
          assertThat(members).hasSize(4);
      }​
  • fetchOne()
    • 단건 조회
    • 결과가 없으면 NULL
    • 결과가 두 개 이상이면 com.querydsl.core.NonUniqueResultException 발생
    • Code
          
      // fetchOne 
      @Test
      void fetchOneTest() {
          // when
          Team actual = jpaQueryFactory
              .selectFrom(team)
              .where(team.name.eq("teamA"))
              .fetchOne();
      
          // then
          assertThat(actual).isNotNull();
          assertThat(actual.getName()).isEqualTo("teamA");
      }​
  • fetchFirst()
    • limit(1).fetchOne()
    • 실제 limit 1 조건절을 추가하여 조회한다
    • Code
      # JPQL
      /* select member1 from Member member1 */ 
      
      # SQL Query
      select 
          member0_.member_id as member_i1_1_,
          member0_.age as age2_1_,
          member0_.team_id as team_id4_1_,
          member0_.user_name as user_nam3_1_ 
      from 
          member member0_ 
      limit 1;​
  • fetchResults()
    • 페이징(limit, offset 등) 관련 결과 조회
    • 복잡한 쿼리를 fetchResults로 조회하게 되면 TotalCount를 조회하는 쿼리와 Contents를 조회하는 쿼리가 예상한 것과 상이할 수 있다, 이럴땐 fetchResults가 아닌 명시적으로 따로 두 개의 QueryDSL로 나눠 조회하자
    • Code
          
      // fetchResults
      @Test
      void fetchResultsTest() {
          // when
          QueryResults<Member> actual = jpaQueryFactory
              .selectFrom(member)
              .offset(2)
              .limit(2)
              .fetchResults();
      
          // then
          assertThat(actual.getTotal()).isEqualTo(4);
          assertThat(actual.getResults().size()).isEqualTo(2);
      }​

      • total, size, limit, offset 등의 페이징 관련 속성들을 얻을 수 있다
      • 조회 쿼리에 추가적으로 Count 쿼리가 한번 더 나가게 된다
    • Query
      // JPQL
      /* select count(member1) from Member member1 */ 
      
      // SQL Query ( total count 조회 )
      select 
          count(member0_.member_id) as col_0_0_
      from 
          member member0_;
      
      
      
      
      // JPQL
      /* select member1 from Member member1 */ 
      
      // SQL Query ( 실제 데이터 페이징 조회 )
      select 
          member0_.member_id as member_i1_1_, 
          member0_.age as age2_1_, 
          member0_.team_id as team_id4_1_, 
          member0_.user_name as user_nam3_1_ 
      from 
          member member0_ 
      limit 2, 2;


  • fetchCount()
    • count 쿼리로 변환하여 조회되는 결과 수만 반환
    • Code
          
      // fetchCount
      @Test
      void fetchCountTest() {
          // when
          long count = jpaQueryFactory
              .selectFrom(member)
              .fetchCount();
      
          // then
          assertThat(count).isEqualTo(4l);
      }​
    • Query
      // JPQL
      /* select count(member1)from Member member1 */ 
      
      // SQL Query
      select 
          count(member0_.member_id) as col_0_0_ 
      from member member0_;​

4-3. 정렬

  • orderBy 메서드를 통해서 다양하게 정렬을 시킬 수 있다
  • Code
        
    // 예제 코드
    @Test
    void sortTest() {
        // given
        em.persist(new Member(null, 100));
        em.persist(new Member("member5", 100));
        em.persist(new Member("member6", 100));
    
        // when
        List<Member> members = jpaQueryFactory
            .selectFrom(member)
            .where(member.age.eq(100))
            .orderBy(member.age.desc(),
                     member.userName.asc().nullsLast())
            .fetch();
    
        // then
        assertThat(members).hasSize(3);
        assertThat(members)
            .extracting("userName")
            .containsSequence("member5", "member6", null);
    }​

    • when 부분을 보면 orderBy를 이용해 member의 "age" 속성은 내림차순(큰것부터 작은것으로)으로 "userName" 속성은 오름차순(작은것부터 큰것으로) 조건을 줬다
    • 추가적으로 nullsLast()가 있는 것을 확인할 수 있는데 CASE문을 이용하여 값이 null일 경우의 추가적인 조건을 명시할 수 있다 
      • nullsLast(): null인 경우 제일 마지막
      • nullsFirst(): null인 경우 제일 첫번째
  • Query
    // JPQL
    /* 
    select 
        member1 
    from 
        Member member1 
    where 
        member1.age = 100
    order by 
        member1.age desc, 
        member1.userName asc nulls last 
    */ 
    
    // SQL Query
    select
        member0_.member_id as member_i1_1_,
        member0_.age as age2_1_,
        member0_.team_id as team_id4_1_,
        member0_.user_name as user_nam3_1_ 
    from 
        member member0_ 
    where 
        member0_.age=100 
        order by member0_.age desc,
        case 
            when member0_.user_name is null 
            then 1 
            else 0 
        end, 
        member0_.user_name asc;​

    • order by member0.age desc, member0.user_name asc 를 확인할 수 있다
    • nullsLast()에 해당하는 부분이 case ... end로 묶여 user_name 속성이 null일 경우 예외 조건을 만들어냈다

4-4. 페이징

  • 페이징 쿼리를 만드는 방법
    • offset ... limit ...  메서드를 이용하여 명시적으로 조회 ( count는 따로 조회해야 한다 )
    • fetchResults를 이용하여 QueryDSL 자체적으로 페이징 조회 ( count까지 포함 )
  • Code ( totalCount와 실제 Contents 따로 조회 )
       
    // 페이징 테스트
    @Test
    void pagingTest() {
        // when
        long totalCount = jpaQueryFactory
            .selectFrom(member)
            .fetchCount();
    
        List<Member> members = jpaQueryFactory
            .selectFrom(member)
            .orderBy(member.userName.desc())
            .offset(1) // 얼마나 스킵할 것인지
            .limit(2) // 몇개나 가져올 것인지
            .fetch();
    
        // then
        assertThat(totalCount).isEqualTo(4);
        assertThat(members).hasSize(2);
        assertThat(members)
            .extracting("userName")
            .containsSequence("memberC", "memberB");
    }​
  • Query ( totalCount와 실제 Contents 따로 조회  )
    # JPQL
    /* 
    select 
        count(member1)
    from 
        Member member1 
    */ 
    
    # Query
    select 
        count(member0_.member_id) as col_0_0_ 
    from member member0_;
    
    
    
    
    # JPQL
    /* 
    select 
        member1
    from 
        Member member1
    order by member1.userName desc 
    */ 
    
    # Query
    select 
        member0_.member_id as member_i1_1_,
        member0_.age as age2_1_,
        member0_.team_id as team_id4_1_,
        member0_.user_name as user_nam3_1_ 
    from 
        member member0_ 
    order by 
        member0_.user_name desc 
    limit 1, 2;​
  • Code ( fetchResults 이용 )
        
    // 페이징 조회 ( fetchResults 이용 )
    @Test
    void pagingTestWithFetchResults() {
        // when
        QueryResults<Member> queryResults = jpaQueryFactory
            .selectFrom(member)
            .orderBy(member.userName.desc())
            .offset(1)
            .limit(2)
            .fetchResults();
    
        // then
        assertThat(queryResults.getTotal()).isEqualTo(4);
        assertThat(queryResults.getResults())
            .hasSize(2)
            .extracting("userName")
            .containsSequence("memberC", "memberB");
    }​

    • 실제 발생하는 쿼리는 앞서 살펴본 totalCount와 실제 Contents 따로 조회했을 때 쿼리와 같다

4-5. 집합

  • GroupBy ... , Having ... 을 이용한 그룹핑과 각종 집합 함수를 제공합니다
  • Code
    // aggregation test
    @Test
    void aggregationTest() {
        // when
        List<Tuple> list = jpaQueryFactory
            .select(
                member.count(),
                member.age.sum(),
                member.age.avg(),
                member.age.max(),
                member.age.min()
            )
            .from(member)
            .fetch();
    
        // then
        Tuple actual = list.get(0);
        assertThat(actual.get(member.count())).isEqualTo(4);
        assertThat(actual.get(member.age.sum())).isEqualTo(60);
        assertThat(actual.get(member.age.avg())).isEqualTo(15);
        assertThat(actual.get(member.age.max())).isEqualTo(20);
        assertThat(actual.get(member.age.min())).isEqualTo(10);
    }

    • member.count(), member.age.avg() 등 각 리턴타입이 다르기 때문에 반환 결과는 com.querydsl.core 패키지의 Tuple 객체가 된다
  • Query
        
    # JPQL
    /* 
    select
        count(member1),
        sum(member1.age),
        avg(member1.age),
        max(member1.age),
        min(member1.age) 
    from
        Member member1 
    */ 
    
    
    # SQL Query
    select
        count(member0_.member_id) as col_0_0_,
        sum(member0_.age) as col_1_0_,
        avg(member0_.age) as col_2_0_,
        max(member0_.age) as col_3_0_,
        min(member0_.age) as col_4_0_ 
    from
        member member0_

 

4-5-1. 집합 ( GroupBy ... Having 예제 )

  • GroupBy를 통해 대상을 묶을수도, 묶여진 대상들에 대해 Having으로 다시 제한을 걸수도 있다
  • Code
        
    // GroupBy ... Having ...
    @Test
    void groupByTest() {
        // when
        List<Tuple> actual = jpaQueryFactory
            .select(team.name, member.age.avg())
            .from(member)
            .join(member.team, team)
            .groupBy(team.name)
            .having(team.name.eq("teamA"))
            .fetch();
    
        // then
        Tuple teamA = actual.get(0);
        assertThat(teamA.get(team.name)).isEqualTo("teamA");
        assertThat(teamA.get(member.age.avg())).isEqualTo(15);
    }​

    • select: team의 name과 member의 age의 평균을 조회
    • from: member entity 대상
    • join: member와 team을 inner join
    • groupBy: team의 name을 그룹핑
    • having: team의 name으로 그룹핑된 결과 중 "teamA"의 값을 가진 결과만 조회
  • Query
        
    # JPQL
    /* 
    select
        team.name,
        avg(member1.age) 
    from
        Member member1   
    inner join
        member1.team as team 
    group by
        team.name 
    having
        team.name = ?1 
    */ 
    
    # SQL Query
    select
        team1_.name as col_0_0_,
        avg(member0_.age) as col_1_0_ 
    from
        member member0_ 
    inner join
        team team1_ 
        on member0_.team_id=team1_.id 
    group by
        team1_.name 
    having
        team1_.name=?​

 

 

5. 조인(Join)

5-1. 기본 조인

  • join 메서드 이용
  • 조인의 기본 문법은 첫 번째 파라미터에 조인 대상, 두 번째 파라미터에 별칭으로 사용할 Q-Type을 지정하면 된다
  • JPQL join과 같다
  • Code
       
    // join (inner join)
    @Test
    void joinTest() {
        // when
        List<Member> members = jpaQueryFactory
            .selectFrom(member)
            .join(member.team, team)
            .where(team.name.eq("teamA"))
            .fetch();
    
        // then
        assertThat(members)
            .hasSize(2)
            .extracting("userName", "age") // 대소문자 구분 조심
            .containsExactly(Tuple.tuple("memberA", 10), Tuple.tuple("memberB", 20));
    
        assertThat(members)
            .extracting("team")
            .extracting("name")
            .containsOnly("teamA");
    }​

    • join 메서드는 기본적으로 inner join ( 명시적으로 innerJoin 메서드를 사용해도된다 )
    • leftJoin 메서드는 left outer join, rightJoin 메서드는 right outer join 쿼리를 발생시킨다
  • Query
        
    # JPQL    
    /* 
    select
        member1 
    from
        Member member1   
    inner join
        member1.team as team 
    where
        team.name = ?1 
    */ 
    
    
    # SQL Query
    select
        member0_.member_id as member_i1_1_,
        member0_.age as age2_1_,
        member0_.team_id as team_id4_1_,
        member0_.user_name as user_nam3_1_ 
    from
        member member0_ 
    inner join
        team team1_ 
        on member0_.team_id=team1_.id 
    where
        team1_.name=?

5-2. Theta join ( Cross join )

  • 카테시안 조인 ( N * M )
  • FROM절의 명시된 모든 테이블의 데이터를 가져와 WHERE절 조건으로 필터링
  • JPA의 연관관계가 설정되지 않았어도 join은 가능하다
  • Code ( 살짝 억지스럽게.. member의 username과 Team의 name이 같은 경우로... )
    // theta join
    @Test
    void thetaJoinTest() {
        // given
        em.persist(new Member("teamA"));
        em.persist(new Member("teamB"));
        em.persist(new Member("teamC"));
    
        em.flush();
        em.clear();
    
        // when
        List<Member> actual = jpaQueryFactory
            .select(member)
            .from(member, team)
            .where(member.userName.eq(team.name))
            .fetch();
    
        // then
        assertThat(actual)
            .hasSize(2)
            .extracting("userName")
            .containsExactly("teamA", "teamB");
    }
    • 명시적으로 join 같은 메서드를 호출하진 않았지만 from절에 두 개의 엔티티(테이블)이 들어간 것을 확인할 수 있다
  • Query
        
    # JPQL    
    /* 
    select
        member1 
    from
        Member member1,
        Team team 
    where
        member1.userName = team.name 
    */ 
    
    # SQL Query
    select
        member0_.member_id as member_i1_1_,
        member0_.age as age2_1_,
        member0_.team_id as team_id4_1_,
        member0_.user_name as user_nam3_1_ 
    from
        member member0_ 
    cross join
        team team1_ 
    where
        member0_.user_name=team1_.name​

5-3. ON절

  • ON절을 이용한 조인 ( JPA 2.1 부터 지원, Hibernate 5.1 이상 )
    • 조인 대상 필터링
    • 연관관계가 없는 엔티티 외부 조인 ( 해당 목적으로 자주 쓰임 )
  • 단 inner join일 경우는 on절로 필터링 하는 것과 Where 조건절을 추가해서 필터링하는 것이 동일하다
  • on절을 활용한 조인 대상 필터링을 사용할 때, 내부 조인(Inner join)이면 익숙한 where절로 해결하고, 정말 외부 조인(outer join)이 필요한 경우에만 on절 기능을 사용하자
  • Code ( 조인 대상 필터링 케이스 )
        
    // on절
    @Test
    void joinOnFiltering() {
        // when
        List<com.querydsl.core.Tuple> actual = jpaQueryFactory
            .select(member, team)
            .from(member)
            .leftJoin(member.team, team)
            .on(team.name.eq("teamA")) // join해서 가져올지 말지 필터링
            .fetch();
    
        // then
        assertThat(actual).hasSize(4);
        List<Team> teams = actual.stream()
                                 .map(tuple -> tuple.get(team))
                                 .filter(Objects::nonNull)
                                 .collect(Collectors.toList());
    
        assertThat(teams)
            .hasSize(2)
            .extracting("name")
            .containsOnly("teamA");
    }​

    • on절은 outer join한 결과를 가져올지 말지를 결정한다
  • Query
        
    # JPQL
    /* 
    select
        member1,
        team 
    from
        Member member1   
    left join
        member1.team as team 
        with team.name = ?1 
    */ 
    
    # SQL Query
    select
        member0_.member_id as member_i1_1_0_,
        team1_.id as id1_2_1_,
        member0_.age as age2_1_0_,
        member0_.team_id as team_id4_1_0_,
        member0_.user_name as user_nam3_1_0_,
        team1_.name as name2_2_1_ 
    from
        member member0_ 
    left outer join
        team team1_ 
        on member0_.team_id=team1_.id 
        and (
            team1_.name=?
        )​

    • JPQL에서 left join ... with ... 구문을 확인할 수 있다
    • SQL Query에서 left outer join ... on ... and ... , and 뒤에 붙은 조건이 on절로 지정해준 조건임을 확인할 수 있다
  • Code ( 연관관계가 없는 엔티티 외부 조인 )
        
    // 연관관계가 없다고 가정하고 left join
    @Test
    void joinWithoutRelationTest() {
        // given
        em.persist(new Member("teamA"));
        em.persist(new Member("teamB"));
        em.persist(new Member("teamC"));
    
        em.flush();
        em.clear();
    
        // when
        List<com.querydsl.core.Tuple> actual = jpaQueryFactory
            .select(member, team)
            .from(member)
            .leftJoin(team) // 연관관계 조인이랑 문법이 다르다. member.team이 아니라 team이다
            .on(member.userName.eq(team.name))
            .fetch();
    
        // then
        List<Team> actualTeams = actual.stream()
                                       .map(innerActual -> innerActual.get(team))
                                       .filter(Objects::nonNull)
                                       .collect(Collectors.toList());
    
        assertThat(actualTeams)
            .hasSize(2)
            .extracting("name")
            .containsExactly("teamA", "teamB");
    }​

    • leftJoin 메서드
      • 기존에 Member와 Team의 연관관계로 join을 만들땐 leftjoin(member.team, team)으로 소속을 나타내줬다, 이럴 경우 SQL Query는 Member 엔티티의 @Id값과 Team 엔티티의 @Id값으로 조인을 한다
      • 하지만 연관관계가 없는 경우 바로 Q-Type 클래스를 적어주며, 이는 on절로 명시된 조건으로만 join을 걸게 된다
  • Query
        
    # JPQL
    /* 
    select
        member1,
        team 
    from
        Member member1   
    left join
        Team team 
        with member1.userName = team.name 
    */ 
    
    # SQL Query
    select
        member0_.member_id as member_i1_1_0_,
        team1_.id as id1_2_1_,
        member0_.age as age2_1_0_,
        member0_.team_id as team_id4_1_0_,
        member0_.user_name as user_nam3_1_0_,
        team1_.name as name2_2_1_ 
    from
        member member0_ 
    left outer join
        team team1_ 
        on (
            member0_.user_name=team1_.name
        )
    • 기존 연관관계를 이용하여(member.team) leftJoin을 사용했을 경우에는 아래처럼 join되는 entity의 id값 매칭과 on절 추가 조건이 매칭됐다
      // on절
      on member0_.team_id=team1_.id 
      and (
          team1_.name=?
      )​
    • 하지만 연관관계가 없기 때문에 on절의 조건이 단순히 on method에 정의해준 조건만 매칭된 것을 확인해볼 수 있다

 

5-4. FETCH JOIN

  • SQL에서 제공하는 문법이 아닌 JPA에서 제공하는 fetch join 기능
  • 연관관계로 맺어진 객체를 같이 조회하는 것 ( Lazy 상태의 연관관계 매핑인 엔티티들 대상 )
  • Code
        
    // fetch join
    
    @PersistenceUnit
    private EntityManagerFactory emf;
    
    @Test
    void fetchJoinUseTest() {
        // given
        em.flush();
        em.clear();
    
        // when
        Member findMember = jpaQueryFactory
            .selectFrom(member)
            .join(member.team, team)
            .fetchJoin()
            .where(member.userName.eq("memberA"))
            .fetchOne();
    
        boolean loaded = emf.getPersistenceUnitUtil().isLoaded(findMember.getTeam());
        assertThat(loaded)
            .as("페치 조인 미적용")
            .isTrue();
    }​

    • @PersistenctUnit을 이용해 EntityManagerFactory를 주입
    • EntityManagerFactory가 가지고있는 util중 isLoadded를 이용해 해당 객체가 로딩됐는지 아닌지를 판단할 수 있다
    • join(), leftJoin(), rightJoin() 등 어떤것을 사용해도되며 뒤에 fetchJoin() 메서드만 추가해주면 된다
  • Query
       
    # JPQL
    /* 
    select
        member1 
    from
        Member member1   
    inner join
        member1.team as team 
    where
        member1.userName = ?1 
    */ 
    
    # SQL Query
    select
        member0_.member_id as member_i1_1_,
        member0_.age as age2_1_,
        member0_.team_id as team_id4_1_,
        member0_.user_name as user_nam3_1_ 
    from
        member member0_ 
    inner join
        team team1_ 
        on member0_.team_id=team1_.id 
    where
        member0_.user_name=?​

6. 서브 쿼리

  • 쿼리안에 또 다른 쿼리를 넣는 방식
  • com.querydsl.jpa.JPAExpressions 사용
  • 사용하는 본 쿼리와 안의 서브 쿼리의 alias는 겹치면 안된다
  • 예제 코드 1: 가장 나이가 많은 멤버 조회
        
    // SubQuery
    @Test
    void subQueryTest() {
        // given
        // 서브쿼리에서 쓸 Q-Type과 본 쿼리 Q-Type이 겹치면 안된다 ( static Q-Type 쓰면 alias가 같음 )
        // 서로의 alias가 달라야한다
        QMember memberSub = new QMember("memberSub");
    
        // when
        List<Member> actual = jpaQueryFactory
            .selectFrom(member)
            .where(member.age.eq(
                JPAExpressions
                    .select(memberSub.age.max())
                    .from(memberSub)
            ))
            .fetch();
    
        // then
        assertThat(actual)
            .hasSize(2)
            .extracting("userName", "age")
            .containsExactly(Tuple.tuple("memberB", 30), Tuple.tuple("memberC", 30));
    }​
  • 예제 1. Query
        
    # JPQL
    /* 
    select
        member1 
    from
        Member member1 
    where
        member1.age = (
            select
                max(memberSub.age) 
            from
                Member memberSub
        ) 
    */ 
    
    # SQL Query
    select
        member0_.member_id as member_i1_1_,
        member0_.age as age2_1_,
        member0_.team_id as team_id4_1_,
        member0_.user_name as user_nam3_1_ 
    from
        member member0_ 
    where
        member0_.age=(
            select
                max(member1_.age) 
            from
                member member1_
        )
  • 예제 2. Select절에 서브쿼리
    // SubQuery
    @Test
    void selectSubQueryTest() {
        // given
        QMember memberSub = new QMember("memberSub");
    
        // when
        List<com.querydsl.core.Tuple> tuples = jpaQueryFactory
            .select(member.userName,
                    JPAExpressions
                        .select(memberSub.age.avg())
                        .from(memberSub))
            .from(member)
            .fetch();
    
        // then
        tuples.forEach(System.out::println);
    }
  • JPA 사용시 서브쿼리의 한계
    • JPA에서는 FROM절에 서브쿼리가 안된다
    • SELECT절이나 WHERE절만 가능
    • JPA JPQL의 한계점으로 FROM절의 서브쿼리(인라인 뷰)는 지원하지 않는다, 당연히 QueryDSL도 지원하지 않음
    • 원래 JPA 표준 스펙으로는 SELECT절 서브쿼리도 안된다. 하이버네이트 구현체로는 사용 가능
  • FROM절 서브쿼리 해결방법
    • 서브쿼리를 join으로 바꿀 수 있으면 변경한다 ( 가능한 상황도 있고, 불가능한 상황도 있음 )
    • 어플리케이션에서 쿼리를 두 번으로 나눠서 실행한다 ( 상황에 따라 다름 )
    • 저 두 가지로 해결이 안되면 nativeSQL을 써라

 

7. CASE문

  • 표현 방식
    • 기본 단순 표현식: 단순한 값으로 WHEN을 걸때
    • CaseBuilder 이용: Q-Type을 이용해 좀 더 복잡하게 상황을 만들어야할 때
  • 예제 코드
        
    // simple case
    @Test
    void caseTest() {
    
        // when
        List<String> actual = jpaQueryFactory
            .select(member.age
                        .when(10).then("열살")
                        .when(20).then("스무살")
                        .otherwise("기타"))
            .from(member)
            .fetch();
    
        // then
        actual.forEach(System.out::println);
    }​
  • SQL Query
    # JPQL
    /* 
    select
        case 
            when member1.age = ?1 then ?2 
            when member1.age = ?3 then ?4 
            else '기타' 
        end 
    from
        Member member1 
    */ 
    
    # SQL Query
    select
        case 
            when member0_.age=? then ? 
            when member0_.age=? then ? 
            else '기타' 
        end as col_0_0_ 
    from
        member member0_
  • 예저 코드: CaseBuilder 사용
        
    // CaseBuilder
    @Test
    void usingCaseBuilderTest() {
        // when
        List<String> actual = jpaQueryFactory
            .select(new CaseBuilder()
            .when(member.age.between(0, 20)).then("10살에서 20살")
            .when(member.age.between(21, 30)).then("21살에서 30살")
            .otherwise("기타"))
        .from(member)
        .fetch();
    
        actual.forEach(System.out::println);
    }​
  • 추가
    • 가급적.. 이런 Case문 같은건 DB에서 하지말자
    • DB는 플랫한 데이터를 추출하는 용도로 사용하고 어플리케이션에서 분류하는 형태를 지향하자 ( 물론.. 성능상 case가 좋은 경우는 뭐... )

 

8. 상수, 문자 더하기

  • 상수 사용 ( com.querydsl.core.types.dsl.Expressions 사용 )
        
    // 상수
    @Test
    void expressionsTest() {
        List<Tuple> actual = jpaQueryFactory
            .select(member.userName, Expressions.constant("A"))
            .from(member)
            .fetch();
    
        actual.forEach(System.out::println);
    }​
  • 문자열 추가
    
    // concat
    @Test
    void concatTest() {
        // when
        List<String> actual = jpaQueryFactory
            .select(member.userName.concat("_").concat(member.age.stringValue()))
            .from(member)
            .fetch();
    
        // then
        actual.forEach(System.out::println);
    }

    • member.userName과 member.age는 서로 다른 타입이다
    • 따라서 member.age.stringValue()로 타입 캐스팅한 것을 확인할 수 있다 ( Enum 처리할 때 자주 사용된다 )

 

728x90
반응형