Tools & Libraries/Querydsl

[Querydsl] Querydsl 기본문법 4 (on 절, 페치 조인, 서브쿼리)

대기업 가고 싶은 공돌이 2024. 9. 6. 02:07

on 절

조인 대상 필터링

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

 

//jpql: select m,t from Member m left join m.team t on t.name = 'teamA'
@Test
public void join_filtering(){
    JPAQueryFactory queryFactory = new JPAQueryFactory(em);

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

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

 

다음과 같이 join을 한 후에 on 절에 team.name = teamA로 걸면

 

 

team A만 나오는 것을 확인할 수 있다.

 

on 절을 쓰지 않고 where(team.name = "teamA")를 사용해도 결과는 동일하다.

 

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

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

 

List<Member> result = queryFactory.selectFrom(member)
        .leftJoin(member.team, team)
        .where(team.name.eq("teamA"))
        .fetch();

 

기존의 조인은 다음과 같이 member.team을 넣어줘서 member의 id와 team의 member id가 자동으로 매칭이 됐었다.

 

그러므로 연관관계가 없는 조인에서는 member.team을 사용하면 자동으로 id가 매칭되니 하면 안 된다.

 

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

연관관계가 없는 조인에선 다음과 같이 Member.team 대신 team만 넣어주고

조인 조건은 on 절에서 username == team.name으로 설정해준다.

 

일반 조인

leftJoin(member.team, team) // member.team을 입력함으로서 조인 조건에 기본적으로

member id == team.member id 가 걸림

 

on 조인

from(member).leftjoin(team).on(xxx)

그냥 team을 사용 하므로써 on 절의 조건만이 조인 조건에 들어감.

 

만약 leftjoin(member.team). on(xxx) 인 경우엔

다음과 같이 on 조건이 두 개가 된다.

 

페치 조인

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

    JPAQueryFactory queryFactory = new JPAQueryFactory(em);

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

    boolean loaded = emf.getPersistenceUnitUtil().isLoaded(findMember.getTeam());

    Assertions.assertThat(loaded).isFalse();
}

 

더보기

짧막한 테스트코드 작성 팁

 

@PersistenceUnit
EntityManagerFactory emf;

 

엔티티 매니저 팩토리에는 특정 엔티티가 영속성 컨텍스트에 존재하는지 안 하는지 판단할 수 있는 메소드가 존재한다.

 

boolean loaded = emf.getPersistenceUnitUtil().isLoaded(findMember.getTeam());

 

다음과 같이 emf 를 통해 is Loaded를 실행해 테스트 코드를 작성할 수도 있다.

위는 기존의 조회 쿼리다.

 

위와 같이 조회를 하면 해당 member entity만 조회를 하고 연관관계에 있는 다른 엔티티는 조회하지 않는다.

 

다른 엔티티들은 해당 엔티티에 접근할 때 쿼리가 나간다.

 

자세한 내용은 fetch join 글 참고 ,,

https://2junbeom.tistory.com/50

 

[JPA] 객체 지향 쿼리 언어 JPQL 5 (경로 표현식, fetch join)

경로 표현식.(점)을 찍어 객체 그래프를 탐색하는 것이다. select m.username -> 상태필드로 객체를 탐색한 경우from Member m   join m.team t -> 단일 값 연관 필드 (여기서 team은 엔티티기 때문에 단일 값을

2junbeom.tistory.com

따라서 한 번에 연관 엔티티를 전부 끌어오고 싶은 때는 다음과 같이 코드를 작성하면 된다.

 

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

    JPAQueryFactory queryFactory = new JPAQueryFactory(em);

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

    boolean loaded = emf.getPersistenceUnitUtil().isLoaded(findMember.getTeam());

    Assertions.assertThat(loaded).isTrue();
}

 

그저 한 번에 끌고 오고 싶은 테이블 전부를 조인시켜준 후 

다음에 .fetchjoin 하나만 적어주면 페치조인이 된다.

 

where절 서브 쿼리

com.querydls.jpa.JPAExpressions를 사용하면 된다.

 

예) 나이가 가장 많은 회원 조회

@Test
public void subQuery(){
    JPAQueryFactory queryFactory = new JPAQueryFactory(em);

    QMember memberSub = new QMember("memberSub");

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

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

 

where 절에 서브쿼리를 사용해줬다.

member. age. eq 이후 

JPAExpressions를 사용해서 서브쿼리를 작성한다.

 

하나 주의할 점은 별칭이 겹치면 안 되므로 위에 QMember를 하나 더 생성해서 별칭을 하나 더 만들어준 후

 

해당 별칭을 서브쿼리에 사용해줘야 한다.

 

다음 예시) 나이가 평균 이상인 회원 찾기

@Test
public void subQuery2(){
    JPAQueryFactory queryFactory = new JPAQueryFactory(em);

    QMember memberSub = new QMember("memberSub");

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

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

똑같이 where 절에 서브쿼리를 사용해줬다.

 

goe는 >=를 의미한다.

 

avg를 통해 평균을 구해 해당 평균보다 높은 애들만 찾아줬다.

 

In 사용해보기

@Test
public void subQueryIn(){
    JPAQueryFactory queryFactory = new JPAQueryFactory(em);

    QMember memberSub = new QMember("memberSub");

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

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

 

약간 억지스러운 코드지만, 예제니까 그냥 넘어가도록 하자.

 

단순하게 member.age.in 이후 서브쿼리에 10 초과를 넣어 20,30,40인 회원만 뽑아냈다.

 

스칼라 서브쿼리

@Test
public void selectSubQuery(){
    QMember memberSub = new QMember("memberSub");

    JPAQueryFactory queryFactory = new JPAQueryFactory(em);

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

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

 

서브쿼리는 다 동일하다 그냥 JPAExpressions를 사용해 원하는 자리에 서브 쿼리를 넣으면 된다.

username과 user의 나이 평균을 출력해주었다.

 

물론 JPAExpressions도 static 임포트를 통해 내부 메소드만 사용할 수도 있다.

import static com.querydsl.jpa.JPAExpressions.*;

 

다음과 같이 스태틱 임포트를 해주면

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

 

이렇게 JPAExpressions 없이도 서브쿼리를 사용할 수 있다.

 

JPA 서브쿼리의 한계

JPA JPQL 서브쿼리는 인라인뷰를 지원하지 않는다.

당연히 query dsl도 지원하지 않는다.

 

from 절의 서브쿼리 해결방안

  1. 서브쿼리를 join으로 변경한다. (가능하다면)
  2. 애플리케이션에서 쿼리를 2번 분리해서 실행한다.
  3. nativeSQL을 사용한다.

참고: 김영한 실전! Querydsl