[QueryDSL] EP5. 실무활용-스프링 데이터 JPA와 Querydsl
Web/QueryDSL

[QueryDSL] EP5. 실무활용-스프링 데이터 JPA와 Querydsl

 

 

스프링 데이터 JPA 리포지토리로 변경

 

순수 JPA로 만들었던 리포지토리를 스프링 데이터 JPA가 적용된 리포지토리로 변경합니다.

package study.querydsl.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import study.querydsl.entity.Member;

import java.util.List;

public interface MemberRepository extends JpaRepository<Member, Long> {

    List<Member> findByUsername(String username);

}

 

 

 

 

 

 

 

사용자 정의 리포지토리

스프링 데이터 JPA로 바꾸면서 스프링 데이터 JPA가 제공하지 않는 메서드들은 사용자 정의로 직접 구현해야합니다.

 

 

동적쿼리를 구현했던 search 메서드를 스프링 데이터 JPA의 사용자 정의 리포지토리로 옮겨보겠습니다.

 

 

MemberRepositoryCustom

package study.querydsl.repository;

import study.querydsl.dto.MemberSearchCondition;
import study.querydsl.dto.MemberTeamDto;

import java.util.List;

public interface MemberRepositoryCustom {
    List<MemberTeamDto> search(MemberSearchCondition condition);
}

 

 

 

MemberRepositoryImpl

package study.querydsl.repository;

import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.jpa.impl.JPAQueryFactory;
import study.querydsl.dto.MemberSearchCondition;
import study.querydsl.dto.MemberTeamDto;
import study.querydsl.dto.QMemberTeamDto;
import study.querydsl.entity.Member;

import javax.persistence.EntityManager;
import java.util.List;

import static org.springframework.util.StringUtils.hasText;
import static study.querydsl.entity.QMember.member;
import static study.querydsl.entity.QTeam.team;

public class MemberRepositoryImpl implements MemberRepositoryCustom{

    private final JPAQueryFactory queryFactory;

    public MemberRepositoryImpl(EntityManager em) {
        this.queryFactory = new JPAQueryFactory(em);
    }

    @Override
    public List<MemberTeamDto> search(MemberSearchCondition condition) {
        return queryFactory
                .select(new QMemberTeamDto(
                        member.id.as("memeerId"),
                        member.username,
                        member.age,
                        team.id.as("teamId"),
                        team.name.as("teamName")
                ))
                .from(member)
                .where(
                        usernameEq(condition.getUsername()),
                        teamNameEq(condition.getTeamName()),
                        ageGoe(condition.getAgeGoe()),
                        ageLoe(condition.getAgeLoe()))
                .fetch();
    }

    private BooleanExpression usernameEq(String username) {
        return hasText(username) ? member.username.eq(username) : null;

    }

    private BooleanExpression teamNameEq(String teamName) {
        return hasText(teamName) ? team.name.eq(teamName) : null;
    }

    private BooleanExpression ageGoe(Integer ageGoe) {
        return ageGoe != null ? member.age.goe(ageGoe) : null;
    }

    private BooleanExpression ageLoe(Integer ageLoe) {
        return ageLoe != null ? member.age.loe(ageLoe) : null;
    }
}

 

 

MemberRepository

 

package study.querydsl.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import study.querydsl.entity.Member;

import java.util.List;

public interface MemberRepository extends JpaRepository<Member, Long>, MemberRepositoryCustom{

    List<Member> findByUsername(String username);

}

 

 

 

 

꼭 모든 사용자 정의 쿼리 메서드들을 스프링 데이터 JPA의 사용자정의 리포지토리로 때려박지 않아도 됩니다.

화면에만 의존하거나 복잡한 조회용 쿼리는 조회용 리포지토리를 따로 만들어 써도 됩니다.

 

 

 

 

 

 

 

스프링 데이터 페이징 활용 1 - Querydsl 페이징 연동

 

 

스프링 데이터 JPA와 Querydsl을 이용해 페이징쿼리메서드를 만들어 봅니다.

 

MemberRepositoryCustom

package study.querydsl.repository;

import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import study.querydsl.dto.MemberSearchCondition;
import study.querydsl.dto.MemberTeamDto;

import java.util.List;

public interface MemberRepositoryCustom {
    List<MemberTeamDto> search(MemberSearchCondition condition);

    Page<MemberTeamDto> searchPageSimple(MemberSearchCondition condition, Pageable pageable);

    Page<MemberTeamDto> searchPageComplex(MemberSearchCondition condition, Pageable pageable);
}

 

 

searchPageSimple

 

.offset

매개변수로 받은 pageable에서 getOffset으로 offset 값을 가져올 수 있습니다.

 

.limit

매개변수로 받은 pageable에서 getPageSize로 pagesize 값을 가져올 수 있습니다.

 

.fetchResults

content를 가져오는 쿼리와 total count를 가져오는 쿼리가 나갑니다.

 

new PageImpl<>(content, pageable, total)

content, pageable, total 정보를 넣어 DTO인 page<MemberTeamDto> 로 반환합니다.

    @Override
    public Page<MemberTeamDto> searchPageSimple(MemberSearchCondition condition, Pageable pageable) {
        QueryResults<MemberTeamDto> results = queryFactory
                .select(new QMemberTeamDto(
                        member.id.as("memberId"),
                        member.username,
                        member.age,
                        team.id.as("teamId"),
                        team.name.as("teamName")
                ))
                .from(member)
                .where(
                        usernameEq(condition.getUsername()),
                        teamNameEq(condition.getTeamName()),
                        ageGoe(condition.getAgeGoe()),
                        ageLoe(condition.getAgeLoe()))
                .offset(pageable.getOffset())
                .limit(pageable.getPageSize())
                .fetchResults();// count Query 도 날아감

        List<MemberTeamDto> content = results.getResults();
        long total = results.getTotal();

        return new PageImpl<>(content, pageable, total);

    }

 

 

member중에서 앞에서 3개 멤버를 가져오는 테스트를 해봅니다.

    @Test
    public void searchPageTest() throws Exception {
        //given
        Team teamA = new Team("teamA");
        Team teamB = new Team("teamB");
        em.persist(teamA);
        em.persist(teamB);

        Member member1 = new Member("member1", 10, teamA);
        Member member2 = new Member("member2", 20, teamA);
        Member member3 = new Member("member3", 30, teamB);
        Member member4 = new Member("member4", 40, teamB);
        em.persist(member1);
        em.persist(member2);
        em.persist(member3);
        em.persist(member4);

        //when
        MemberSearchCondition condition = new MemberSearchCondition();
        PageRequest pageRequest = PageRequest.of(0, 3);

        Page<MemberTeamDto> result = memberRepository.searchPageSimple(condition, pageRequest);

        //then
        assertThat(result.getSize()).isEqualTo(3);
        assertThat(result.getContent()).extracting("username").containsExactly("member1", "member2", "member3");
    }

 

total count를 가져오는 쿼리와, 페이징 조건에 따라 member를 가져오는 쿼리를 볼 수 있습니다.

 

 

 

 

 

 

 

 

total count 쿼리 따로보내기

 

기존에 .fetchResults로 count쿼리를 같이 보내는 것이 아니라 fetch로 content를 가져오고 fetchCount로 count쿼리를 가져오도록 구현할 수 있습니다.

 

    @Override
    public Page<MemberTeamDto> searchPageComplex(MemberSearchCondition condition, Pageable pageable) {
        List<MemberTeamDto> content = queryFactory
                .select(new QMemberTeamDto(
                        member.id.as("memberId"),
                        member.username,
                        member.age,
                        team.id.as("teamId"),
                        team.name.as("teamName")
                ))
                .from(member)
                .where(
                        usernameEq(condition.getUsername()),
                        teamNameEq(condition.getTeamName()),
                        ageGoe(condition.getAgeGoe()),
                        ageLoe(condition.getAgeLoe()))
                .offset(pageable.getOffset())
                .limit(pageable.getPageSize())
                .fetch();

        long total = queryFactory
                .selectFrom(member)
                .leftJoin(member.team, team)
                .where(
                        usernameEq(condition.getUsername()),
                        teamNameEq(condition.getTeamName()),
                        ageGoe(condition.getAgeGoe()),
                        ageLoe(condition.getAgeLoe()))
                .fetchCount();

        return new PageImpl<>(content, pageable, total);
    }

 

 

 

 

 

스프링 데이터 페이징 활용 2 - CountQuery 최적화

 

count 쿼리가 생략 가능한 경우가 2가지 있습니다.

 

1. 페이지 시작이면서 컨텐츠 사이즈가 페이지 사이즈보다 작을 때

 

만약 데이터가 100개밖에 없을때 (?page=0&size=200) 이렇게 페이지 사이즈를 200개로 주면 첫번째 페이지에 모든 데이터가 다 담기게 됩니다. 이 경우 count query를 굳이 날리지 않아도 넘어온 데이터에 개수가 count쿼리가 됩니다.

 

2. 마지막 페이지 일 때

지금까지 나온 페이지수 * pagesize + 마지막 페이지의 컨텐츠(데이터) 수가 count 쿼리가 됩니다.

 

 

 

 

위 조건에 따른 계산을 스프링 데이터 JPA가 제공해줍니다.

 

countQuery부분에서 fetchCount()를 빼주고,

스프링 데이터 jpa의 PageableExecutionUtils.getPage에 content, pageable, 그리고 fetchCount()를 빼준 식에 .fetchCount를 lambda로 동적으로 계산할 수 있게 넘겨줍니다.

 

content, pageable의 정보를 보고 count 쿼리를 날릴지 말지 알아서 결정해줍니다.

    @Override
    public Page<MemberTeamDto> searchPageComplex(MemberSearchCondition condition, Pageable pageable) {
        List<MemberTeamDto> content = queryFactory
                .select(new QMemberTeamDto(
                        member.id.as("memberId"),
                        member.username,
                        member.age,
                        team.id.as("teamId"),
                        team.name.as("teamName")
                ))
                .from(member)
                .where(
                        usernameEq(condition.getUsername()),
                        teamNameEq(condition.getTeamName()),
                        ageGoe(condition.getAgeGoe()),
                        ageLoe(condition.getAgeLoe()))
                .offset(pageable.getOffset())
                .limit(pageable.getPageSize())
                .fetch();


        JPAQuery<Member> countQuery = queryFactory
                .selectFrom(member)
                .leftJoin(member.team, team)
                .where(
                        usernameEq(condition.getUsername()),
                        teamNameEq(condition.getTeamName()),
                        ageGoe(condition.getAgeGoe()),
                        ageLoe(condition.getAgeLoe()));


//        return PageableExecutionUtils.getPage(content, pageable, () -> countQuery.fetchCount());
        return PageableExecutionUtils.getPage(content, pageable, countQuery::fetchCount);

    }

 

 

 

 

 

 

 

 

 

스프링 데이터 페이징 활용 3 - 컨트롤러 개발

 

이제 컨트롤러를 만들어서 요청을 보내면 제대로 동작하는지 확인해 봅니다.

 

    @GetMapping("/v2/members")
    public Page<MemberTeamDto> searchMemberV2(MemberSearchCondition condition, Pageable pageable) {
        return memberRepository.searchPageSimple(condition, pageable);
    }

    @GetMapping("/v3/members")
    public Page<MemberTeamDto> searchMemberV3(MemberSearchCondition condition, Pageable pageable) {
        return memberRepository.searchPageComplex(condition, pageable);
    }

 

 

 

 

 

첫번째 페이지, 그리고 페이지 사이즈는 200요청을 v2로 보내보겠습니다.

 

 

멤버의 개수는 100개이므로 모든 멤버와 page정보가 응답으로 왔습니다.

{
    "content": [
        {
            "memberId": 3,
            "username": "member0",
            "age": 0,
            "teamId": 1,
            "teamName": "teamA"
        },
        {
            "memberId": 4,
            "username": "member1",
            "age": 1,
            "teamId": 2,
            "teamName": "teamB"
        },
        {
            "memberId": 5,
            "username": "member2",
            "age": 2,
            "teamId": 1,
            "teamName": "teamA"
        },
        {
            "memberId": 6,
            "username": "member3",
            "age": 3,
            "teamId": 2,
            "teamName": "teamB"
        },
        ...
        {
            "memberId": 101,
            "username": "member98",
            "age": 98,
            "teamId": 1,
            "teamName": "teamA"
        },
        {
            "memberId": 102,
            "username": "member99",
            "age": 99,
            "teamId": 2,
            "teamName": "teamB"
        }
    ],
    "pageable": {
        "sort": {
            "unsorted": true,
            "sorted": false,
            "empty": true
        },
        "offset": 0,
        "pageNumber": 0,
        "pageSize": 200,
        "unpaged": false,
        "paged": true
    },
    "last": true,
    "totalPages": 1,
    "totalElements": 100,
    "numberOfElements": 100,
    "sort": {
        "unsorted": true,
        "sorted": false,
        "empty": true
    },
    "size": 200,
    "number": 0,
    "first": true,
    "empty": false
}

 

 

그리고 count쿼리, content쿼리 두개가 나갑니다.

 

 

 

 

 

 

그런데 첫번째페이지, 그리고 컨텐츠의 사이즈가 페이지의 사이즈 보다 작습니다.

count쿼리가 굳이 필요하지 않은상황입니다.

 

v3에서 최적화가 잘 적용된다면 같은 요청을 보냈을때 count 쿼리가 나가지 않아야 합니다.

 

 

응답도 정상적으로 왔고,

{
    "content": [
        {
            "memberId": 3,
            "username": "member0",
            "age": 0,
            "teamId": 1,
            "teamName": "teamA"
        },
        {
            "memberId": 4,
            "username": "member1",
            "age": 1,
            "teamId": 2,
            "teamName": "teamB"
        },
        {
            "memberId": 5,
            "username": "member2",
            "age": 2,
            "teamId": 1,
            "teamName": "teamA"
        },
        {
            "memberId": 6,
            "username": "member3",
            "age": 3,
            "teamId": 2,
            "teamName": "teamB"
        },
        ...
        {
            "memberId": 101,
            "username": "member98",
            "age": 98,
            "teamId": 1,
            "teamName": "teamA"
        },
        {
            "memberId": 102,
            "username": "member99",
            "age": 99,
            "teamId": 2,
            "teamName": "teamB"
        }
    ],
    "pageable": {
        "sort": {
            "unsorted": true,
            "sorted": false,
            "empty": true
        },
        "offset": 0,
        "pageNumber": 0,
        "pageSize": 200,
        "unpaged": false,
        "paged": true
    },
    "last": true,
    "totalPages": 1,
    "totalElements": 100,
    "numberOfElements": 100,
    "sort": {
        "unsorted": true,
        "sorted": false,
        "empty": true
    },
    "size": 200,
    "number": 0,
    "first": true,
    "empty": false
}

 

count 쿼리는 나가지 않는 것을 볼 수 있습니다.

 

 

 

마지막 페이지인 경우에도 count쿼리는 나가지 않습니다.