Querydsl - 기본 문법

Chooooo·2024년 6월 25일
0

Querydsl

목록 보기
4/8

JPQL 대신 Querydsl을 이용하는 이유 중 하나가 type-safe(컴파일 시점에 알 수 있는) 쿼리를 날리기 위해서 사용한다.

이 말은 JPQL에서 쿼리에 오타가 발생해도 컴파일 시점에 알기 힘들다. 오로지 런타임에서만 체크가 가능하다. 하지만 Querydsl은 컴파일 시점에 오류를 잡아줄 수 있기 때문에 좋다.

Querydsl 환경설정 검증.

엔티티 생성 후

Gradle IntelliJ 사용법

  • Gradle → Tasks → build → clean
  • Gradle → Tasks → other → compileQuerydsl

Gradle 콘솔 사용법

  • ./gradlew clean compileQuerydsl

Q타입 생성 확인

  • build → generated → querydsl
💡 참고 : Q타입은 컴파일 시점에 자동 생성되므로 버전관리(Git)에 포함하지 않는 것이 좋다. 앞서 설정에서 생성 위치를 gradle build 폴더 아래 생성되도록 했기 때문에 이 부분도 자연스럽게 해결 된다.

기본 문법

기본 Q-Type 활용

Q클래스 인스턴스를 사용하는 2가지 방법

QMember qMember = new QMember("m"); //별칭 직접 지정
QMember qMember = QMember.member; //기본 인스턴스 사용

기본 인스턴스를 static import와 함께 사용하는 방법

import static study.querydsl.domain.QMember.*;

@Test
public void startQuerydsl3() {
     Member findMember = queryFactory
     .select(member)
     .from(member)
     .where(member.username.eq("member1"))
     .fetchOne();
     
    assertThat(findMember.getUsername()).isEqualTo("member1");
}
  • 같은 테이블을 조인해야 하는 경우가 아니면 기본 인스턴스를 사용하자

application.yml에 설정 추가 → 실행되는 JPQL 볼 수 있다

spring.jpa.properties.hibernate.use_sql_comments: true

검색 조건 쿼리

기본 검색 쿼리 - and(), or()

@Test
public void search() {
     Member findMember = queryFactory
     .selectFrom(member)
     .where(member.username.eq("member1")
     .and(member.age.eq(10)))
     .fetchOne();
     
    assertThat(findMember.getUsername()).isEqualTo("member1");
}
  • 검색 조건은 and(), or() 메서드를 체인으로 이용해 연결할 수 있다.
  • select(), from()을 합친 selectFrom()으로 함께 사용하는게 가능하다.

JPQL이 제공하는 모든 검색 조건 제공한다.

member.username.eq("member1") // username = 'member1'
member.username.ne("member1") //username != 'member1'
member.username.eq("member1").not() // username != 'member1'

member.username.isNotNull() //이름이 is not null

member.age.in(10, 20) // age in (10,20)
member.age.notIn(10,20) // age not in (10, 20)
member.age.between(10,30) // between 10, 230

member.age.goe(30) // age >= 30
member.age.gt(30) // age > 30
member.age.loe(30) // age <= 30
member.age.lt(30) // age < 30

member.username.like("member%") // like 검색
member.username.contains("member") // like %member% 검색
member.username.startswith("member") // like member% 검색

AND 조건을 파라미터로 처리할 수 있다.

@Test
public void searchAndParam() {
     List<Member> result1 = queryFactory
     .selectFrom(member)
     .where(member.username.eq("member1"), member.age.eq(10))
     .fetch();
     
     assertThat(result1.size()).isEqualTo(1);
}
  • where() 절에 파라미터로 추가한다면, and 조건이 추가되는 것이다.

결과 조회

  • fetch() 메서드로 리스트를 조회할 수 있다. 데이터가 없으면 빈 리스트가 조회된다.
  • fetchOne() 단 한건의 결과를 조회한다.
    • 데이터가 없으면 Null이 조회된다.
    • 결과가 둘 이상이라면 예외 발생.
  • fetchFirst()로 첫번째로 발견되는 결과를 조회할 수 있다.
    • limit(1).fetchOne()과 동일하다.
  • fetchResults()로 페이징을 포함한 결과를 조회할 수 있다.
  • fetchCount()로 count 쿼리로 변경해서 count 수 조회가 가능하다.
//List
List<Member> fetch = queryFactory
        .selectFrom(member)
        .fetch();

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

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

//페이징에서 사용
QueryResults<Member> results = queryFactory
        .selectFrom(member)
        .fetchResults();

//count 쿼리로 변경
long count = queryFactory
        .selectFrom(member)
        .fetchCount();

정렬

바로 예제부터 보면 정렬 순서는 다음과 같다.

  1. 회원 나이 내림차순 (desc)
  2. 회원 이름 오름차순
  • 단 2에서 회원 이름이 빈 값이라면 마지막으로 결과가 출력하도록 하겠다.
@Test
void sort(){
    //given
    em.persist(new Member(null, 100));
    em.persist(new Member("member5", 100));
    em.persist(new Member("member6", 100));
    //when
    List<Member> result = queryFactory
            .selectFrom(member)
            .where(member.age.eq(100))
            .orderBy(member.age.desc(), member.username.asc().nullsLast())
            .fetch();

    Member member5 = result.get(0);
    Member member6 = result.get(1);
    Member memberNull = result.get(2);
    //then
    assertEquals("member5", member5.getUsername());
    assertEquals("member6", member6.getUsername());
    assertNull(memberNull.getUsername());
}
  • orderBy()를 통해서 정렬 시작할 수 있음
  • desc()를 이용해 정렬 기준 값에 내림차순으로 asc()를 기준으로 오름차순으로 정렬할 수 있다.
  • nullLast()나 nullFirst()로 null 데이터에 순서를 부여할 수 있다.

페이징

페이징 조회 개수 제한

@Test
void paging1(){
    //given

    //when
    List<Member> result = queryFactory
            .selectFrom(member)
            .orderBy(member.username.asc())
            .offset(0) // 0부터 시작
            .limit(3)
            .fetch();
    Member member1 = result.get(0);
    //then
    assertEquals("member1", member1.getUsername());
    assertEquals(3, result.size());
}

전체 조회 수가 필요한 경우

@Test
void paging2(){
    //given

    //when
    QueryResults<Member> queryResults = queryFactory
            .selectFrom(member)
            .orderBy(member.username.desc())
            .offset(0)
            .limit(2)
            .fetchResults();

    //then
    assertEquals(4, queryResults.getTotal());
    assertEquals(2, queryResults.getLimit());
    assertEquals(0, queryResults.getOffset());
    assertEquals(2, queryResults.getResults().size());
}
  • fetchResults()를 이용하면 count 쿼리가 한번 나가고 select 쿼리가 이따라서 나가게된다.
    • 실무에서는 페이징 쿼리를 작성할 때 데이터를 조회하는 쿼리는 여러 테이블을 조인해서 하지만, count 자체는 그럴 필요가 없는 경우도 많다. 그런데 fetchResults()를 하면 자동화된 count 쿼리가 나가니까 성능 상 안 좋음. → 이 경우 따로 count 쿼리 별도 작성

집합

  • select.member.count() : 멤버 숫자 구할 수 있음
  • select.member.age.sum() : 멤버 나이 합 구할 수 있음
  • select.member.age.avg() : 멤버 나이 평균 값 구할 수 있음
  • select.member.age.max() : 멤버 나이 맥스 구할 수 있음
  • select.member.age.min() : 멤버 나이 최소 구할 수 있음

일반적인 집합 함수 사용 예

@Test
void aggregation(){
    //given

    //when
    List<Tuple> result = queryFactory
            .select(
                    member.count(),
                    member.age.sum(),
                    member.age.avg(),
                    member.age.max(),
                    member.age.min()
            )
            .from(member)
            .fetch();
    //then
    Tuple tuple = result.get(0);
    assertEquals(4, tuple.get(member.count()));
    assertEquals(100, tuple.get(member.age.sum()));
    assertEquals(25, tuple.get(member.age.avg()));
    assertEquals(40, tuple.get(member.age.max()));
    assertEquals(10, tuple.get(member.age.min()));
}

select에서 내가 원하는 데이터 타입이 여러개라면 Tuple로 결과 조회. Tuple은 querydsl에서 제공하는 자료구조.

  • 실무에서는 Tuple로 뽑기보다는 DTO로 가져온다.

groupBy() 예제

@Test
void group(){
    //given
    
    //when
    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);

    //then
    assertEquals("TeamA" ,teamA.get(team.name));
    assertEquals(15, teamA.get(member.age.avg()));
    assertEquals("TeamB" ,teamB.get(team.name));
    assertEquals(35, teamA.get(member.age.avg()));
}

having() 예제

@Test
void having(){
    //given

    //when
    List<Tuple> result = queryFactory
            .select(team.name, member.age.avg())
            .from(member)
            .join(member.team, team)
            .groupBy(team.name)
            .having(member.age.avg().gt(20))
            .fetch();

    Tuple teamB = result.get(0);
    //then
    assertEquals("TeamB", teamB.get(team.name));
    assertEquals(35, teamB.get(member.age.avg()));
}

조인 - 기본 조인

Querydsl에서 조인을 사용할 때, .join() 메서드의 첫번째 파라미터는 조인할 대상 엔티티를 지정하고, 두번째 파라미터는 별칭(alias)를 지정한다. 이 별칭은 조인된 엔티티를 참조하기 위해 사용된다.

기본조인

@Test
void join(){
    //given

    //when
    List<Member> result = queryFactory
            .selectFrom(member)
            .join(member.team, team)
            .where(team.name.eq("TeamA"))
            .fetch();
    //then
    assertThat(result)
            .extracting("username")
            .containsExactly("member1", "member2");
}
  • join은 innerJoin 말고, leftJon이나 rightJoin도 할 수 있다.
  • join이후에 on을 넣어서 대상 지정을 넣을 수도 있다.
  • 그리고 연관관계가 없어도 조인을 할 수 있는 세타 조인도 할 수 있다.

세타조인

@Test
void thetaJoin(){
    //given
    em.persist(new Member("TeamA"));
    em.persist(new Member("TeamB"));
    //when
    List<Member> result = queryFactory
            .select(member)
            .from(member, team)
            .where(member.username.eq(team.name))
            .fetch();
    //then
    Member memberA = result.get(0);
    Member memberB = result.get(1);

    assertEquals("TeamA", memberA.getUsername());
    assertEquals("TeamB", memberB.getUsername());
}
  • 세타 조인은 연관관계가 없어도 데이터를 다 가지고 온 다음 조인을 하는 방식.
  • from에 여러 엔티티를 가지고 와서 세타 조인을 한다.

조인 - On 절

ON 절을 활용한 조인은 조인 대상을 필터링 해주고 연관관계가 없는 엔티티는 외부 조인을 활용할 수 있다. : 조인 대상의 조건을 지정하여 필터링할 때 사용.

  • ON절과 Where절의 차이는 ON절 같은 경우 JOIN할 데이터를 필터링하기 위해서 사용하는 반면, WHERE 절은 JOIN을 하고 나서 데이터를 필터링하기 위해서 사용한다고 생각하면 된다. 즉 ON절이 WHERE절보다 먼저 실행이 되고, 이는 LEFT_OUTER_JOIN을 하면 뚜렷히 드러난다.

ON 절 예제

@Test
@DisplayName("예) 회원과 팀을 조인하면서, 팀 이름이 TeamA인 팀만 조인 하고 회원은 모두 조회한다.")
void joinOnFiltering(){
    //given

    //when
    List<Tuple> result = queryFactory
            .select(member, team)
            .from(member)
            .leftJoin(member.team, team)
            .on(team.name.eq("TeamA"))
            .fetch();
    //then
    for (Tuple tuple : result) {
        System.out.println(tuple);
    }
}
  • on절 : 회원 이름과 팀 이름이 같은 경우만 조인
  • leftJoin : 회원(member)의 모든 데이터를 조회하고, 조건에 맞는 팀(team)데이터를 조인한다.
  • on절을 활용해서 조인 대상을 필터링 할 때 INNER JOIN을 이용한다면 WHERE절과 결과적으로 동일하다.
  • OUTER JOIN에서만 ON 절이 달라진다.

연관관계가 없는 엔티티를 외부 조인할 경우에 ON절이 쓰인다.

@Test
@DisplayName("연관관계가 없는 엔터티 외부 조인으로 회원의 이름과 팀 이름이 같은 회원을 조인한다.")
void joinOnNoRelation(){
    //given
    em.persist(new Member("TeamA"));
    em.persist(new Member("TeamB"));
    em.persist(new Member("TeamC"));
    //when
    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);
    }
}
  • 원래 join을 할 때는 leftJoin(member.team, team)을 통해서 member가 가진 FK를 통해서 조인을 한다. 하지만 여기서는 연관관계가 없으므로 이렇게 하지 않고, leftJoin(team).on()절을 통해서 team으로 조인을 하는데, 이 조건이 ON절에 담기게 해서 조인을 한다.

조인 - 패치조인

패치조인은 SQL에서 제공하는 기능이 아니다. JPA에서 주로 성능 최적화를 위해서 사용하는 기능. 패치 조인을 사용하면 연관된 엔티티를 한번의 쿼리로 함께 조회할 수 있어 성능을 향상시킬 수 있음

패치 조인 미적용

@PersistenceUnit
EntityManagerFactory emf;

@Test
@DisplayName("패치조인을 사용하지 않고 데이터를 가지고 오는 방법이다.")
void fetchJoinNo(){
    //given
    em.flush();
    em.clear();
    //when
    Member member = queryFactory
            .selectFrom(QMember.member)
            .where(QMember.member.username.eq("member1"))
            .fetchOne();
    
    //then
    boolean isLoaded = emf.getPersistenceUnitUtil().isLoaded(member.getTeam());
    assertEquals(false, isLoaded);
}
Hibernate: 
    select
        member0_.member_id as member_i1_0_,
        member0_.age as age2_0_,
        member0_.team_id as team_id4_0_,
        member0_.username as username3_0_ 
    from
        member member0_ 
    where
        member0_.username=?
Member(id=3, username=member1, age=10)
  • Member를 조회하면 Lazy 로딩 적용을 했으므로 쿼리를 보면 팀을 조회하지 않고 멤버만 조회한다.(패치 조인을 사용하지 않으면 Lazy 로딩이 적용된다. 이는 연관된 엔티티가 필요할 때마다 추가 쿼리가 실행되어 성능 문제가 발생할 수 있다.)
  • 연관된 팀 엔티티는 로드되지 않음 (Lazy 로딩) → member 엔티티를 조회할 때 team엔티티는 Lazy 로딩으로 설정되어 있어, 실제로 team엔티티를 사용할 때 추가 쿼리가 발생한다.

패치 조인 적용 예제

패치 조인을 사용하면, 연관된 엔티티를 한번의 쿼리로 함께 조회할 수 있다. → 결국 Lazy 로딩 대신 즉시 로딩(Eager Loading) 처럼 연관된 엔티티를 한 번의 쿼리로 함께 조회한다는 뜻.

@Test
@DisplayName("패치 조인을 사용해서 데이터를 가지고 오는 방법이다.")
void fetchJoinYes() {
    // given
    em.flush();
    em.clear();

    // when
    Member findMember = queryFactory
            .selectFrom(QMember.member)
            .join(member.team, team).fetchJoin() // 패치 조인 적용
            .where(QMember.member.username.eq("member1"))
            .fetchOne();

    // then
    boolean isLoaded = emf.getPersistenceUnitUtil().isLoaded(findMember.getTeam());
    assertEquals(true, isLoaded); // 연관된 팀 엔티티도 함께 로드됨
}

패치조인 동작 방식

패치 조인을 사용하면, 연관된 엔티티를 즉시 로딩하는 쿼리가 생성된다. 이는 Lazy 로딩을 우회하여 한 번의 쿼리로 필요한 데이터를 모두 가져온다.

SELECT member0_.member_id AS member_i1_0_0_, 
       team1_.team_id AS team_id1_1_1_, 
       member0_.age AS age2_0_0_, 
       member0_.team_id AS team_id4_0_0_, 
       member0_.username AS username3_0_0_, 
       team1_.name AS name2_1_1_ 
FROM   member member0_ 
       INNER JOIN team team1_ 
               ON member0_.team_id = team1_.team_id 
WHERE  member0_.username = ?

이 예제에서는 member 엔티티와 team 엔티티를 패치 조인을 통해 함께 조회한다. 이를 통해 team 엔티티도 즉시 로딩되어 추가 쿼리가 발생하지 않는다.

패치 조인의 이점

  1. N+1 문제 해결 : 연관된 엔티티를 한 번의 쿼리로 함께 조회하여 N+1 문제 해결
  2. 성능 향상 : Lazy 로딩 대신 즉시 로딩을 사용하여 필요한 데이터를 미리 가져올 수 있어 성능을 향상시킨다.
  3. 코드 간소화 : 필요한 데이터를 한 번에 가져오므로, 추가적인 쿼리 작성이 필요 없다.

패치 조인의 제약

  • 여러 컬렉션 패치 조인 금지 : 한 번의 쿼리에서 여러 개의 컬렉션을 패치조인하는 것은 허용되지 않는다.
  • 페이징 제약 : 패치 조인과 페이징을 함께 사용할 때, 메모리 내에서 페이징 처리. 성능 저하 초래할 수도.

패치 조인 사용 시기

성능 최적화를 위해 필요할 때만 연관된 엔티티를 로드하고자 할 때 사용한다. 주로 N+1 문제를 해결하기 위해 사용된다.

→ JPA에서 성능 최적화를 위해 제공되는 강력한 기능인 패치 조인은 Lazy 로딩을 우회하고 연관된 엔티티를 한 번의 쿼리로 함께 조회하여 성능을 향상시킬 수 있다. 하지만 여러 컬렉션 패치 조인 금지, 페이징 제약 등의 제약이 있다.

서브 쿼리

서브 쿼리란 select 문 안에 다시 select 문이 기술된 형태의 쿼리로 안에 있는 결과를 밖에서 받아쳐 처리하는 구조.

단일 select 문으로 조건식을 만들기가 복잡한 경우에 또는 완전히 다른 테이블에서 값을 조회해서 메인 쿼리로 이용하고자 할 때 사용한다.

서브 쿼리는 from절은 안되고, select 절이나 where절에서만 가능

JPAExpressions를 static import해서 줄이자.

SubQuery 예제 - 나이가 가장 많은 사람

@Test
@DisplayName("나이가 가장 많은 회원을 조회한다고 해보자.")
void subQuery(){
    //given
    QMember qMember = new QMember("m"); // alias 가 중복되면 안되므로 QMember 를 만들어줘야 한다.

    //when
    Member findMember = queryFactory
            .selectFrom(member)
            .where(member.age.eq(
                    JPAExpressions
                            .select(qMember.age.max())
                            .from(qMember)

            ))
            .fetchOne();
    //then
    assertEquals(40, findMember.getAge());
}

SubQuery 예제 - 나이가 평균보다 같거나 큰 사람들 조회

@Test
@DisplayName("나이가 평균 이상인 회원을 조회한다고 해보자.")
void subQueryAvg(){
    //given
    QMember qMember = new QMember("m"); // alias 가 중복되면 안되므로 QMember 를 만들어줘야 한다.

    //when
    List<Member> findMembers = queryFactory
            .selectFrom(member)
            .where(member.age.goe(
                    JPAExpressions
                            .select(qMember.age.avg())
                            .from(qMember)

            ))
            .fetch();
    //then
    assertEquals(2, findMembers.size());
}

SubQuery 예제 - IN 사용

@Test
@DisplayName("나이가 10인 회원을 조회한다고 해보자. - In 을 이용")
void subQueryIn(){
    //given
    QMember qMember = new QMember("m"); // alias 가 중복되면 안되므로 QMember 를 만들어줘야 한다.

    //when
    List<Member> findMembers = queryFactory
            .selectFrom(member)
            .where(member.age.in(
                    JPAExpressions
                            .select(qMember.age)
                            .from(qMember)
                            .where(qMember.age.in(10))
            ))
            .fetch();
    //then
    assertEquals(1, findMembers.size());
    assertEquals(10, findMembers.get(0).getAge());
}

서브쿼리는 복잡한 조회나, 조건 적용을 위해 유용하니 잘 생각해서 사용하자.

Case 문

조건에 따라서 값을 지정해주는 Case 문은 select, where, orderBy에서 사용 가능하다.

일반적인 Case 문

@Test
void baseCase(){
    //given

    //when
    List<String> result = queryFactory
            .select(member.age
                    .when(10).then("열살")
                    .when(20).then("스무살")
                    .otherwise("기타"))
            .from(member)
            .fetch();
    //then
    for (String s : result) {
        System.out.println(s);
    }
}

조금 복잡한 Case문

@Test
void complexCase(){
    //given

    //when
    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();

    //then
    for (String s : result){
        System.out.println(s);
    }
}
  • CaseBuilder()를 이용하면 간단하다.

복잡한 Case 문

  • 0 ~ 30살이 아닌 회원을 가장 먼저 출력
  • 0 ~ 20살 회원 출력
  • 21 ~ 30살 회원 출력
@Test
@DisplayName("0 ~ 30 살이 아닌 사람을 가장 먼저 출력하고 그 다음 0 ~ 20살, 그 다음 21살 ~ 30살 출력한다.")
void complexCase2(){
    //given
    NumberExpression<Integer> rankCase = new CaseBuilder()
            .when(member.age.between(0, 20)).then(2)
            .when(member.age.between(21, 30)).then(1)
            .otherwise(3);
    //when
    List<Tuple> result = queryFactory
            .select(member.username, member.age, rankCase)
            .from(member)
            .orderBy(rankCase.desc())
            .fetch();
    //then
    for(Tuple tuple : result) {
        System.out.println(tuple);
    }
}

상수 더하기

상수가 필요하다면 Expressions.constant()를 사용하면 된다. 줄여서 쓰고 싶다면 static import를 사용

@Test
void addConstant(){
    //given
    //when
    List<Tuple> result = queryFactory
            .select(member.username, constant("A"))
            .from(member)
            .fetch();
    
    //then
    for (Tuple tuple : result) {
        System.out.println(tuple);
    }
}

문자 더하기

문자를 더할거면 concat을 이용하면 편하다.

@Test
void addConcat(){
    //given

    //when
    String result = queryFactory
            .select(member.username.concat("_").concat(member.age.stringValue()))
            .from(member)
            .where(member.username.eq("member1"))
            .fetchOne();
    //then
    System.out.println(result);
}

Querydsl 동작하는 과정은 JPQL을 거쳐서 SQL로 변환되어 실행된다.

profile
back-end, 지속 성장 가능한 개발자를 향하여

0개의 댓글