Spring Framework/[인프런] 실전! QueryDSL

Chapter 05. 실무 활용 - 스프링 데이터 JPA와 Querydsl

계란💕 2023. 5. 8. 23:00

Spring Data JPA repository 로 변경

  • Repository
<hide/>
@Repository
public interface MemberRepository extends JpaRepository<Member, Long> {

    List<Member> findByUsername(String username);

}

 

  • test
    • querydsl의 전용 기능인 search를 작성할 수 없다. 
    • 따라서 사용자 정의 리포지토리가 필요하다. 
<hide/>
@Test
public void basicTest(){
    Member member = new Member("member1", 10);
    memberRepository.save(member);
    Member findMember = memberRepository.findById(member.getId()).get();
    assertThat(findMember).isEqualTo(member);
    List<Member> result1 = memberRepository.findAll();
    assertThat(result1).containsExactly(member);
    List<Member> result2 = memberRepository.findByUsername("member1");
    assertThat(result2).containsExactly(member);
}

 


사용자 정의 Repository

출처 - 인프런&nbsp; &nbsp;https://www.inflearn.com/course/querydsl-%EC%8B%A4%EC%A0%84

  • 사용자 정의 리포지토리 MemberRepositoryCustom을 만들고 MemberRepository가 두 인터페이스를 상속하도록 할 예정이다. 

 

 

  • 인터페이스 
<hide/>
public interface MemberRepositoryCustom      {
    List<MemberTeamDto> search(MemberSearchCondition condition);
}

 

 

  • 사용자 인터페이스 구현
    • 앞에서 만든 customRepo를 구현한다. 
<hide/>
public class MemberRepositoryImpl implements MemberRepositoryCustom {
    private final JPAQueryFactory queryFactory;
    @Override
    public List<MemberTeamDto> search(MemberSearchCondition condition) {
        return queryFactory.select(new QMemberTeamDto(
                member.id, member.username, member.age, team.id, team.name
            ))
            .from(member)
            .leftJoin(member.team, team)
            .where(usernameEq(condition.getUsername()),
                teamNameEq(condition.getTeamName()),
                ageGoe(condition.getAgeGoe()),
                ageLoe(condition.getAgeLoe())
            )
            .fetch();
    }
}

 

 

  • 사용자 정의 인터페이스 상속
    • MemberRepository 는 JPA repo와  MemberRepositoryCustom을 상속한다. 
<hide/>
public interface MemberRepository extends JpaRepository<Member, Long>, MemberRepositoryCustom {
    List<Member> findByUsername(String username);
}

 

 

  • test
    • MemberRepo는 CustomRepo를 상속한다. 
    • test 클래스에서 MemberRepo를 변수로 선언하면 CustomRepo 안에 정의된  search()메서드를 사용할 수 있다. 
<hide/>
@Test
public void searchTest(){

    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);
    MemberSearchCondition condition = new MemberSearchCondition();
    condition.setAgeGoe(35);
    condition.setAgeLoe(40);
    condition.setTeamName("teamB");
    List<MemberTeamDto> result = memberRepository.search(condition);
    assertThat(result).extracting("username").containsExactly("member4");
}

Spring Data 페이징 활용 (1) Querydsl 페이징 연동

 

  • Spring data에서 제공하는 편리한 기능인 Page, Pageable를 활용해보려고 한다. 

 

  • 사용자 정의 인터페이스에 페이징 2가지 추가 
    • 다음과 같이 새로운 메서드를 두 개 추가한다. 
<hide/>
public interface MemberRepositoryCustom {

    List<MemberTeamDto> search(MemberSearchCondition condition);

    Page<MemberTeamDto> searchPageSimple(MemberSearchCondition condition);

    Page<MemberTeamDto> searchPageComplex(MemberSearchCondition condition);
    
}

 


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

<hide/>
@Override
public Page<MemberTeamDto> searchPageSimple(MemberSearchCondition condition, Pageable pageable) {
    QueryResults<MemberTeamDto> results = queryFactory.select(new QMemberTeamDto(
            member.id,
            member.username,
            member.age,
            team.id,
            team.name
        ))
        .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);
}
  • 단순한 페이징 
  • fetchResults(): 내용 전체 카운트를 한번에 조회할 수 있다. 실제 쿼리는 두 번 호출된다. 

 


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

<hide/>
@Override
public Page<MemberTeamDto> searchPageComplex(MemberSearchCondition condition,
    Pageable pageable) {

    List<MemberTeamDto> content = queryFactory.select(new QMemberTeamDto(
            member.id,
            member.username,
            member.age,
            team.id,
            team.name
        )).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.select(member)
        .from(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);
}
  • 전체 카운트를 조회하는 방법을 최적화할 수 있으면 이렇게 분리할 수 있다. 
  • 전체 카운트를 조회할 때 조인 쿼리를 줄일 수 있다면 효과가 있다. 
  • 코드를 리팩토링해서 내용 쿼리와 카운트 쿼리를 읽기 좋게 분리하면 좋다. 

 


Spring Data 페이징 활용 (2) CountQuery 최적화

 

  Ex) Spring Data paging 활용2

<hide/>
public Page<MemberTeamDto> searchPage_countQuery(MemberSearchCondition condition,
    Pageable pageable) {
    List<MemberTeamDto> content = queryFactory.select(new QMemberTeamDto(
            member.id,
            member.username,
            member.age,
            team.id,
            team.name
        )).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();
    JPAQuery<Member> countQuery = queryFactory
        .select(member)
        .from(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);
}
  • PageableExecutionUtils
    • Spring Data 라이브러리가 제공한다. 
  • count 쿼리가 생략 가능한 경우에 생략해서 처리한다. 
    • 페이지 시작하면서 컨텐츠 사이즈가 페이지 사이즈보다 작은 경우(예를 들어 한 페이지의 사이즈가 10개로 설정되어 있는데 마지막 페이지에 데이터가 10보가 작은 경우)
    • 마지막 페이지일 때, 전체 사이즈 = offset + 컨텐츠 사이즈

 


Spring Data 페이징 활용 (3) 컨트롤러 개발

 

  Ex) simpleSearch() 

 

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

 

  • GET http://localhost:8080/v2/members?size=5&page=2

 

  Note) 실행 결과

  • 마지막 페이지를 출력하면  empty = true 로 반환된다. 
  • URL에 저렇게 key=value 를 이어붙이면 Query Params으로 데이터가 들어가고 
  • 컨트롤러 메서드의 매개변수인 Pageable 의 변수에 페이지 관련 데이터가 세팅된다. 
  • 페이징에 필요한 데이터인 limit, offset, pageNumber, pageSize, totalElememts, totalPages와 같은 데이터가 모두 포함되어있어서 편리하다.   
<hide/>
{
    "content": [
        {
            "memberId": 13,
            "username": "member10",
            "age": 10,
            "teamId": 1,
            "teamName": "teamA"
        },
        {
            "memberId": 14,
            "username": "member11",
            "age": 11,
            "teamId": 2,
            "teamName": "teamB"
        },
        {
            "memberId": 15,
            "username": "member12",
            "age": 12,
            "teamId": 1,
            "teamName": "teamA"
        },
        {
            "memberId": 16,
            "username": "member13",
            "age": 13,
            "teamId": 2,
            "teamName": "teamB"
        },
        {
            "memberId": 17,
            "username": "member14",
            "age": 14,
            "teamId": 1,
            "teamName": "teamA"
        }
    ],
    "pageable": {
        "sort": {
            "unsorted": true,
            "sorted": false,
            "empty": true
        },
        "offset": 10,
        "pageSize": 5,
        "pageNumber": 2,
        "paged": true,
        "unpaged": false
    },
    "totalElements": 100,
    "last": false,
    "totalPages": 20,
    "numberOfElements": 5,
    "first": false,
    "size": 5,
    "number": 2,
    "sort": {
        "unsorted": true,
        "sorted": false,
        "empty": true
    },
    "empty": false
}

 


스프링 데이터 정렬(sort)

  • Spring Data JPA는 자신의 정렬(Sort)를 querydsl의 정렬(OrderSpecifier)로 편리하게 변경하는 기능을 제공한다. 

 

  Ex) Spring Data의 Sort를 Querydsl의 OrderSpecifier로 변환

<hide/>
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();
  • sort는 조건이 복잡해지면 Pageable의 Sort 기능을 사용하기 어렵다. 
  • 루트 엔티티 범위를 넘어가는 동적 정렬 기능이 필요하면 Spring Data 페이징이 제공하는 Sort를 사용하기 보다는 파라미터를 받아서 직접 처리하는 것을 권장한다. 

 

출처 -  https://www.inflearn.com/course/querydsl-%EC%8B%A4%EC%A0%84