[Query DSL] 기본 문법 ②

홍정완·2022년 8월 10일
0

JPA

목록 보기
25/38
post-thumbnail

조인 - 기본 조인


조인의 기본 문법은 첫 번째 파라미터에 조인 대상을 지정하고,
두 번째 파라미터에 별칭(alias)으로 사용할 Q 타입을 지정하면 된다.

@Test
public void join() throws Exception {
 	QMember member = QMember.member;
 	QTeam team = QTeam.team;
    
 	List<Member> result = queryFactory
 			.selectFrom(member)
 			.join(member.team, team)
 			.where(team.name.eq("teamA"))
 			.fetch();
 
 	assertThat(result)
 			.extracting("username")
 			.containsExactly("member1", "member2");
}

👉 Querydsl에서는 JOIN 함수도 역시 제공된다.

  • join() , innerJoin() : 내부 조인(inner join)
  • leftJoin() : left 외부 조인(left outer join)
  • rightJoin() : right 외부 조인(right outer join)
  • JPQL의 on 과 성능 최적화를 위한 fetch 조인 제공

그렇다면 연관관계가 없는 엔티티 간의 조인은 어떻게 하는가 ❓ → 세타 조인


/**
 * 세타 조인
 * 팀 이름과 이름이 같은 회원 조회
 */
@Test
public void theta_join() throws Exception {
  
    em.persist(new Member("teamA"));
    em.persist(new Member("teamB"));
    em.persist(new Member("teamC"));

    List<Member> result = queryFactory
            .select(member)
            .from(member, team)
            .where(member.username.eq(team.name))
            .fetch();

    assertThat(result)
            .extracting("username")
            .containsExactly("teamA", "teamB");
}
  • from 절에서 여러 엔티티를 선택해서 세타 조인이 가능하다.
  • 전혀 연관관계가 없는 필드로도 조인이 가능하다.
  • 외부 조인(leftJoin, rightJoin)이 불가능한데 on 절을 사용해서 외부 조인이 가능하다.



조인 - on 절


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

  • 조인 대상 필터링
  • 연관관계없는 엔티티 외부 조인

1. 조인 대상 필터링

  • 회원과 팀을 조인하면서, 팀 이름이 teamA인 팀만 조인, 회원은 모두 조회
  • JPQL: select m, t from Member m left join m.team t on t.name = 'teamA';
@Test
public void join_on_filtering() throws Exception {
    List<Tuple> result = queryFactory
            .select(member, team)
            .from(member)
            .leftjoin(member.team, team).on(team.name.eq("teamA"))
            .fetch();
            
    for (Tuple tuple : result) {
        System.out.println("tuple = " + tuple);
    }
}
/* 실행 결과 */
/*
tuple = [Member(id=3, username=member1, age=10), Team(id=1, name=teamA)]
tuple = [Member(id=4, username=member2, age=20), Team(id=1, name=teamA)]
tuple = [Member(id=5, username=member3, age=30), null]
tuple = [Member(id=6, username=member4, age=40), null]
*/
  • on 절을 사용하여 조인 대상을 필터링할 때 내부 조인(inner Join)을 사용하면 where 절에서 필터링하는 것과 기능이 동일하다. 따라서 on 절을 활용한 조인 대상 필터링을 사용할 때, 내부 조인 이면 익숙한 where 절로 해결하고, 정말 외부 조인이 필요한 경우에만 이 기능을 사용하자.
.leftjoin(member.team, team).on(team.name.eq("teamA")) ->
.join(member.team, team).on(team.name.eq("teamA"))



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

  • 회원의 이름과 팀의 이름이 같은 대상 외부 조인
@Test
public void join_on_no_relation() throws Exception {
 
    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 자체적으로 제공하는 기능은 아니고 JPA에서 SQL 조인을 활용해서 연관된 엔티티를 SQL 한 번에 조회해 가져오는 기능입니다.
성능 최적화를 위해 엔티티 연관관계에서 모든 로딩 전략을 지연 로딩(fetch = fetchType.LAZA)으로 설정하는데, 페치조인을 사용하면, getter 호출 시마다 쿼리를 수행하는 것을 막을 수 있습니다.


Before:: 페치 조인 미적용

@PersistenceUnit
EntityManagerFactory emf;

@Test
public void fetchJoinNo() throws Exception {
    em.flush();
    em.clear();

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

    boolean loaded = emf.getPersistenceUnitUtil().isLoaded(member1.getTeam());
    
    assertThat(loaded).as("페치 조인 미적용").isFalse();
}

/* 수행 쿼리 */
/*
	select member1
	from Member member1
	where member1.username = 'member1'
*/
  • Member Entity만 조회되었을 뿐 연관관계에 있는 Team Entity는 조회되지 않았다.

👉 그렇기 때문에 assertThat(loaded).as("페치 조인 미적용").isFalse(); 은 통과된다.



After::페치 조인 적용

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


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

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

    boolean loaded = emf.getPersistenceUnitUtil().isLoaded(member1.getTeam());
    
    assertThat(loaded).as("페치 조인 적용").isTrue();
}

  • join(member.team, team) 옆을 보면 .fetchJoin()을 호출하는 것을 볼 수 있다.
    이렇게 fetchJoin()을 호출해 주면 연관관계에 있는 Team Entity도 함께 조회하기 때문에 loaded 가 true가 된 것을 확인할 수 있다.



서브 쿼리


👉 com.querydsl.jpa.JPAExpressions를 사용하여 서브 쿼리 사용이 가능하다.


서브 쿼리 eq 사용

@Test
public void subQuery() throws Exception {
    QMember memberSub = new QMember("memberSub");

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

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

서브 쿼리 goe 사용

@Test
public void subQueryGoe() throws Exception {
    QMember mSub = new QMember("memberSub");

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

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

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

@Test
public void subQueryIn() throws Exception {
    QMember mSub = new QMember("memberSub");

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

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

서브 쿼리 select 절에서 select 사용

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

    List<Tuple> fetch = queryFactory
            .select(member.username,
                    select(mSub.age.avg())
                            .from(mSub)
            )
            .from(member)
            .fetch();
    for (Tuple tuple : fetch) {
        System.out.println("tuple = " + tuple);
    }
}

✅ from 절의 서브 쿼리 한계

👉 JPA JPQL 서브 쿼리의 한계점으로 from 절의 서브 쿼리(인라인 뷰)는 지원하지 않습니다.
JPQL 쿼리 빌더인 Querydsl 역시 같은 이유로 지원하지 않습니다.

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


✅ from 절의 서브 쿼리 한계돌파 방안

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



Case 문


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


단순한 조건

@Test
public void basicCase() throws Exception {
    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() throws Exception {
    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);
    }
}
  • CaseBuilder()를 통해 동작합니다.
  • basic한 case와는 다르게 when 절 안에 조건이 들어갑니다.



상수, 문자 더하기


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

@Test
public void constant() throws Exception {
    List<Tuple> result = queryFactory
            .select(member.username, Expressions.constant("A"))
            .from(member)
            .fetch();

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

✅ 위와 같이 최적화가 가능하면 SQL에 constant 값을 넘기지 않는다. 상수를 더하는 것처럼 최적화가 어려우면 SQL에 constant 값을 넘긴다



문자 더하기 concat

@Test
public void concat() throws Exception {
    List<String> result = queryFactory
            .select(member.username.concat("_").concat(member.age.stringValue()))
            .from(member)
            .fetch();

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

member.age.stringValue() : 문자가 아닌 다른 타입(ex: int, float..)들을 문자로 변환해 준다.

이 방법은 ENUM을 처리할 때도 자주 사용한다.

profile
습관이 전부다.

0개의 댓글