7 분 소요

📌 Topic

  • 조인 - 기본 조인
  • 조인 - on 절
  • 조인 - 페치 조인
  • 서브쿼리
  • CASE 문
  • 상수, 문자 더하기

01. 조인 - 기본 조인

이번 시간에는 Querydsl이 제공하는 기본 조인에 대해 알아보자

01-1. 기본 조인

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

//ex
join(조인 대상, 별칭으로 사용할 Q타입)
//ex
List<Member> result = queryFactory
    .select(member)
    .from(member)
    .join(member.team, team)
    .where(team.name.eq("데이터 플랫폼 팀"))
    .fetch();

Querydsl을 사용한 기본 조인(inner join)의 형태는 위와 같다.

01-1. inner join 사용 예시

/**
 *  데이터 플랫폼 팀에 소속된 모든 회원을 조인하여 가져온다
 */
@Test
@DisplayName("조인 테스트")
public void join() throws Exception {
    List<Member> memberList = queryFactory
            .selectFrom(member)
            .join(member.team, team) // inner join 사용
            .where(team.name.eq("데이터 플랫폼 팀"))
            .fetch();

    for (Member member : memberList) {
        System.out.println("[TEST] member = " + member.toString());
    }

    assertThat(memberList)
            .extracting("userName")
            .containsExactly("김영민", "원영식");
}
  • inner join의 기본 형태
  • 첫 번째 파라미터에 조인 대상, 두 번째에는 Q 타입(별칭) 지정

결과는 아래와 같이 출력이 된다

[TEST] member = Member(id=4, userName=김영민, age=33)
[TEST] member = Member(id=5, userName=원영식, age=30)

01-2. left join

/**
 *  데이터 플랫폼 팀에 소속된 모든 회원을 조인하여 가져온다
 */
@Test
@DisplayName("조인 테스트")
public void join() throws Exception {
    List<Member> memberList = queryFactory
            .selectFrom(member)
            .leftJoin(member.team, team) // left join
            .where(team.name.eq("데이터 플랫폼 팀"))
            .fetch();

    for (Member member : memberList) {
        System.out.println("[TEST] member = " + member.toString());
    }

    assertThat(memberList)
            .extracting("userName")
            .containsExactly("김영민", "원영식");
}
  • Left join의 기본 형태
  • inner join과 형식은 동일하다, 왼쪽 테이블 기준 모든 데이터 조회

결과는 아래와 같이 출력이 된다

[TEST] member = Member(id=4, userName=김영민, age=33)
[TEST] member = Member(id=5, userName=원영식, age=30)

01-3. theta join

연관 관계가 없는 필드로 조인

우선 theta join에 대해 알아보기 전에
JPA의 연관 관계에 대해 간략히 짚고 넘어가자.

연관 관계란 아래와 같이 엔티티 필드 간의 관계가 맺어져있는 것을 얘기한다.

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
public class Student {

    @Id
    @GeneratedValue
    private Long id;

    private String userName;
    private int academyNo;

    // 연관 관계가 있는 필드
    @ManyToOne
    @JoinColumn(name = "academy_id", foreignKey = @ForeignKey(name = "fk_student_academy"))
    private Academy academy;
}

현재 Student 클래스의 academy 필드는 연관 관계 필드다.
Querydsl에서는 연관 관계 필드로만 join을 할 수 있다 알고 있지만
최근에는 연관 관계 없이 조인이 되는 기능을 지원한다.

/**
 * 세타 조인
 * 회원의 이름이 팀 이름과 같은 회원 조회
 */
@Test
@DisplayName("세타 조인 - 연관관계가 없는 필드로 조인")
public void theta_join() throws Exception {
    // 연관관계가 없는 경우 조인을 하는 방식
    // Team 객체가 아닌 Member 객체를 생성하는 것이다
    // Member 이름 -> 데이터 플랫폼 팀, Team 이름 -> 데이터 플랫폼 팀
    em.persist(new Member("데이터 플랫폼 팀"));
    em.persist(new Member("인프라 팀"));

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

    assertThat(result)
            .extracting("userName")
            .containsExactly("데이터 플랫폼 팀", "인프라 팀");
}

조인하고자 하는 테이블의 모든 행을 결합, 두 테이블의 행 갯수를 곱한 만큼의 결과 반환

  • 연관관계가 없는 필드로 조인하는 것을 의미한다
  • 세타 조인은 from 절에 테이블을 나열한다 (연관관계의 조인이 아님)
  • 실제 sql은 Cross join이 발생
  • 외부 조인은 불가능하나, JPQL의 on절을 통해 외부 조인이 가능함

02. 조인 - on 절

조인 on절의 경우 JPA 2.1부터 지원이 되는 기능이다.
해당 기능을 통해 처리할 수 있는 내용은 다음과 같다.

  1. 조인 대상 필터링
  2. 연관 관계가 없는 엔티티의 외부 조인 지원

02-1. 조인 대상 필터링

/**
* ex) 회원과 팀을 조인하면서, 팀 이름이 '데이터 플랫폼 팀'인 팀만 조회, 회원은 모두 조회
* JPQL
*  - select m, t
*      from Member m
*      left join m.team t
*      on t.name = 'teamA'
*/
@Test
@DisplayName("조인 - on 절 테스트")
public void join_on_filtering() throws Exception {
    List<Tuple> result = queryFactory
            .select(member, team)
            .from(member)
            .leftJoin(member.team, team)
            .on(team.name.eq("데이터 플랫폼 팀")) // left join 의 조건이 되주어 null로 남는다
            //.where(team.name.eq("데이터 플랫폼 팀")) // 전체 데이터에서 필터링(조건에 맞는 데이터만 출력)
            .fetch();

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

inner join의 경우엔 on절과 where의 차이가 없다. 하지만 outer join의 경우 on 절은 join의 조건이 되주어 null로 값이 셋팅 되고, where의 경우 전체 데이터를 대상으로 필터링을 해버린다. 결과는 다음과 같다.

// where, on 절 사용 안하고 전체 목록 출력
tuple = [Member(id=4, userName=김영민, age=33), Team(id=1, name=데이터 플랫폼 )]
tuple = [Member(id=5, userName=원영식, age=30), Team(id=1, name=데이터 플랫폼 )]
tuple = [Member(id=6, userName=김진엽, age=27), Team(id=2, name=인프라 )]
tuple = [Member(id=7, userName=박진우, age=28), Team(id=2, name=인프라 )]
tuple = [Member(id=8, userName=임수현, age=29), Team(id=2, name=인프라 )]

// join - where절 사용
tuple = [Member(id=4, userName=김영민, age=33), Team(id=1, name=데이터 플랫폼 )]
tuple = [Member(id=5, userName=원영식, age=30), Team(id=1, name=데이터 플랫폼 )]

// join - on 절 사용
tuple = [Member(id=4, userName=김영민, age=33), Team(id=1, name=데이터 플랫폼 )]
tuple = [Member(id=5, userName=원영식, age=30), Team(id=1, name=데이터 플랫폼 )]
tuple = [Member(id=6, userName=김진엽, age=27), null]
tuple = [Member(id=7, userName=박진우, age=28), null]
tuple = [Member(id=8, userName=임수현, age=29), null]

필터링이 수행되는 시점은 아래와 같다

  • ON: JOIN이 실행되기 전
  • WHERE: JOIN이 실행된 후

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

/**
 * 연관 관계가 없는 외부 조인
 * 회원의 이름이 팀 이름과 같은 대상을 외부 조인해라
 */
@Test
public void join_on_no_relation() throws Exception {
    em.persist(new Member("데이터 플랫폼 팀"));
    em.persist(new Member("인프라 팀"));

    // 연관 관계가 없는 경우
    List<Tuple> result = queryFactory
            .select(member, team)
            .from(member)
            .join(team) // 일반 조인과 다르게 엔티티 하나만 들어감
            .on(member.userName.eq(team.name)) // id값이 아닌 이름으로만 조인(필터링)
            .fetch();

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

이전에 세타 조인(theta join)에서는 외부 조인이 불가능하다고 했다.
또한 세타 조인에서 외부 조인을 사용할 일이 있으면 on 절을 사용하면 된다.
하지만 기존 세타 조인에서 달라지는 부분이 있는데 다음과 같다.

  • 하이버네이트 5.1부터 on 을 사용해 서로 관계 없는 필드 조인 기능 추가
  • leftjoin 부분에 일반 조인과 다르게 엔티티 하나만 들어간다
    • 일반조인: leftJoin(member.team, team)
    • on조인: leftJoin(team)

03. 조인 - 페치 조인

지연 로딩 설정 시에 N + 1 문제를 해결하기 위해 fetch join 사용

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

03-1. 페치 조인 미적용하여 테스트 코드 작성

@PersistenceUnit
EntityManagerFactory emf;

우선 패치 조인을 테스트 하기에 앞서 EntityManager를 생성 해주는
EntityManagerFactory 객체의 의존성 주입(DI)을 진행 한다.

@Test
@DisplayName("패치 조인 적용 안한 경우 테스트")
public void fetchJoinNo() throws Exception {
    em.flush(); // DB 현재 결과 반영
    em.clear(); // 영속성 컨텍스트 초기화

    // fetch join 미 반영
    Member findMember = queryFactory
            .select(member)
            .from(member)
            .where(member.userName.eq("김영민"))
            .fetchOne();

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

현재 query의 경우 이름이 ‘김영민’인 회원의 단일 데이터를 가져오는 쿼리다.
여기서 검증하고자 하는 부분은 Member 엔티티와 연관이 있는 Team 엔티티가
로딩 되었는지에 대한 여부를 검증하고자 한다.

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

현재 fetch 조인을 사용하지 않았기 때문에 해당 테스트 결과는 통과 해야 한다.

test_result

03-2. 페치 조인 적용하여 테스트 코드 작성

@Test
@DisplayName("패치 조인 적용 후 테스트")
public void fetchJoin() throws Exception {
    em.flush();
    em.clear();

    Member findMember = queryFactory
            .select(member)
            .from(member)
            .join(member.team, team).fetchJoin() // join 후 .fetchJoin()을 사용
            .where(member.userName.eq("김영민"))
            .fetchOne();

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

패치 조인에 대한 내용은 아래 링크를 참고하자.

.fetchJoin()을 사용하여 fetch join을 진행 한다.

04. 서브 쿼리

com.querydsl.jpa.JPAExpression 사용

04-1. 서브 쿼리 작성

나이가 가장 많은 회원을 조회 한다.

@Test
@DisplayName("서브 쿼리 테스트")
// @Rollback(false)
public void subQuery() throws Exception {
    // alias가 겹치면 안되기에 QMember를 생성
    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(33);
}

나이가 평균 이상인 회원 조회

@Test
@DisplayName("서브 쿼리 - 나이가 평균 이상인 회원 조회")
public void subQueryGoe() throws Exception {

    QMember memberSub = new QMember("memberSub");

    List<Member> result = queryFactory
            .selectFrom(member)
            // .where(member.age.goe(
            .where(member.age.goe(
                    JPAExpressions
                            .select(memberSub.age.avg())
                            .from(memberSub)
            ))
            .fetch();
    System.out.println("result = " + result.toString());

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

특정 나이에 포함되는 회원 조회

@Test
@DisplayName("서브 쿼리 - 특정 나이 포함 여부")
public void subQueryIn() throws Exception {

    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))
            ))
            .orderBy(member.age.desc())
            .fetch();
    System.out.println("result = " + result.toString());

    assertThat(result).extracting("age")
                        .containsExactly(33, 30, 29, 28, 27);
}

select절에 subquery 사용

@Test
@DisplayName("select - subquery")
public void selectSubQuery() throws Exception {
    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);
    }
}

04-2. JPA 서브쿼리의 한계

💡 : 생각해보니 정말 중요한 부분

  • JPA SubQuery(인라인뷰)는 FROM절에서 사용할 수 없다
  • Querydsl 역시 FROM절에 SubQuery(인라인뷰)를 지원하지 않는다
  • 하이버네이트 구현체를 사용하면 select 절의 서브쿼리는 지원한다
  • 결국 FROM절에는 서브쿼리 사용이 불가능하다는 의미

04-3. JPA 서브쿼리의 한계 극복

💡 : FROM절에 죽어도 서브쿼리를 사용해야 한다면 아래 예시 참고

  • 서브쿼리를 join으로 변경한다. (가능하거나, 불가능한 상황이 있음)
  • 애플리케이션에서 쿼리를 2번 분리하여 실행
  • NativeSQL을 사용한다
    • Mybatis 같이 사용

참고 자료

댓글남기기