티스토리 뷰

Querydsl

[Querydsl] 기본 문법 - 정렬, 페이징, 함수, 집합

배워서 남 주는 Jaime.Lee 2021. 7. 18. 04:26
728x90
반응형

모든 소스 코드는 여기 있습니다.

이전 포스팅에 이어서 Querydsl의 기본 문법을 소개합니다.

정렬

JPAQueryFactory에서 orderBy 메서드를 호출해 정렬 기능을 사용합니다.

orderBy의 파라미터로 정렬할 항목들을 전달하는데 아래 테스트 코드처럼 작성하면 됩니다.

package io.lcalmsky.querydsl.domain;

import com.querydsl.jpa.impl.JPAQueryFactory;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import javax.persistence.EntityManager;
import org.springframework.transaction.annotation.Transactional;;
import java.util.List;

import static io.lcalmsky.querydsl.domain.QPlayer.player;
import static org.junit.jupiter.api.Assertions.assertEquals;

@SpringBootTest
@Transactional
class PlayerTest {
    @Autowired
    EntityManager entityManager;

    @BeforeEach
    void setup() {
        Team tottenhamHotspur = new Team("Tottenham Hotspur F.C.");
        Team manchesterCity = new Team("Manchester City F.C.");
        entityManager.persist(tottenhamHotspur);
        entityManager.persist(manchesterCity);

        Player harryKane = new Player("Harry Kane", 27, tottenhamHotspur);
        Player heungminSon = new Player("Heungmin Son", 29, tottenhamHotspur);
        Player kevinDeBruyne = new Player("Kevin De Bruyne", 30, manchesterCity);
        Player raheemSterling = new Player("Raheem Shaquille Sterling", 26, manchesterCity);

        entityManager.persist(harryKane);
        entityManager.persist(heungminSon);
        entityManager.persist(kevinDeBruyne);
        entityManager.persist(raheemSterling);
    }

    @Test
    void simpleQuerydslWithSortTest() {
        // given
        JPAQueryFactory queryFactory = new JPAQueryFactory(entityManager);
        List<Player> players = queryFactory.selectFrom(player)
                .orderBy(player.age.desc(), player.name.asc().nullsLast())
                .fetch();
        // then
        assertEquals("Kevin De Bruyne", players.get(0).getName());
        assertEquals("Raheem Shaquille Sterling", players.get(3).getName());
    }
}

나이는 내림차순으로, 이름은 오름차순으로 정렬하였고 이름이 없을 경우 마지막에 나타나게 하였습니다.

테스트를 실행해보면,

2021-07-18 03:30:43.998 DEBUG 2231 --- [           main] org.hibernate.SQL                        : 
    /* select
        player 
    from
        Player player 
    order by
        player.age desc,
        player.name asc nulls last */ select
            player0_.player_id as player_i1_1_,
            player0_.age as age2_1_,
            player0_.name as name3_1_,
            player0_.team_id as team_id4_1_ 
        from
            player player0_ 
        order by
            player0_.age desc,
            player0_.name asc nulls last

원하는 쿼리가 실행된 것을 확인할 수 있습니다.

nullsLast()(null인 항목을 마지막으로)와 nullsFirst()(null인 항목을 처음으로) 두 가지 기능만 따로 알아놓으시면 나머지는 쿼리 작성할 때 이미 많이 사용해보셨기 때문에 쉽게 사용 가능합니다.

페이징

페이징 처리를 위해선 offset()limit()를 사용합니다. 스프링 데이터 JPA의 Pageable 인터페이스를 전달하는 방식보다는 다소 투박(?) 하다고 볼 순 있지만 동적쿼리를 작성하면서 페이징이 필요한 경우 유용하게 사용할 수 있습니다.

package io.lcalmsky.querydsl.domain;

import com.querydsl.jpa.impl.JPAQueryFactory;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import javax.persistence.EntityManager;
import org.springframework.transaction.annotation.Transactional;;
import java.util.List;

import static io.lcalmsky.querydsl.domain.QPlayer.player;
import static org.junit.jupiter.api.Assertions.assertEquals;

@SpringBootTest
@Transactional
class PlayerTest {
    @Autowired
    EntityManager entityManager;

    @BeforeEach
    void setup() {
        Team tottenhamHotspur = new Team("Tottenham Hotspur F.C.");
        Team manchesterCity = new Team("Manchester City F.C.");
        entityManager.persist(tottenhamHotspur);
        entityManager.persist(manchesterCity);

        Player harryKane = new Player("Harry Kane", 27, tottenhamHotspur);
        Player heungminSon = new Player("Heungmin Son", 29, tottenhamHotspur);
        Player kevinDeBruyne = new Player("Kevin De Bruyne", 30, manchesterCity);
        Player raheemSterling = new Player("Raheem Shaquille Sterling", 26, manchesterCity);

        entityManager.persist(harryKane);
        entityManager.persist(heungminSon);
        entityManager.persist(kevinDeBruyne);
        entityManager.persist(raheemSterling);
    }

    @Test
    void simpleQuerydslWithPaging() {
        // given
        JPAQueryFactory queryFactory = new JPAQueryFactory(entityManager);
        List<Player> players = queryFactory.selectFrom(player)
                .orderBy(player.name.asc())
                .offset(1)
                .limit(2)
                .fetch();
        // then
        assertEquals(2, players.size());
    }
}

테스트를 실행해보면 offsetlimit가 제대로 사용된 것을 확인할 수 있습니다.

2021-07-18 03:40:47.862 DEBUG 2404 --- [           main] org.hibernate.SQL                        : 
    /* select
        player 
    from
        Player player 
    order by
        player.name asc */ select
            player0_.player_id as player_i1_1_,
            player0_.age as age2_1_,
            player0_.name as name3_1_,
            player0_.team_id as team_id4_1_ 
        from
            player player0_ 
        order by
            player0_.name asc limit ? offset ?

이전 포스팅에서 페이징 결과 매핑에 사용할 수 있는 fetchResults()를 이용하기위해 마지막을 수정해서 다시 테스트해보겠습니다.

@Test
void simpleQuerydslWithPaging2() {
    // given
    JPAQueryFactory queryFactory = new JPAQueryFactory(entityManager);
    QueryResults<Player> players = queryFactory.selectFrom(player)
            .orderBy(player.name.asc())
            .offset(1)
            .limit(2)
            .fetchResults();
    // then
    assertEquals(4, players.getTotal());
    assertEquals(2, players.getResults().size());
}
2021-07-18 03:46:53.357 DEBUG 2446 --- [           main] org.hibernate.SQL                        : 
    /* select
        count(player) 
    from
        Player player */ select
            count(player0_.player_id) as col_0_0_ 
        from
            player player0_
2021-07-18 03:46:53.371 DEBUG 2446 --- [           main] org.hibernate.SQL                        : 
    /* select
        player 
    from
        Player player 
    order by
        player.name asc */ select
            player0_.player_id as player_i1_1_,
            player0_.age as age2_1_,
            player0_.name as name3_1_,
            player0_.team_id as team_id4_1_ 
        from
            player player0_ 
        order by
            player0_.name asc limit ? offset ?

totalCount를 위한 쿼리 1회, limitoffset을 이용한 쿼리 1회, 총 2회 쿼리가 발생하는 것을 확인할 수 있습니다.

페이징을 위한 쿼리가 매우 복잡할 경우 totalCount를 위한 쿼리는 분리하여 작성하신 뒤 호출하는 것이 더 나은 상황들이 있습니다.

스프링 데이터 JPA에서 count 쿼리를 따로 작성하는 것 처럼 간단한 방법을 따로 지원하지 않기 때문에 fetchResults() 대신 fetch()fetchCount()로 분리해서 각각 호출한 뒤 결과를 가공해서 반환하는 방식으로 처리해야 합니다.

함수

SQL에서의 함수와 동일하게 사용할 수 있습니다.

package io.lcalmsky.querydsl.domain;

import com.querydsl.core.Tuple;
import com.querydsl.jpa.impl.JPAQueryFactory;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import javax.persistence.EntityManager;
import org.springframework.transaction.annotation.Transactional;;
import java.util.List;

import static io.lcalmsky.querydsl.domain.QPlayer.player;

@SpringBootTest
@Transactional
class PlayerTest {
    @Autowired
    EntityManager entityManager;
    private JPAQueryFactory queryFactory;

    @BeforeEach
    void setup() {
        Team tottenhamHotspur = new Team("Tottenham Hotspur F.C.");
        Team manchesterCity = new Team("Manchester City F.C.");
        entityManager.persist(tottenhamHotspur);
        entityManager.persist(manchesterCity);

        Player harryKane = new Player("Harry Kane", 27, tottenhamHotspur);
        Player heungminSon = new Player("Heungmin Son", 29, tottenhamHotspur);
        Player kevinDeBruyne = new Player("Kevin De Bruyne", 30, manchesterCity);
        Player raheemSterling = new Player("Raheem Shaquille Sterling", 26, manchesterCity);

        entityManager.persist(harryKane);
        entityManager.persist(heungminSon);
        entityManager.persist(kevinDeBruyne);
        entityManager.persist(raheemSterling);
        queryFactory = new JPAQueryFactory(entityManager);
    }

    @Test
    void simpleQuerydslWithFunction() {
        Tuple players = queryFactory.select(player.count(), player.age.sum(), player.age.avg(), player.age.max(), player.age.min())
                .from(player)
                .fetchOne();
        System.out.println(players);
    }
}

count(), sum(), avg(), max(), min() 등 사용하고자 하는 함수를 select() 메서드 안에 파라미터로 전달하면 아주 쉽게 원하는 쿼리를 작성할 수 있습니다.

이 때 결과는 Tuple이라는 인터페이스로 반환됩니다.

Tuple 자체는 실무에서는 거의 사용하지 않지만 사용법은 나중에 다시 다룰 예정입니다.

테스트를 실행해서 쿼리 및 결과만 확인해보면,

2021-07-18 03:58:27.552 DEBUG 2565 --- [           main] org.hibernate.SQL                        : 
    /* select
        count(player),
        sum(player.age),
        avg(player.age),
        max(player.age),
        min(player.age) 
    from
        Player player */ select
            count(player0_.player_id) as col_0_0_,
            sum(player0_.age) as col_1_0_,
            avg(cast(player0_.age as double)) as col_2_0_,
            max(player0_.age) as col_3_0_,
            min(player0_.age) as col_4_0_ 
        from
            player player0_
[4, 112, 28.0, 30, 26]

이렇게 함수들이 정상 동작한 것을 확인할 수 있습니다.

집합(group by, having)

함수와 같이 사용하려면 group byhaving 절이 필수겠죠. 이 부분도 아주 간단히 해결할 수 있습니다.

팀별로 선수들의 평균 나이를 구하는 테스트를 작성해보겠습니다.

package io.lcalmsky.querydsl.domain;

import com.querydsl.core.Tuple;
import com.querydsl.jpa.impl.JPAQueryFactory;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import javax.persistence.EntityManager;
import org.springframework.transaction.annotation.Transactional;;
import java.util.List;

import static io.lcalmsky.querydsl.domain.QPlayer.player;
import static io.lcalmsky.querydsl.domain.QTeam.team;

@SpringBootTest
@Transactional
class PlayerTest {
    @Autowired
    EntityManager entityManager;
    private JPAQueryFactory queryFactory;

    @BeforeEach
    void setup() {
        Team tottenhamHotspur = new Team("Tottenham Hotspur F.C.");
        Team manchesterCity = new Team("Manchester City F.C.");
        entityManager.persist(tottenhamHotspur);
        entityManager.persist(manchesterCity);

        Player harryKane = new Player("Harry Kane", 27, tottenhamHotspur);
        Player heungminSon = new Player("Heungmin Son", 29, tottenhamHotspur);
        Player kevinDeBruyne = new Player("Kevin De Bruyne", 30, manchesterCity);
        Player raheemSterling = new Player("Raheem Shaquille Sterling", 26, manchesterCity);

        entityManager.persist(harryKane);
        entityManager.persist(heungminSon);
        entityManager.persist(kevinDeBruyne);
        entityManager.persist(raheemSterling);
        queryFactory = new JPAQueryFactory(entityManager);
    }

    @Test
    void simpleQueryDslWithAggregation() {
        List<Tuple> ages = queryFactory.select(team.name, player.age.avg())
                .from(player)
                .join(player.team, team)
                .groupBy(team.name)
                .having(player.age.avg().goe(28))
                .fetch();
        System.out.println(ages);
    }
}

player Entity에선 ageselect해서 평균을 구하고, team Entity에서는 이름만 select 하도록 하였고, join()을 이용해 playerteamteam Entity를 매핑하였습니다.

그리고 마지막으로 team Entityname으로 group by하여 팀 이름별로 평균 나이를 획득하도록 하였고 평균 나이가 28세 이상인 팀만 추려내기위해 having절을 사용하였습니다.

테스트를 실행하면,

2021-07-18 04:17:36.283 DEBUG 2684 --- [           main] org.hibernate.SQL                        : 
    /* select
        team.name,
        avg(player.age) 
    from
        Player player   
    inner join
        player.team as team 
    group by
        team.name 
    having
        avg(player.age) >= ?1 */ select
            team1_.name as col_0_0_,
            avg(cast(player0_.age as double)) as col_1_0_ 
        from
            player player0_ 
        inner join
            team team1_ 
                on player0_.team_id=team1_.team_id 
        group by
            team1_.name 
        having
            avg(cast(player0_.age as double))>=?
[[Manchester City F.C., 28.0], [Tottenham Hotspur F.C., 28.0]]

쿼리가 잘 적용되고 결과 또한 정확히 출력되는 것을 확인할 수 있습니다.


다음 포스팅에선 join 문법에 대해 다루겠습니다.

728x90
반응형
댓글
댓글쓰기 폼