Querydsl - 기본 문법(3)

YulHee Kim·2022년 1월 9일
0

Querydsl

목록 보기
4/6

김영한님의 '실전! Querydsl'을 수강하며 정리하는 글입니다

기본 문법

✔️조인 - on절

on 절을 활용한 조인입니다.

조인 대상 필터링

예) 팀원과 팀을 조인하면서, 팀 이름이 teamA인 팀만 조인, 회원은 모두 조회

    /**
     * 예) 회원과 팀을 조인하면서, 팀 이름이 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)
                //.join(member.team, team)
                //.where(team.name.eq("teamA"))
                .leftJoin(member.team, team).on(team.name.eq("teamA"))
                .fetch();

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

결과

만약 여기서 leftJoin이 아닌 그냥 join을 사용하면 결과는 팀 이름이 null값인 회원들은 조회하지 않게됩니다.

또한 on절을 활용해 조인 대상을 필터링 할 때, 외부조인이 아니라 내부조인(inner join)을 사용하면, where절에서 필터링하는것과 기능이 동일합니다.

연관관계 없는 엔티티 외부 조인

예) 회원의 이름과 팀의 이름이 같은 대상 외부 조인

    /**
     * 연관 관계 없는 엔티티 외부 조인
     * 회원의 이름과 팀 이름이 같은 대상 외부 조인
     * */
    @Test
    public void join_on_no_relation() {
        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)

결과

✔️조인 - 페치조인

페치 조인은 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());
        assertThat(loaded).as("패치 조인 미적용").isFalse();
    }

페치 조인 적용

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

    @Test
    public void fetchJoinUse() {
        em.flush();
        em.clear();

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

        boolean loaded = emf.getPersistenceUnitUtil().isLoaded(findMember.getTeam());
        assertThat(loaded).as("패치 조인 적용").isTrue();
    }
  • join(), leftJoin() 등 조인 기능 뒤에 fetchJoin()이라고 추가하면 된다.

✔️서브 쿼리

com.querydsl.jpa.JPAExpressions 사용
밑에 예제들은 JPAExpressions를 static import한 상태입니다.

서브 쿼리 eq 사용

    /**
     * 나이가 가장 많은 회원 조회
     */
    @Test
    public void subQuery() {

        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);
    }

서브 쿼리 goe 사용

    /**
     * 나이가 평균 이상인 회원
     */
    @Test
    public void subQueryGoe() {

        QMember memberSub = new QMember("memberSub");

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

        assertThat(result).extracting("age")
                .containsExactly(30, 40);
    }

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

    @Test
    public void subQueryIn() {

        QMember memberSub = new QMember("memberSub");

        List<Member> result = queryFactory
                .selectFrom(member)
                .where(member.age.in(
                        select(memberSub.age)
                                .from(memberSub)
                                .where(memberSub.age.gt(10))
                ))
                .fetch();

        assertThat(result).extracting("age")
                .containsExactly(20, 30, 40);
    }

select 절에 subquery

    @Test
    public void selectSubQuery() {

        QMember memberSub = new QMember("memberSub");

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

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

    }

from 절의 서브쿼리 한계

JPA JPQL 서브쿼리의 한계점으로 from 절의 서브쿼리(인라인 뷰), Querydsl를 지원하지 않습니다. 하이버네이트 구현체를 사용하면 select 절의 서브쿼리는 지원합니다. Querydsl도 하이버네이트 구현체를 사용하면 select절의 서브쿼리를 지원합니다.

from 절의 서브쿼리 해결방안

  1. 서브쿼리를 join으로 변경한다.(가능한 상황도 있고, 불가능한 상황도 있다)
  2. 애플리케이션에서 쿼리를 2번 분리해서 실행한다.
  3. nativeSQL을 사용한다.

✔️Case 문

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

단순한 조건

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

        for (String s : result) {
            System.out.println("s = " + s);
        }
    }

복잡한 조건

    @Test
    public void complexCase() {
        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();

        for (String s : result) {
            System.out.println("s= " + s);
        }
    }

orderBy에서 Case문 함께 사용하기 예제

예를 들어 다음과 같은 순서로 회원을 출력하고 싶다면?
1. 0~30살이 아닌 회원을 가장 먼저 출력
2. 0~20살 회원 출력
3. 21~30살 회원 출력

    @Test
    public void orderByCase() {
        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);
        }

    }

✔️상수, 문자 더하기

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

 Tuple result = queryFactory
            .select(member.username, Expressions.constant("A"))
            .from(member)
            .fetchFirst();

문자 더하기 concat

 String result = queryFactory
            .select(member.username.concat("_").concat(member.age.stringValue()))
            .from(member)
            .where(member.username.eq("member1"))
            .fetchOne();

문자가 아닌 다른 타입들은 stringValue()로 문자로 변환할 수 있다. ENUM을 처리할 때도 자주 사용한다.

profile
백엔드 개발자

0개의 댓글