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
- 사용자 정의 리포지토리 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
'Spring Framework > [인프런] 실전! QueryDSL' 카테고리의 다른 글
Chapter 06. Spring Data JPA가 지원하는 querydsl 기능 (0) | 2023.05.13 |
---|---|
Chapter 04. 실무 활용 - 순수 JPA와 Querydsl (1) | 2023.05.06 |
Chapter 03. 중급 문법 (0) | 2023.05.01 |
Chapter 02. 기본 문법 (0) | 2023.04.13 |
Chapter 01. QueryDsl 프로젝트 생성 및 환경 설정 (0) | 2023.04.09 |