[인강노트 - querydsl] 4-2. 기본 문법 2편

봄도둑·2022년 12월 27일
0

김영한님의 실전! querydsl 강의 내용을 정리한 노트입니다. 블로그에 있는 자료를 사용하실 때에는 꼭 김영한님 강의 링크를 남겨주세요!

7. 집합

  • groupBy, having 등등에 관한 내용
  • 코드로 바로 보자!
@Test
public void aggregation() {
    //아래의 예시처럼 내가 원하는 column으로 선택 시 Tuple이라는 것으로 조회하게 됨
    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);
    Assertions.assertThat(tuple.get(member.count())).isEqualTo(4);
    Assertions.assertThat(tuple.get(member.age.sum())).isEqualTo(100);
    Assertions.assertThat(tuple.get(member.age.avg())).isEqualTo(25);
    Assertions.assertThat(tuple.get(member.age.max())).isEqualTo(40);
    Assertions.assertThat(tuple.get(member.age.min())).isEqualTo(10);
}
  • 실무에서는 Tuple은 잘 안씀 → DTO로 직접 뽑아오는 방법을 주로 선호함
  • 인텔리제이에서 오류 있는 부분으로 바로 이동하는 단축키는 F2
  • groupBy 예제
/**
 * 팀의 이름과 각 팀의 평균 연령을 구해라.
 */
@Test
public void group() {
    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); //(10 + 20) / 2
    assertThat(teamB.get(team.name)).isEqualTo("teamB");
    assertThat(teamB.get(member.age.avg())).isEqualTo(35); //(30 + 40) / 2
}

8. 조인 -기본 조인

  • 조인의 기본 문법은 첫번째 파라미터에 조인 대상을 지정하고, 두번째 파라미터에 별칭으로 사용할 Q타입을 넣어줌
  • 코드로 바로 보자!
/**
 * 팀 A에 소속된 모든 회원 찾기
 */
@Test
public void join() {
    List<Member> result = queryFactory
            .selectFrom(member)
//                .join(member.team, QTeam.team) 와 같음
//                .leftJoin(member.team, team)으로도 쓸 수 있음
            .join(member.team, team)
            .where(team.name.eq("teamA"))
            .fetch();

    assertThat(result)
            .extracting("username") //column 명 username에 
            .containsExactly("member1", "member2"); //member1과 member2가 포함되어 있는가

}
  • 쿼리는 이렇게 나감
  • 연관관계가 없어도 조인할 수 있음 → theta join(세타 조인)
/**
 * 회원의 이름이 팀 이름과 같은 회원 조회
*/
@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();

    //모든 member와 모든 team을 조회 후 한꺼번에 조인 -> where절에서 필터링 진행 -> 이걸 theta join이라고 부름
    //물론 db마다 성능 최적화는 진행해줌

    assertThat(result)
            .extracting("username")
            .containsExactly("teamA", "teamB");
}
  • theta join 시 from절에 여러 엔티티를 선택 → 단, 외부 조인 불가능(left outer, right outer join)
  • cross join을 해버림

9. 조인 - on절

  • on절을 이용한 조인은 JPA 2.1부터 지원
    • 조인 대상 필터링
    • 연관관계 없는 엔티티 외부 조인 → 요러한 이유로 쓰는 게 더 많음
  • 코드로 바로 보자!
/**
* 예) 회원과 팀을 조인하면서, 팀  이름이 teamA인 팀만 조인, 회원은 모두 조회
 * JPQL : select m, t from Member m left join m.team t on 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);
    }
}
  • 결과

  • left join이기 때문에 member data는 다 가져오되 teamA에 소속된 member들은 team data를 가지고 있음 → teamB인 member는 team data가 null

  • on절로 조인 딱 한다음 and로 team.name에 대한 정보를 조건 조회. 즉 and (team1_.name=?) 요 부분이 추가됨

  • on 절에을 활용해 조인 대상을 필터링 할 때 외부조인이 아니라 내부조인(inner join)을 사용하면 where 절에서 필터링 하는 것과 기능이 동일

  • n절을 활용한 조인 대상 필터링을 사용할 때 내부 조인이면 익숙한 where절로 해결, 정말 외부 조인이 필요한 경우에만 이 기능을 사용하자

  • 내부 조인을 쓰면 이렇게 바꿀 수 있음

List<Tuple> result = queryFactory
            .select(member, team)
            .from(member)
            .join(member.team, team)
            .where(team.name.eq("teamA"))
            .fetch();

//같은 결과를 내놓음
  • inner 조인이면 on절로 걸러내나 where로 걸러내나 결과는 똑같음
  • 연관 관계가 없는 외부 엔티티 조인하기
/**
 * 연관 관계가 없는 외부 엔티티 조인
 * 회원의 이름이 팀 이름과 같은 대상 외부 조인
 */
@Test
public void join_on_no_relation() {
    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) //기존에는 leftJoin(member.team, team)으로 썼는데 파라미터로 team 하나만 넘겨줌
            .on(member.username.eq("teamA"))
            .fetch();

    for (Tuple tuple : result) {
        System.out.println("tuple : " + tuple);
    }
}
  • 결과
  • 하이버네이트 5.1부터 on을 사용해 서로 관계가 없는 필드로 외부 조인하는 기능이 추가
  • 문법을 잘 봐둬야 하는 게 leftJoin시 엔티티 하나만 넣어서 사용함!
  • 일반 조인 : leftJoin(member.team, team) , on조인 : from(member).leftJoin(team).on(조건)

10. 조인 - 페치 조인

  • 페치 조인은 sql에서 제공하는 기능은 아니며, sql 조인을 활용해서 연관된 엔티티를 한 번에 조회하는 기능 → 주로 성능 최적화에서 사용
  • 먼저 member를 조회 하나만 해보자
@PersistenceUnit
EntityManagerFactory emf;

@Test
public void fetchJoinNo() {
    //페치 조인 시 올바른 결과를 보려면 영속성 컨텍스트를 비워줘야 함
    em.flush();
    em.clear();

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

    //EntityManagerFactory emf의 isLoaded는 해당 엔티티가 이미 로딩된 엔티티인지, 초기화가 안된 엔티티인지 가르쳐 주는 녀석
    boolean loaded = emf.getPersistenceUnitUtil().isLoaded(findMember.getTeam());
    assertThat(loaded).as("패치 조인 미적용").isFalse();
}
  • 이 때 우리는 Member의 team에 feth = LAZY를 걸어놨기 때문에 team 말고 순수 member에 대한 것들만 조회함
  • 이제 페치 조인을 적용해보자
@Test
public void fetchJoinUse() {
    //페치 조인 시 올바른 결과를 보려면 영속성 컨텍스트를 비워줘야 함
    em.flush();
    em.clear();

    Member findMember = queryFactory
            .selectFrom(member)
            .join(member.team, team).fetchJoin() //join을 해주되 fetchJoin을 싸악 추가하면 됨
            .where(member.username.eq("member1"))
            .fetchOne();
    
    boolean loaded = emf.getPersistenceUnitUtil().isLoaded(findMember.getTeam());
    assertThat(loaded).as("패치 조인 적용").isTrue();
}
  • 쿼리는 요렇게 나감
  • 페치 조인은 정말 많이 씀 → JPA에서 제공하는 기능이기 때문에 잘 모른다면 꼭 공부해둘 것

11. 서브 쿼리

  • com.querydsl.jpa.JPAExpressions 을 사용
  • 코드로 바로 보자!
/**
 * 나이가 가장 많은 회원 조회
*/
@Test
public void subQuery() {
    //sub query 안에 들어가는 큐타입은 본 쿼리에서 사용하는 큐타입과 겹치면 안되기 때문에 QMember를 새로 하나 만들어줌
    //sql에서 alias(별칭)가 곂치면 안되는 거랑 같음
    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);

}
  • 쿼리는 요렇게 나감
  • 나이가 평균 이상인 회원 조회
/**
 * 나이가 평균 이상인 회원 조회
 */
@Test
public void subQueryGoe() {
    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);

}
  • subquery에 in절 사용해보기
@Test
public void subQueryIn() {
    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에 sub query 사용하기 → JPAExpressions는 static import가 가능
@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("tuple : " + tuple);
    }

}
  • JPA를 쓸 때 sub query는 한계점이 있음
  • JPA의 서브 쿼리는 from절에서 사용할 수 없음(인라인 뷰 사용 불가) → select, where절에서는 사용 가능
  • 서브 쿼리를 from 절에서 사용할 수 없기 때문에 querydsl도 지원하지 않음
  • from절에서 서브 쿼리를 사용하려면 → 서브 쿼리를 join으로 변경(그러나 이렇게 해도 불가능한 상황이 있음), 또는 쿼리를 2번 분리해서 실행, 또는 navtiveSQL 사용
  • 그런데, from절에 서브 쿼리를 쓰는 안좋은 이유가 있음
    • from 절에 서브 쿼리를 쓰는 이유 중 안 좋은 이유들이 있음 → DB에서 워낙 많은 기능을 제공하다보니까 화면에 그려주기 위한 로직도 쿼리 안에서 수행하도록 하는 경우가 있음 → 이 경우 from절 안에 서브 쿼리가 잔뜩 들어갈 수 있음
    • SQL은 데이터를 가져오는 걸 집중 하고, 필요에 따라 자바가 로직을 한 번 태우는 것까지는 괜찮음. 화면에 이쁘게 보여주기 위한 가공은 사실 화면 레벨에서 처리해주는 것이 맞음 → 이렇게 해야 DB 쿼리의 재사용성을 높일 수 있음
    • 어떻게든 화면에 맞추기 위해 from절 내에 서브 쿼리를 연속 호출하는 경우가 있음
    • 애플리케이션 로직과 프레젠테이션 로직에서 풀어야 할 것들에 대해 명확하게 구분할 필요가 있음
    • 쿼리에서 어떻게든 풀어고자 하는 의도로 인해 쿼리가 복잡해지는 경우가 있음→DB는 데이터만 날려주고 데이터를 가공해주는 것은 애플리케이션, 프레젠테이션 레벨에서 처리하도록 재설계를 해줄 필요가 있음 → DB는 데이터를 퍼 올리는 역할만 수행 → 이렇게만 설계 해도 from절 내에서 서브 쿼리를 사용하는 상황을 줄일 수 있음
    • 실시간 트래픽이 중요하다면 쿼리 한 방 한 방이 중요하다면 한 방 쿼리가 중요하겠지. 그러나 어드민 같은 조금 느려도 괜찮은 서비스라면 복잡한 한 방 쿼리보다 쿼리를 2~3번 나눠서 호출하는 게 더 나을 수 있음 → 애플리케이션 로직에서 시퀀시 하게 여러 번 나눠서 호출하는 것이 몇 천 줄 짜리 한 방 쿼리보다 더 간결하게 처리할 수 있음
    • 이 내용과 관련해서는 SQL AntiPatterns를 한 번 읽어보자

12. case 문

  • 코드로 바로 보자!
@Test
public void basicCase() {
    List<String> result = queryFactory
            .select(member.age
                    .when(10).then("10살")
                    .when(20).then("20살")
                    .otherwise("기타")
            )
            .from(member)
            .fetch();

    for (String s : result) {
        System.out.println("s : " + s);
    }
}
  • 조금 더 복잡한 조건을 다뤄보자 → CaseBuilder
@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);
    }
}
  • 그런데, 이렇게 써야 해??? → DB는 row 데이터를 필터링하고 그룹핑 해주는 정도로 데이터를 줄이는 일을 하고, 데이터를 가공해주는 것은 db에서 해주면 안됨 → 애플리케이션, 프레젠테이션 레벨에서 해결하도록 하자

13. 상수, 문자 더하기

  • 코드로 바로 보자! → 상수 A가 고정적으로 출력됨
@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);
    }
}
  • 문자를 concat(합치기)
@Test
public void concat() {
    //username_age 구조를 만들어보자
    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 : " + result);
    }
}
  • 결과는 요렇게 나옴
  • stringValue는 생각보다 사용할 곳이 많음 → enum 처리 등등…
    • 문자가 아닌 다른 타입들을 문자로 변환해주는 녀석
profile
Java Spring 백엔드 개발자입니다. java 외에도 다양하고 흥미로운 언어와 프레임워크를 학습하는 것을 좋아합니다.

0개의 댓글