3 분 소요


도메인 모델과 테스트 기본 코드는 전 포스트 내용을 이어간다.
예제 도메인 모델


목차

  • 결과 조회
    • fetch()
    • fetchOne()
    • fetchFirst()
  • 정렬
  • 페이징
  • 집합
    • 집합 함수
    • GroupBy
  • 조인
    • 기본 조인
    • 세타 조인
    • ON절
    • 패치 조인


결과 조회

  • fetch() : 리스트 조회, 데이터 없으면 빈 리스트 반환
  • fetchOne() : 단 건 조회
    • 결과가 없으면 : null
    • 결과가 둘 이상이면 : com.querydsl.core.NonUniqueResultException
  • fetchFirst() : limit(1).fetchOne()
//List
List<Member> fetch = queryFactory
        .selectFrom(member)
        .fetch();

//단 건
Member findMember1 = queryFactory
        .selectFrom(member)
        .fetchOne();

//처음 한 건 조회
Member findMember2 = queryFactory
        .selectFrom(member)
        .fetchFirst();


정렬

  • 회원 정렬 순서
  • 1.회원 나이 내림차순(desc)
  • 2.회원 이름 올림차순(asc)
  • 단 2에서 회원 이름이 없으면 마지막에 출력(nulls last)
    @Test
    void sort() {
      em.persist(new Member(null, 100));
      em.persist(new Member("member5", 100));
      em.persist(new Member("member6", 100));
    
      List<Member> result = queryFactory
              .selectFrom(member)
              .where(member.age.eq(100))
              .orderBy(member.age.desc(), member.username.asc().nullsLast())
              .fetch();
    }
    
  • desc(), asc() : 일반 정렬
  • nullsLast(), nullsFirst() : null 데이터 순서 부여


페이징

조회 건수 제한

@Test
void Paging() {
    List<Member> result = queryFactory
            .selectFrom(member)
            .orderBy(member.username.desc())
            .offset(1)
            .limit(2) // 최대 2건 조회
            .fetch();
}


집합

집합 함수

/**
 * JPQL
 * select
 * COUNT(m), //회원수
 * SUM(m.age), //나이 합
 * AVG(m.age), //평균 나이
 * MAX(m.age), //최대 나이
 * MIN(m.age) //최소 나이
 * from Member m
 */

 @Test
 void aggregation() {
    List<Tuple> result = queryFactory
            .select(member.count(),
                    member.age.sum(),
                    member.age.avg(),
                    member.age.max(),
                    member.age.min())
            .from(member)
            .fetch();
 }
  • JPQL이 제공하는 모든 집합 함수를 제공한다.
  • tuple은 여러 타입을 받아올 때 사용한다고 보면 된다.(자세한 내용은 뒤에서…)

GroupBy

// 팀의 이름과 각 팀의 평균 연령을 구해라.
@Test
void group() {
    List<Tuple> result = queryFactory
            .select(team.name, member.age.avg())
            .from(member)
            .join(member.team, team)
            .groupBy(team.name)
            .fetch();
}
  • groupBy() 그룹화된 결과를 제한하려면 having

GroupBy, having 예시

...
.groupBy(item.price)
.having(item.price.gt(1000))
...


조인

기본 조인

join(조인 대상, 별칭으로 사용할 Q타입)
// 팀 A에 소속된 모든 회원
@Test
void join() {
    List<Member> result = queryFactory
            .selectFrom(member)
            .join(member.team, team)
            .where(team.name.eq("teamA"))
            .fetch();

    assertThat(result)
            .extracting("username")
            .containsExactly("member1", "member2");
}
  • join(), innerJoin() : 내부 조인(inner join)
  • leftJoin() : left 외부 조인(left outer join)
  • rightJoin() : right 외부 조인(right outer join)
  • JPQL의 on과 성능 최적화를 위한 fetch조인 제공

  • extracting() : 특정 필드를 추출하여 테스트

세타 조인

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

/**
 * 세타 조인(연관관계가 없는 필드로 조인)
 * 회원의 이름이 팀 이름과 같은 회원 조회
 */
@Test
void theta_join() {
    em.persist(new Member("teamA"));
    em.persist(new Member("teamB"));

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

    assertThat(result)
            .extracting("username")
            .containsExactly("teamA", "teamB");
}
  • from 절에 여러 엔티티를 선택해서 세타 조인
  • 외부 조인 불가능 -> on을 사용하면 외부 조인 가능

ON절

  • ON절을 활용한 조인(JPA 2.1부터 지원)
    1. 조인 대상 필터링
    2. 연관관계 없는 엔티티 외부 조인

조인 대상 필터링

/*
    * 회원과 팀을 조인하면서, 팀 이름이 teamA인 팀만 조인, 회원은 모두 조회
    * JPQL : select m, t from Member m left join m.team t on t.name = "teamA"
*/
@Test
void join_on_filtering() {
    List<Tuple> result = queryFactory
            .select(member, team)
            .from(member)
            .leftJoin(member.team, team).on(team.name.eq("teamA"))
            .fetch();
}

결과

t=[Member(id=3, username=member1, age=10), Team(id=1, name=teamA)]
t=[Member(id=4, username=member2, age=20), Team(id=1, name=teamA)]
t=[Member(id=5, username=member3, age=30), null]
t=[Member(id=6, username=member4, age=40), null]

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

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

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

    List<Tuple> result = queryFactory
            .select(member, team)
            .from(member)
            .leftJoin(team).on(member.username.eq(team.name))
            .fetch();
}
  • 주의! 문법을 잘 봐야한다. leftJoin()부분에 일반 조인과 다르게 엔티티 하나만 들어간다.
    • 일반 조인: leftJoin(member.team, team)
    • on 조인 : from(member).leftJoin(team).on(xxx)

결과

t=[Member(id=3, username=member1, age=10), null]
t=[Member(id=4, username=member2, age=20), null]
t=[Member(id=5, username=member3, age=30), null]
t=[Member(id=6, username=member4, age=40), null]
t=[Member(id=7, username=teamA, age=0), Team(id=1, name=teamA)]
t=[Member(id=8, username=teamB, age=0), Team(id=2, name=teamB)]

패치 조인

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

패치 조인 미적용

@PersistenceUnit EntityManagerFactory emf;

// 지연 로딩으로 Member, Team SQL 쿼리 각각 실행
@Test
void no_fetchJoin() {
    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
void use_fetchJoin() {
    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()이라고 추가하면 된다.


<출처 : 인프런 - 실전! Querydsl(김영한)>

댓글남기기