스프링 데이터 JPA 리포지토리로 변경
기존에 작성했던 MemberJpaRepository를 스프링 데이터 JPA를 이용하여 MemberRepository를 생성해보자.
📌 MemberRepository.interface
package study.querydsl.repository;
public interface MemberRepository extends JpaRepository<Member, Long> {
List<Member> findByUsername(String username);
}
동적 쿼리를 제외한 정적쿼리는 스프링 JPA가 알아서 생성해준다.
동적쿼리는 사용자 정의 리포지토리가 필요하다.
📌 MemberRepositoryTest
package study.querydsl.repository;
@SpringBootTest
@Transactional
public class MemberRepositoryTest {
@Autowired
MemberRepository memberRepository;
@Autowired
EntityManager em;
@Test
void basicTest() {
Member member = new Member("member1", 10);
memberRepository.save(member);
Member findMember = memberRepository.findById(member.getId()).orElse(null);
assertThat(member).isEqualTo(findMember);
List<Member> result1 = memberRepository.findAll();
assertThat(result1).containsExactly(member);
List<Member> result2 = memberRepository.findByUsername("member1");
assertThat(result2).containsExactly(member);
}
}
스프링 데이터 JPA를 이용한 정적 쿼리 테스트
사용자 정의 리포지토리
복잡한 쿼리 같은 경우에는 사용자 정의 리포지토리를 통해서 구현해야한다.
사용자 리포지토리 사용법
- 사용자 정의 인터페이스 작성
- 사용자 정의 인터페이스 구현
- 스프링 데이터 리포지토리에 사용자 정의 인터페이스를 상속
🔍 사용자 정의 인터페이스 작성
📌 MemberRepositoryCustom.interface
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);
}
🔍 사용자 정의 인터페이스 구현
📌 MemberRepositoryCustomImpl
package study.querydsl.repository;
@RequiredArgsConstructor
public class MemberRepositoryCustomImpl 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()),
ageGoeEq(condition.getAgeGoe()),
ageLoeEq(condition.getAgeLoe())
)
.fetch();
}
private BooleanExpression usernameEq(String username) {
return StringUtils.hasText(username) ? member.username.eq(username) : null;
}
private BooleanExpression teamNameEq(String teamName) {
return StringUtils.hasText(teamName) ? team.name.eq(teamName) : null;
}
private BooleanExpression ageGoeEq(Integer ageGoe) {
return ageGoe != null ? member.age.goe(ageGoe) : null;
}
private BooleanExpression ageLoeEq(Integer ageLoe) {
return ageLoe != null ? member.age.loe(ageLoe) : null;
}
}
여기서 사용자 정의 인터페이스를 구현할 클래스 명칭이 중요하다.
[사용자_정의_인터페이스명 + Impl] 이렇게 작성해야 스프링 데이터 JPA에서 해당 클래스가 사용자 정의 인터페이스 구현 클래스라는 것을 자동으로 인식할 수 있다.
또는, [리포지토리명 + Imple] 이렇게 작성할 수 있으나 위 방법이 더 직관적이다.
🔍 스프링 데이터 리포지토리에 사용자 정의 인터페이스 상속
📌 MemberRepository.interface
package study.querydsl.repository;
import java.util.List;
public interface MemberRepository extends JpaRepository<Member, Long>, MemberRepositoryCustom {
List<Member> findByUsername(String username);
}
사용자 정의 인터페이스인 MemberRepositoryCustom을 상속받아야 한다.
🔍 커스텀 리포지토리 동작 테스트
📌 MemberRepositoryTest
package study.querydsl.repository;
@SpringBootTest
@Transactional
public class MemberRepositoryTest {
@Autowired
MemberRepository memberRepository;
@Autowired
EntityManager em;
/**
* 검색 조건
* - 나이 5 이상
* - 나이 40 이하
* - 팀명이 'teamB'인 경우
*/
@Test
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);
em.flush();
em.clear();
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");
}
}
/* select
member1.id,
member1.username,
member1.age,
team.id,
team.name
from
Member member1
left join
member1.team as team
where
team.name = ?1
and member1.age >= ?2
and member1.age <= ?3 */ select
member0_.member_id as col_0_0_,
member0_.username as col_1_0_,
member0_.age as col_2_0_,
team1_.team_id as col_3_0_,
team1_.name as col_4_0_
from
member member0_
left outer join
team team1_
on member0_.team_id=team1_.team_id
where
team1_.name=?
and member0_.age>=?
and member0_.age<=?
스프링 데이터 페이징 활용 - Querydsl 페이징 연동
스프링 데이터의 Page와 Pageable을 활용해보자.
🔍 사용자 정의 인터페이스에 페이징 기능을 추가한다.
📌 MemberRepositoryCustom.interface
package study.querydsl.repository;
import java.util.List;
public interface MemberRepositoryCustom {
// 페이징 (count 쿼리와 아닌 것을 분리)
Page<MemberTeamDto> searchPageComplex(MemberSearchCondition condition, Pageable pageable);
}
페이징 기능을하는 searchPageComplex() 메서드를 추가한다.
🔍 searchPagecomplex() 메서드 구현
📌 MemberRepositoryCustomImpl
package study.querydsl.repository;
import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.jpa.impl.JPAQuery;
import com.querydsl.jpa.impl.JPAQueryFactory;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.data.support.PageableExecutionUtils;
import org.springframework.util.StringUtils;
import study.querydsl.dto.MemberSearchCondition;
import study.querydsl.dto.MemberTeamDto;
import study.querydsl.dto.QMemberTeamDto;
import java.util.ArrayList;
import java.util.List;
import static study.querydsl.entity.QMember.member;
import static study.querydsl.entity.QTeam.team;
@RequiredArgsConstructor
public class MemberRepositoryCustomImpl implements MemberRepositoryCustom {
private final JPAQueryFactory queryFactory;
@Override
public Page<MemberTeamDto> searchPageComplex(MemberSearchCondition condition, Pageable pageable) {
// 데이터 조회 쿼리
List<MemberTeamDto> content = getMemberTeamDtos(condition, pageable);
// 전체 카운터 쿼리
JPAQuery<Long> totalCount = getTotalCount(condition);
return PageableExecutionUtils.getPage(content, pageable, totalCount::fetchOne);
// PageImpl보다 PageableExecutionUtils.getPage가 더 낫다.
// return new PageImpl<>(content, pageable, totalCount);
}
// 전체 카운터 쿼리
private JPAQuery<Long> getTotalCount(MemberSearchCondition condition) {
JPAQuery<Long> totalCount = queryFactory
.select(member.count())
.from(member)
.leftJoin(member.team, team)
.where(
usernameEq(condition.getUsername()),
teamNameEq(condition.getTeamName()),
ageGoeEq(condition.getAgeGoe()),
ageLoeEq(condition.getAgeLoe())
);
return totalCount;
}
// 데이터 조회 쿼리
private List<MemberTeamDto> getMemberTeamDtos(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()),
ageGoeEq(condition.getAgeGoe()),
ageLoeEq(condition.getAgeLoe())
)
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetch();
return content;
}
}
데이터 내용과 전체 카운트 쿼리를 개별적으로 처리한다. 이렇게 개별적으로 처리해야 전체 카운트 쿼리를 개별적으로 최적화가 가능하다.
Page<T> 타입을 반환할 때 PageImpl 생성자로 반환할 수 있으나 PageableExecutionUtils.getPage() 메서드로 반환하는 것이 더 낫다.
PageImpl은 Page인터페이스의 구현체이다 PageImpl에 첫번째 인자로는 content(조회된 컨텐츠), Pageable(요청으로부터 가져온 페이지 요청데이터), totalCount(전체 컨텐츠의 개수)를 주면 된다. 페이징 데이터가 많지않거나 접속량이 중요하지 않은 곳이라면 해당 방법을 사용해도 문제 없을 것 같다 .
PageableExecutionUtils.getPage() 메서드는 PageImpl과 같은 역할을 한다. 그러나 PageableExecutionUtils.getPage()는 마지막 인자로 함수를 전달하는데 내부 작동에 의해서 토탈카운트가 페이지사이즈 보다 적거나 , 마지막페이지 일 경우 해당 함수를 실행하지 않는다 쿼리를 조금더 줄일 수 있다. 위의 코드의 경우 카운트 쿼리 마지막에 fetchCount()가 여기서 관리되고 있다. PageableExecutionUtils.getPage() 메서드를 사용하면 조금 더 성능 최적화가 된다. PageImpl 보다는 PageableExecutionUtils.getPage() 메서드를 사용하자
🔍 페이징 처리 테스트
📌 MemberRepositoryCustomImplTest
package study.querydsl.repository;
@SpringBootTest
@Transactional
public class MemberRepositoryTest {
@Autowired
MemberRepository memberRepository;
@Autowired
EntityManager em;
@Test
void basicTest() {
Member member = new Member("member1", 10);
memberRepository.save(member);
Member findMember = memberRepository.findById(member.getId()).orElse(null);
assertThat(member).isEqualTo(findMember);
List<Member> result1 = memberRepository.findAll();
assertThat(result1).containsExactly(member);
List<Member> result2 = memberRepository.findByUsername("member1");
assertThat(result2).containsExactly(member);
}
/**
* 페이징 처리 테스트
* condition에 조건을 추가할 수 있으니 현재 데이터 수가 적어서 조건을 추가하지 않았다.
*/
@Test
void searchPageComplex() {
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);
em.flush();
em.clear();
MemberSearchCondition condition = new MemberSearchCondition();
PageRequest pageRequest = PageRequest.of(0, 3);
Page<MemberTeamDto> result = memberRepository.searchPageComplex(condition, pageRequest);
assertThat(result.getSize()).isEqualTo(3);
assertThat(result.getContent()).extracting("username").containsExactly("member1", "member2", "member3");
}
}
/* select
member1.id,
member1.username,
member1.age,
team.id,
team.name
from
Member member1
left join
member1.team as team */ select
member0_.member_id as col_0_0_,
member0_.username as col_1_0_,
member0_.age as col_2_0_,
team1_.team_id as col_3_0_,
team1_.name as col_4_0_
from
member member0_
left outer join
team team1_
on member0_.team_id=team1_.team_id limit ?
/* select
count(member1)
from
Member member1
left join
member1.team as team */ select
count(member0_.member_id) as col_0_0_
from
member member0_
left outer join
team team1_
on member0_.team_id=team1_.team_id
PageableExecutionUtils.getPage() 메서드는 count 쿼리가 생략 가능한 경우 생각해서 처리한다.
- 페이지 시작이면서 컨텐츠 사이즈가 페이즈 사이즈보다 작을 때
- 마지막 페이지 일 때 (offset + 컨텐츠 사이즈를 더해서 전체 사이즈 구함)
스프링 데이터 페이징 활용3 - 컨트롤러 개발
📌 MemberController
package study.querydsl.controller;
@RestController
@RequiredArgsConstructor
public class MemberController {
private final MemberJpaRepository memberJpaRepository;
private final MemberRepository memberRepository;
@GetMapping("/v2/members")
public Page<MemberTeamDto> searchMemberV2(MemberSearchCondition condition, Pageable pageable) {
return memberRepository.searchPageComplex(condition, pageable);
}
...
}
페이지는 0, size는 10으로 보내보자.
/* select
member1.id,
member1.username,
member1.age,
team.id,
team.name
from
Member member1
left join
member1.team as team */ select
member0_.member_id as col_0_0_,
member0_.username as col_1_0_,
member0_.age as col_2_0_,
team1_.team_id as col_3_0_,
team1_.name as col_4_0_
from
member member0_
left outer join
team team1_
on member0_.team_id=team1_.team_id limit ?
/* select
count(member1)
from
Member member1
left join
member1.team as team */ select
count(member0_.member_id) as col_0_0_
from
member member0_
left outer join
team team1_
on member0_.team_id=team1_.team_id
결과에 데이터 조회쿼리와 count쿼리가 둘 다 생성된 것을 알 수 있다.
PageableExecutionUtils.getPage()를 사용하면 마지막 페이지일 때와 시작페이지면서 컨텐즈 사이즈가 페이지 사이즈보다 작을 때 count 쿼리를 발생시키지 않는다. 이것을 확인해보자.
먼저 마지막 페이지일 경우
/* select
member1.id,
member1.username,
member1.age,
team.id,
team.name
from
Member member1
left join
member1.team as team */ select
member0_.member_id as col_0_0_,
member0_.username as col_1_0_,
member0_.age as col_2_0_,
team1_.team_id as col_3_0_,
team1_.name as col_4_0_
from
member member0_
left outer join
team team1_
on member0_.team_id=team1_.team_id limit ? offset ?
카운트 쿼리가 발생하지 않았다.
시작 페이지이면서 컨텐즈 사이즈가 페이즈 사이즈보다 작을 경우
/* select
member1.id,
member1.username,
member1.age,
team.id,
team.name
from
Member member1
left join
member1.team as team */ select
member0_.member_id as col_0_0_,
member0_.username as col_1_0_,
member0_.age as col_2_0_,
team1_.team_id as col_3_0_,
team1_.name as col_4_0_
from
member member0_
left outer join
team team1_
on member0_.team_id=team1_.team_id limit ?
이 경우에도 count 쿼리가 발생하지 않았다는 것을 알 수 있다.
🧷 참고. 스프링 데이터 정렬(Sort)
스프링 데이터 JPA는 자신의 정렬(Sort)을 Querydsl의 정렬(OrderSpecifier)로 편리하게 변경하는 기능을 제공한다.
스프링 데이터 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()
참고로 정렬(Sort)은 조건이 조금만 복잡해도 Pageable의 Sort기능을 사용하기 어렵다. 조인이 필요한 경우나 동적 정렬 기능이 필요하면 스프링 데이터 페이징이 제공하는 Sort를 사용하기보다는 파라미터를 받아서 직접 정렬 기능을 구현하는 것을 권장한다.
👀 참고자료
https://www.inflearn.com/course/Querydsl-%EC%8B%A4%EC%A0%84/dashboard
'[JPA] > 실전! Querydsl' 카테고리의 다른 글
[Querydsl] 스프링 데이터 JPA가 제공하는 Querydsl 기능 (0) | 2022.04.29 |
---|---|
[Querydsl] 실무 활용 - 순수 JPA와 Querydsl (0) | 2022.04.27 |
[QueryDsl] 중급 문법 (0) | 2022.04.26 |
[Querydsl] 기본 문법 (0) | 2022.04.24 |
[QueryDSL] 프로젝트 환경 설정 (0) | 2022.04.24 |