Spring Data JPA repository 로 변경
@Repository
public interface MemberRepository extends JpaRepository <Member , Long > {
List<Member> findByUsername (String username) ;
}
test
querydsl의 전용 기능인 search를 작성할 수 없다.
따라서 사용자 정의 리포지토리가 필요하다.
@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
출처 - 인프런 https://www.inflearn.com/course/querydsl-%EC%8B%A4%EC%A0%84
사용자 정의 리포지토리 MemberRepositoryCustom을 만들고 MemberRepository가 두 인터페이스를 상속하도록 할 예정이다.
public interface MemberRepositoryCustom {
List<MemberTeamDto> search (MemberSearchCondition condition) ;
}
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와 MemberRepository Custom을 상속한다.
public interface MemberRepository extends JpaRepository <Member , Long >, MemberRepositoryCustom {
List<Member> findByUsername (String username) ;
}
test
MemberRepo는 CustomRepo를 상속한다.
test 클래스에서 MemberRepo를 변수로 선언하면 CustomRepo 안에 정의된 search()메서드를 사용할 수 있다.
@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가지 추가
다음과 같이 새로운 메서드를 두 개 추가한다.
public interface MemberRepositoryCustom {
List<MemberTeamDto> search (MemberSearchCondition condition) ;
Page<MemberTeamDto> searchPageSimple (MemberSearchCondition condition) ;
Page<MemberTeamDto> searchPageComplex (MemberSearchCondition condition) ;
}
Ex) 전체 카운트를 한번에 조회하는 단순한 방법
@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) 데이터 내용과 전체 카운트를 별도로 조회하는 방법
@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
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
count 쿼리가 생략 가능한 경우에 생략해서 처리한다.
페이지 시작하면서 컨텐츠 사이즈가 페이지 사이즈보다 작은 경우(예를 들어 한 페이지의 사이즈가 10개로 설정되어 있는데 마지막 페이지에 데이터가 10보가 작은 경우)
마지막 페이지일 때, 전체 사이즈 = offset + 컨텐츠 사이즈
Spring Data 페이징 활용 (3) 컨트롤러 개발
Ex) simpleSearch()
@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와 같은 데이터가 모두 포함되어있어서 편리하다.
{
"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 로 변환
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