기본 문법 2

LeeKyoungChang·2022년 5월 5일
0
post-thumbnail

Querydsl 수업을 듣고 정리한 내용입니다.

 

📚 7. 집합

✔️ 집합 함수

/**  
 * JPQL * select * COUNT(m), //회원수  
 * SUM(m.age), //나이 합  
 * AVG(m.age), //평균 나이  
 * MAX(m.age), //최대 나이  
 * MIN(m.age) //최소 나이 * from Member m  
 */@Test  
public void aggregation() {  
    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이 제공하는 모든 집합 함수를 제공한다.
  • tuple은 프로젝션과 결과반환에서 공부한다.

 

실행 결과

 

✔️ 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을 사용할 수 있다.

ex) groupBy(), having() 예시

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

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

 

실행 결과

 

📚 8. 조인 - 기본 조인

📖 A. 기본 조인

join(조인 대상, 별칭으로 사용할 Q타입)
  • 첫 번째 파라미터에 조인 대상을 지정한다.
  • 두 번째 파라미터에 별칭(alias)으로 사용할 Q 타입을 지정하면 된다.

 

/**  
 * 팀 A에 소속된 모든 회원  
 */  
@Test  
public void join() {  
	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 조인 제공

 

실행 결과

 

📖 B. 세타 조인

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

/**  
 * 세타 조인(연관관계가 없는 필드로 조인)  
 * 회원의 이름이 팀 이름과 같은 회원 조회  
 */  
@Test  
public void theta_join() {  
    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 절에 여러 엔티티를 선택해서 세타 조인
  • 지금은 외부 조인 불가능 → 조인 on을 사용하면 외부 조인 가능

 

실행 결과

 

📚 9. 조인 - on절

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

 

📖 A. 조인 대상 필터링

예) 회원과 팀을 조인하면서, 팀 이름이 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() {  
    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 절을 활용해 조인 대상을 필터링 할 때, 외부조인이 아니라 내부조인(inner join)을 사용하면, where 절에서 필터링 하는 것과 기능이 동일하다. 따라서 on 절을 활용한 조인 대상 필터링을 사용할 때, 내부조인 이면 익숙한 where 절로 해결하고, 정말 외부조인이 필요한 경우에만 이 기능을 사용하자.

 

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

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

/**  
 * 2. 연관관계 없는 엔티티 외부 조인  
 * 예) 회원의 이름과 팀의 이름이 같은 대상 외부 조인  
 */  
@Test  
public void join_on_no_filtering() {  
    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)

 

실행 결과

 

📚 10. 조인 - 페치 조인

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

 

📖 A. 페치 조인 미적용

지연로딩으로 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은 조회하지 않는다.

 

실행 결과

 

📖 B. 페치 조인 적용

즉시로딩으로 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()이라고 추가하면 된다.

 

실행 결과

 

📚 11. 서브 쿼리

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

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);
}

 

✔️ 서브쿼리 여러 건 처리 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);
}

 

✔️ select 절에 subquery

@Test  
public void selectSubQuery() {  
    QMember memberSub = new QMember("memberSub");  
      
    List<Tuple> result = queryFactory  
            .select(member.username,  
                    JPAExpressions  
                            .select(memberSub.age.avg())  
                            .from(memberSub)  
            ).from(member)  
            .fetch();  
  
    for (Tuple tuple : result) {  
        System.out.println("username = " + tuple.get(member.username));  
        System.out.println("age = " + tuple.get(JPAExpressions.select(memberSub.age.avg()).from(memberSub)));  
    }  
}

 

✔️ static import 활용


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

@Test  
public void selectSubQuery() {  
    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("username = " + tuple.get(member.username));  
        System.out.println("age = " + tuple.get(JPAExpressions.select(memberSub.age.avg()).from(memberSub)));  
    }  
  
  
}

 

🌋 from절의 서브쿼리 한계

  • JPA JPQL 서브쿼리의 한계점으로 from 절의 서브쿼리(인라인 뷰)는 지원하지 않는다.
  • Querydsl도 지원하지 않는다.
  • 하이버네이트 구현체를 사용하면 select 절의 서브쿼리는 지원한다.
  • Querydsl도 하이버네이트 구현체를 사용하면 select 절의 서브쿼리를 지원한다.

 

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

 

📚 12. Case 문

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

✔️ 단순한 조건

@Test  
public void basicCase() {  
    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);  
    }  
}

age

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

 

실행 결과

 

✔️ 복잡한 조건

@Test  
public void complexCase() {  
    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()를 통해 동작한다.
  • when 절 안에 조건이 들어간다.

 

실행 결과

 

✔️ orderBy에서 Case 문 함께 사용하기 예제

예를 들어서 다음과 같은 임의의 순서로 회원을 출력하고 싶다면?
1. 0 ~ 30살이 아닌 회원을 가장 먼저 출력
2. 0 ~ 20살 회원 출력
3. 21 ~ 30살 회원 출력

@Test  
public void exampleCase(){  
    NumberExpression<Integer> rankPath = new CaseBuilder()  
            .when(member.age.between(0, 20)).then(2)  
            .when(member.age.between(21, 30)).then(1)  
            .otherwise(3);  
  
    List<Tuple> result = queryFactory  
            .select(member.username, member.age, rankPath)  
            .from(member)  
            .orderBy(rankPath.desc())  
            .fetch();  
  
    for (Tuple tuple : result) {  
        String username = tuple.get(member.username);  
        Integer age = tuple.get(member.age);  
        Integer rank = tuple.get(rankPath);  
        System.out.println("username = " + username + " age = " + age + " rank = " + rank);  
    }  
}
  • Querydsl은 자바 코드로 작성하기 때문에 rankPath 처럼 복잡한 조건을 변수로 선언해서 select 절, orderBy 절에서 함께 사용할 수 있다.

 

실행 결과

 

📚 13. 상수, 문자 더하기

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

@Test  
public void constant() {  
    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() {  
    List<String> result = queryFactory  
            .select(member.username.concat("_").concat(member.age.stringValue()))  
            .from(member)  
            .where(member.username.eq("member1"))  
            .fetch();  
  
    for (String s : result) {  
        System.out.println("s = " + s);  
    }  
}
  • member.age.stringValue() 부분이 중요하다.
  • 문자가 아닌 다른 타입들은 stringValue()로 문자로 변환할 수 있다.
  • 이 방법은 ENUM을 처리할 때도 매우 자주 사용한다.

 

실행 결과

 

profile
"야, (오류 만났어?) 너두 (해결) 할 수 있어"

0개의 댓글