Spring Data JPA 리포지토리 변경


package study.querydsl.repository;

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

public interface MemberRepository extends JpaRepository<Member, Long> {
    // 메소드 쿼리 복습
    // select m from Member m where m.username = username
    List<Member> findByUsername(String username);

}

interface로 만들어야하는 것 잊지말자!!

 

 

사용자 정의 리포지토리


이전에 Spring Data JPA를 공부하며 나왔던 개념이다.  Spring Data JPA에서 제공하는 기능 외 동적 쿼리등 복잡한 쿼리를 만들고 싶을 때 사용해야되는 방법이다. 사용자 정의 리포지토리를 만드는 방법은 아래와 같다.

 

사용자 정의 리포지토리 사용법

  1. 사용자 정의 인터페이스 작성
  2. 사용자 정의 인터페이스 구현
  3. 스프링 데이터 리포지토리에 사용자 정의 인터페이스 상속

그림으로 보면 위와 같다. JpaRepository를 상속받은 MemberRepository가 있다. 동적 쿼리등 복잡한 쿼리를 추가하고 싶은데, Spring Data JPA에서 제공하는 기능으로는 한계가있다.

1. MemberRepositoryCustom interface 생성

2. MemberRepositoryImpl 생성 후 기능 구현

3. MemberRepository에 MemberRepositoryCustom 상속

여기서 지켜야하는 룰이 하나있다.

추가 기능을 수행하는 클래스 명은 = "JPARepository를 상속받은 클래스 명" + "아무값(없어도 됌)" + "Impl" 로 작성해야 한다.

 

MemberRepositoryImpl에는 기존에 qeurydsl로 만들었던 search 메소드와 연관된 메소드를 그대로 넣어주었다.

 

 

 

스프링 데이터 페이징 활용


  • 스프링 데이터의 Page, Pageable을 활용해보자.
  • 전체 카운트를 한번에 조회하는 단순한 방법
  • 데이터 내용과 전체 카운트를 별도로 조회하는 방법

전체 카운트를 한번에 조회하는 단순한 방법

    @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)
                .leftJoin(member.team, team)
                .where(
                        usernameEq(condition.getUsername()),
                        teamnameEq(condition.getTeamName()),
                        ageGoe(condition.getAgeGoe()),
                        ageLoe(condition.getAgeLoe()))
                .offset(pageable.getOffset())
                .limit(pageable.getPageSize())
                .fetchResults();

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

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

데이터 내용과 전체 카운트를 별도로 조회하는 방법

    @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)
                .leftJoin(member.team, team)
                .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)        // 어떤 상황에는 join이 필요없는 경우가 있다.
                .where(
                        usernameEq(condition.getUsername()),
                        teamnameEq(condition.getTeamName()),
                        ageGoe(condition.getAgeGoe()),
                        ageLoe(condition.getAgeLoe()))
                .fetchCount();
        return new PageImpl<>(content, pageable, total);
    }

별도로 조회하는 방법은 성능을 조금 높이기 위함일 수 있습니다. 예를들어, count 쿼리를 조회한 뒤 count가 0인 경우 content 쿼리를 하지 않을 수 있습니다. 아니면 카운트 쿼리를 content 쿼리보다 단순하게 조회할 수 있는 경우 성능상 이점이 생길 수 있습니다.

참고
Querydsl이 향후 fetchCount()를 제공하지 않습니다. 
fetchCount() => fetchOne()
fetchresults() => fetch() 권장

 

 

CountQuery 최적화


count 쿼리를 생략할 수 있는 경우는 아래와 같다.

  1. 페이지 시작이면서 컨텐츠 사이즈가 페이지 사이즈보다 작을 때 
  2. 마지막 페이지 일 때 (offset + 컨텐츠 사이즈를 더해서 전체 사이즈 구함)

위의 최적화를 할 수 있는 코드는 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)
                .leftJoin(member.team, team)
                .where(
                        usernameEq(condition.getUsername()),
                        teamnameEq(condition.getTeamName()),
                        ageGoe(condition.getAgeGoe()),
                        ageLoe(condition.getAgeLoe()))
                .offset(pageable.getOffset())
                .limit(pageable.getPageSize())
                .fetch();

        // countQuery에서는 fetchCount()를 해야 카운트 쿼리가 날라간다.
        JPAQuery<Member> countQuery = queryFactory
                .selectFrom(member)
                .leftJoin(member.team, team)        // 어떤 상황에는 join이 필요없는 경우가 있다.
                .where(
                        usernameEq(condition.getUsername()),
                        teamnameEq(condition.getTeamName()),
                        ageGoe(condition.getAgeGoe()),
                        ageLoe(condition.getAgeLoe())
                );

        // PageableExecutionUtils.getPage에서는 content pagealbe의 토탈 사이즈를 보고 count 쿼리를 생략할 수 있는 경우에
        // fetchCount() 쿼리 함수를 실행하지 않는다.
        return PageableExecutionUtils.getPage(content, pageable, () -> countQuery.fetchCount());
        //return new PageImpl<>(content, pageable, total);
    }
참고
Querydsl이 향후 fetchCount()를 제공하지 않습니다. 
fetchCount() => fetchOne()
PagealbeExecutionUtils.getPage에서는 content, pageable의 토탈 사이즈를 보고 count 쿼리를 생략할 수 있는 경우에 fetchCount()를 실행하지 않기 때문에 count쿼리가 필요하지 않는 경우 쿼리가 발생하지않는다.

getPage코드는 아래와 같다.

	public static <T> Page<T> getPage(List<T> content, Pageable pageable, LongSupplier totalSupplier) {

		Assert.notNull(content, "Content must not be null!");
		Assert.notNull(pageable, "Pageable must not be null!");
		Assert.notNull(totalSupplier, "TotalSupplier must not be null!");

		if (pageable.isUnpaged() || pageable.getOffset() == 0) {

			if (pageable.isUnpaged() || pageable.getPageSize() > content.size()) {
				return new PageImpl<>(content, pageable, content.size());
			}

			return new PageImpl<>(content, pageable, totalSupplier.getAsLong());
		}

		if (content.size() != 0 && pageable.getPageSize() > content.size()) {
			return new PageImpl<>(content, pageable, pageable.getOffset() + content.size());
		}

		return new PageImpl<>(content, pageable, totalSupplier.getAsLong());
	}

 

 

 

Controller 개발


이제 controller를 개발해보자.

MemberController 코드 추가

public class MemberController {
    ...

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

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

이제 postman에서 아래와 같이 page, size를 바꿔가며 데이터를 조회해보면 페이지별로 조회가 가능하다.

localhost:8080/v2/members?page=0&size=5

테스트를 해보니 V3의 경우 데이터 < 조회한 데이터 크기 경우 count 쿼리가 나가지 않는 것을 확인할 수 있습니다.

 

 

 

스프링 데이터 정렬


Spring Data JPA는 자신의 정렬(Sort)를 Querydsl의 정렬(OrderSpecifier)로 편리하게 변경하는 기능을 제공한다. 해당 부분은 뒤에 Spring Data JPA가 제공하는 Querydsl 기능에서 살펴본다. 

Spring Data Sort를 Querydsl의 OrderSpecifier로 변환

    JPAQuery<Member> query = queryFactory
            .selectFrom(member);
            
    for (Sort.Order o : pageable.getSort()) {
        PathBuilder pathBuilder = new PathBuilder(member.getType(),
    member.getMetadata());
    
        query.orderBy(new OrderSpecifier(o.isAscending() ? Order.ASC : Order.DESC,
                pathBuilder.get(o.getProperty())));
    }
    
    List<Member> result = query.fetch();
참고
정렬은 조건이 조금만 복잡해져도 Pagealbe의 sort기능을 사용하기 어렵다고 한다. 루트 엔티티 범위를 넘어가는 동적 정렬 기능이 필요하면 스프링 데이터 페이징이 제공하는 sort를 사용하기 보다는 파라미터를 받아서 직접 처리하는 것을 권장.

 

 

 

출처

 

실전! Querydsl - 인프런 | 강의

Querydsl의 기초부터 실무 활용까지, 한번에 해결해보세요!, - 강의 소개 | 인프런...

www.inflearn.com

 

+ Recent posts