[QueryDSL] 기본 문법3

윤경·2021년 12월 6일
1

QueryDSL

목록 보기
5/11
post-thumbnail

[9] 조인 - on절

(= 필터링)

ON절을 활용한 조인(JPA 2.1부터 지원함)

  1. 조인 대상 필터링
  2. 연관관계 없는 엔티티 외부 조인
    (첫번째 보다는 두번째 것이 필요할 때 많이 쓰임)

1. 조인 대상 필터링

    /**
     * 예) 회원과 팀을 조인하면서, 팀 이름이 teamA인 팀만 조인, 회원은 모두 조회
     * JPQL 문법으로는 select m, t from Member m left join m.team t on t.name = 'teamA'
     */
    @Test
    public void join_on_filtering() {
        List<Tuple> result = queryFactory
                .select(member, team)
                .from(member)
                .leftJoin(member.team, team)
//                .on(team.name.eq("teamA"))
                .where(team.name.eq("teamA"))   // 이렇게 하면 사실 on과 결과 똑같음
                .fetch();

        for (Tuple tuple : result) {
            System.out.println("tuple = " + tuple);
        }
    }

➡️ and로 팀의 이름까지 걸러서 찾을 수 있게 해줌

⚠️ on 절을 활용해 조인 대상을 필터링 할 때, 외부 조인이 아니라 내부 조인(Inner join)을 사용하면, where 절에서 필터링 하는 것과 기능이 동일하다.
따라서, On절을 활용한 조인 대상 필터링을 사용할 때, 내부 조인이면 익숙한 where 절로 해결하고, 정말 외부 조인이 필요한 경우에만 On 기능을 사용하자.

2. 연관관계가 없는 엔티티 외부 조인

    /**
     * 연관관계가 없는 엔티티 외부 조인
     * 회원 이름이 팀 이름과 같은 대상 외부 조인
     */
    @Test
    public void join_on_no_relation() {  // 연관관계가 없어도 join 할 수 있음
        em.persist(new Member("teamA"));
        em.persist(new Member("teamB"));
        em.persist(new Member("teamC"));

        List<Tuple> result = queryFactory
                .select(member, team)
                .from(member)
                .leftJoin(team).on(member.username.eq(team.name))
                .fetch();

        for (Tuple tuple : result) {
            System.out.println("tuple = " + tuple);
        }
    }

하이버네이트 5.1부터 on을 사용해 서로 관계가 없는 필드로 외부 조인하는 기능이 추가되었다. (물론 내부 조인도 가능)

⚠️ 문법에 주의
leftJoin() 부분에 일반 조인과 다르게 엔티티 하나만 들어간다.

  • 일반 조인: leftJoin(member.team, team)
  • on 조인: from(member).leftJoin(team).on(xxx) ➡️ leftJoin에 조인 대상이 바로 들어감

[10] 조인 - 페치 조인

(실무에서 정말 많이 사용)

페치 조인은 SQL에서 제공하는 기능은 아님
SQL 조인을 활용해 연관된 엔티티를 SQL 한번에 조회하는 기능
주로 성능 최적화에 사용하는 방법

페치 조인 미적용

지연 로딩으로 Member, Team SQL 쿼리 각각 실행

    @PersistenceUnit    // 엔티티 매니저를 만드는 팩토리라는 것이 있음
    EntityManagerFactory emf;

    @Test
    public void fetchJoinNo() { // 페치조인이 없을 때
        // 영속성 컨텍스트를 깔끔하게 날린 뒤 실행하겠음
        em.flush();
        em.clear();

        Member findMember = queryFactory
                .selectFrom(member)
                .where(member.username.eq("member1"))
                .fetchOne();

        boolean loaded = emf.getPersistenceUnitUtil().isLoaded(findMember.getTeam());
        // emf: getPersistenceUniUtil을 얻을 수 있음
        // isLoaded: 이미 로딩된 엔티티인지, 초기화가 안 된 엔티티인지 알려주는

        assertThat(loaded).as("페치 조인 미적용").isFalse();
    }

페치 조인 적용

즉시 로딩으로 Member, Team SQL 쿼리 조인으로 한번에 조회

...
                .join(member.team, team).fetchJoin()    // 이렇게 뒤에 fetchJoin만 적어두면 됨(간단)
...

나머지는 동일하고 이렇게 .join(...)(또는 leftJoin(...)) 뒤에 .fetchJoin()을 붙여주면 된다.

📌 페치 조인에 대한 자세한 내용은 이 포스트를 참고하자


[11] 서브 쿼리

: 쿼리 안에 쿼리를 넣겠다

com.querydsl.jpa.JPAExpressions 사용하면 됨

1. 서브 쿼리 eq 사용

    /**
     * 나이가 가장 많은 회원 조회
     */
    @Test
    public void subQuery() {    // 서브 쿼리 eq 사용
        QMember memberSub = new QMember("memberSub");

        List<Member> result = queryFactory
                .selectFrom(member)
                .where(member.age.eq(
                        select(memberSub.age.max())
                                .from(memberSub)
                ))
                .fetch();

        assertThat(result).extracting("age")
                .containsExactly(40);
    }
  • QMember memberSub = new QMember("memberSub");: alias(별명)가 중복되면 안되는 경우에는 이렇게 생성해주면 됨

  • .selectFrom(member): 여기의 member가 바깥에 있는 member와 겹치면 안되기 때문에 QMember를 생성해준 것

  • 아래의 네모 안의 값이 결국 40이 되는 것!

2. 서브 쿼리 goe 사용

3. 서브쿼리 여러 건 처리 in 사용

4. select 절에 subquery

5. static import 활용

(2~5 코드 생략. 깃허브 참고)

from 절의 서브쿼리 한계

JPA JPQL 서브 쿼리의 한계점으로 from 절의 서브쿼리(인라인 뷰)는 지원하지 않는다.
(물론 Querydsl도 지원하지 않음. querydsl이 뭐 용빼는 재주가 있는게 아님(?) JPQL이 안되면 그냥 querydsl에서는 당연히 안되는 것)

하이버네이트 구현체를 사용하면 select 절의 서브쿼리는 지원한다. QueryDSL도 하이버네이트 구현체를 사용하면 select 절의 서브쿼리를 지원한다.

결론: 다른건 다 되는데 from 절이 안됨

from 절의 서브쿼리 해결방안

  1. 서브쿼리를 join으로 변경한다. (불가능한 상황도 있는데 거의 됨)
  2. 애플리케이션에서 쿼리를 2번 분리해 실행한다.
  3. (이것도 저것도 안된다면) nativeSQL을 사용

[12] Case 문

: select, 조건절(where), order by에서 사용 가능

단순한 조건을 사용하는 방법

        List<String> result = queryFactory
                .select(member.age
                        .when(10).then("열살")
                        .when(20).then("스무살")
                        .otherwise("기타"))
                .from(member)
                .fetch();

복잡한 조건을 사용하는 방법

        List<String> result = queryFactory
                .select(new CaseBuilder()
                        .when(member.age.between(0, 20)).then("0~20살")
                        .when(member.age.between(21, 30)).then("21~30살")
                        .otherwise("기타"))
                .from(member)
                .fetch();

(사실 이런 과정은 DB에서 하기보다는 application 계층에서 해결하도록 해야함)

강의엔 없는 예제 살펴보기

📌 다음과 같은 임의의 순서로 회원 출력하기

  1. 0~30살이 아닌 회원을 가장 먼저 출력
  2. 0~20살 회원 출력
  3. 21~30살 회원 출력
    @Test
    public void orderByCase() { // orderBy에서 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();

        for (Tuple tuple : result) {
            String username = tuple.get(member.username);
            Integer age = tuple.get(member.age);
            Integer rank = tuple.get(rankPath);

            System.out.println("username = " + username + " age = " + age + " rank = " + rank);
        }
    }

Querydsl은 자바 코드로 작성하기 때문에 rankPath처럼 복잡한 조건을 변수로 선언해 select 절, orderBy 절에서 함께 사용할 수 있다.


[13] 상수, 문자 더하기

상수가 필요하면 Expressions.constant(xxx) 사용하기

최적화가 가능하면 SQL에 constant 값을 넘기지 않는다.
상수를 더하는 것처럼 최적화가 어려울 때 SQL에 constant 값을 넘긴다.

⚠️ member.age.stringValue() 부분이 중요한데, 문자가 아닌 다른 타입들은 stringValue()를 통해 문자로 변환할 수 있다. 이 방법은 ENUM을 처리할 때도 자주 사용한다.


profile
개발 바보 이사 중

0개의 댓글