[QueryDSL] 기본 문법 ②

kiteB·2021년 12월 3일
0

QueryDSL

목록 보기
4/9
post-thumbnail

[ 집합 ]

1. 집합 함수

/**
 * JPQL
 * select
 * COUNT(m), //회원수
 * SUM(m.age), //나이 합
 * AVG(m.age), //평균 나이
 * MAX(m.age), //최대 나이
 * MIN(m.age) //최소 나이
 * from Member m
 */
@Test
public void aggregation() throws Exception {
    List<Tuple> result = queryFactory
            .select(member.count(),
                    member.age.sum(),
                    member.age.avg(),
                    member.age.max(),
                    member.age.min())
            .from(member)
            .fetch();
            
    Tuple tuple = result.get(0);
    assertThat(tuple.get(member.count())).isEqualTo(4);
    assertThat(tuple.get(member.age.sum())).isEqualTo(100);
    assertThat(tuple.get(member.age.avg())).isEqualTo(25);
    assertThat(tuple.get(member.age.max())).isEqualTo(40);
    assertThat(tuple.get(member.age.min())).isEqualTo(10);
}
  • JPQL이 제공하는 모든 집합 함수를 제공한다.

2. GroupBy 사용

/**
 * 팀의 이름과 각 팀의 평균 연령을 구해라.
 */
@Test
public void group() throws Exception {
    List<Tuple> result = queryFactory
            .select(team.name, member.age.avg())
            .from(member)
            .join(member.team, team)
            .groupBy(team.name)
            .fetch();
            
    Tuple teamA = result.get(0);
    Tuple teamB = result.get(1);
    
    assertThat(teamA.get(team.name)).isEqualTo("teamA");
    assertThat(teamA.get(member.age.avg())).isEqualTo(15);
    
    assertThat(teamB.get(team.name)).isEqualTo("teamB");
    assertThat(teamB.get(member.age.avg())).isEqualTo(35);
}
  • groupBy를 사용하여 그룹별로 원하는 결과를 얻어낼 수 있다.
  • 그룹화된 결과 중 원하는 조건의 결과만 필터링하기 위해서 having을 사용할 수 있다.
  • 예시
.groupBy(item.price)
.having(item.price.gt(1000))

: itemprice를 기준으로 그룹핑을 하되, 가격이 1000보다 큰 값만 그룹핑한다.


[ 조인 - 기본 조인 ]

1. 기본 조인

join(조인 대상, 별칭으로 사용할 Q타입)
  • 첫 번째 파라미터에 조인 대상을 지정한다.
  • 두 번째 파라미터에 별칭(alias)으로 사용할 Q 타입을 지정하면 된다.
/**
 * 팀 A에 소속된 모든 회원
 */
@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");
}
  • join(), innerJoin(): 내부 조인
  • leftJoin(): left 외부 조인
  • rightJoin(): right 외부 조인
  • JPQL의 on과 성능 최적화를 위한 fetch 조인 제공

2. 세타 조인

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

/**
 * 세타 조인(연관관계가 없는 필드로 조인)
 * 회원의 이름이 팀 이름과 같은 회원 조회
 */
@Test
public void theta_join() throws Exception {
    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. 연관관계 없는 엔티티 외부 조인

1. 조인 대상 필터링

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

/**
 * 예) 회원과 팀을 조인하면서, 팀 이름이 teamA인 팀만 조인, 회원은 모두 조회
 * JPQL: SELECT m, t FROM Member m LEFT JOIN m.team t on t.name = 'teamA'
 * SQL: SELECT m.*, t.* FROM Member m LEFT JOIN Team t ON m.TEAM_ID=t.id and 
 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);
    }
}

on절을 활용하여 조인 대상을 필터링 할 때, 내부조인을 사용하면, where 절에서 필터링 하는 것과 기능이 동일하다.
→ 그러므로 내부조인이면 익숙한 where 절로 해결하고, 정말 외부조인이 필요한 경우에만 이 기능을 사용하자!

실행 결과

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]

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

Ex. 회원과 이름과 팀의 이름이 같은 대상 외부 조인

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

실행 결과

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 한번에 조회하는 기능이다.
주로 성능 최적화에 사용하는 방법!

1. 페치 조인 미적용

지연 로딩으로 Member, Team SQL 쿼리 각각 실행

@PersistenceUnit
EntityManagerFactory emf;
@Test
public void fetchJoinNo() throws Exception {

    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은 조회 ❌

2. 페치 조인 적용

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

@Test
public void fetchJoinUse() throws Exception {

    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 사용

1. 서브 쿼리 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);
}

2. 서브 쿼리 goe 사용

goe: 크거나 같은

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

    QMember memberSub = new QMember("memberSub");
    
    List<Member> result = queryFactory
            .selectFrom(member)
            .where(member.age.goe(
                    JPAExpressions
                            .select(memberSub.age.avg())
                            .from(memberSub)
            ))
            .fetch();
            
    assertThat(result).extracting("age")
            .containsExactly(30,40);
}

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

/**
 * 서브쿼리 여러 건 처리, in 사용
 */
@Test
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))
            ))
            .fetch();
            
    assertThat(result).extracting("age")
            .containsExactly(20, 30, 40);
}

4. select 절에 subquery

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

for (Tuple tuple : fetch) {
    System.out.println("username = " + tuple.get(member.username));
    System.out.println("age = " + tuple.get(JPAExpressions.select(memberSub.age.avg()).from(memberSub)));
}

5. static import 활용

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

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

📌 from 절의 서브쿼리 한계

JPA JPQL, Qeurydsl에서 from 절의 서브쿼리(인라인 뷰)는 지원하지 않는다.

💡 from 절의 서브쿼리 해결방안

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

[ Case문 ]

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

1. 단순한 조건

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

age

  • 10이면 열살 출력
  • 20이면 스무살 출력
  • 그 밖에는 기타 출력

2. 복잡한 조건

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();
  • CaseBuilder()를 통해 동작한다.
  • when 절 안에 조건이 들어간다.

[ 상수, 문자 더하기 ]

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

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

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

2. 문자 더하기 concat

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

member.age.stringValue() 부분 중요!

  • 문자가 아닌 다른 타입들은 stringValue()로 문자로 변환할 수 있다.
  • ENUM을 처리할 때도 자주 사용한다.
profile
🚧 https://coji.tistory.com/ 🏠

0개의 댓글