스프링 데이터 JPA 리포지토리로 변경
public interface MemberRepository extends JpaRepository<Member, Long> {
List<Member> findByUsername(String username); // select m from Member m where m.username=?
}
@SpringBootTest
@Transactional
class MemberRepositoryTest {
@Autowired
EntityManager em;
@Autowired MemberRepository memberRepository;
@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);
}
}
사용자 정의 리포지토리
- 사용자 정의 리포지토리 사용법
- 사용자 정의 인터페이스 작성
- 사용자 정의 인터페이스 구현
- 스프링 데이터 리포지토리에 사용자 정의 인터페이스 상속
public interface MemberRepositoryCustom {
List<MemberTeamDto> search(MemberSearchCondition condition);
}
public class MemberRepositoryImpl implements MemberRepositoryCustom{ // MemberRepository 라는 스프링데이터 인터페이스명 + Impl 규칙
private final JPAQueryFactory queryFactory;
public MemberRepositoryImpl(EntityManager em) {
this.queryFactory =new JPAQueryFactory(em);
}
@Override
public List<MemberTeamDto> search(MemberSearchCondition condition) {
return 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()))
// ageBetween(condition.getAgeLoe(), condition.getAgeGoe()))
.fetch();
}
private BooleanExpression usernameEq(String username) {
return hasText(username) ? member.username.eq(username) : null;
}
private BooleanExpression teamNameEq(String teamName) {
return hasText(teamName) ? team.name.eq(teamName) : null;
}
private BooleanExpression ageGoe(Integer ageGoe) {
return ageGoe != null ? member.age.goe(ageGoe) : null;
}
private BooleanExpression ageLoe(Integer ageLoe) {
return ageLoe != null ? member.age.loe(ageLoe) : null;
}
}
public interface MemberRepository extends JpaRepository<Member, Long>, MemberRepositoryCustom {
List<Member> findByUsername(String username); // select m from Member m where m.username=?
}
커스텀 리포지토리를 상속받지 않고,
조회용 기능을 화면에 맞게 분리해 내고 싶으면
별도의 조회용 리포지토리를 인터페이스가 아닌 클래스로 빼서 주입받아 쓰는 것도 좋은 방법이다.
스프링 데이터 페이징 활용1 - Querydsl 페이징 연동
public interface MemberRepositoryCustom {
List<MemberTeamDto> search(MemberSearchCondition condition);
Page<MemberTeamDto> searchPageSimple(MemberSearchCondition condition, Pageable pageable);
Page<MemberTeamDto> searchPageComplex(MemberSearchCondition condition, Pageable pageable);
}
전체 카운트를 한번에 조회하는 단순한 방법
MemberRepositoryImpl
@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();// fetchResults() 하면 컨텐츠용 쿼리, 카운트 커리 두개 날린다.
List<MemberTeamDto> content = results.getResults();
long total = results.getTotal();
return new PageImpl<>(content, pageable, total); // Page 의 구현체
}
참고로 orderBy 같은 쿼리는 토탈 카운트 쿼리에서는 들어가지 않게 최적화해준다.
데이터 내용과 전체 카운트를 별도로 조회하는 방법
@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);
}
직접 카운트 쿼리를 날린다.
컨텐트 쿼리는 복잡한데 카운트 쿼리는 쉽게 만들 수 있는 경우가 있을 수 있다. (ex 조인을 없애기 등)
그런데 query dsl 에서 제공하는 fetchResults를 쓰면 다 조인을 하기 때문에 최적화를 못한다.
따로 fetchCount() 로 분리하면 최적화할 수 있다.
예를 들어 카운트 쿼리를 해서 카운트가 0이면 컨텐츠 쿼리를 아예 안하는 식으로 최적화할 수도 있다.
데이터가 별로 없을 땐 fetchResult, 데이터가 몇천만건 정도씩 많으면 고려하면 된다.
스프링 데이터 페이징 활용 2 - CountQuery 최적화
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 new PageImpl<>(content, pageable, total);
return PageableExecutionUtils.getPage(content, pageable, () -> countQuery.fetchCount());
페이지의 시작이면서 컨텐츠 사이즈가 페이지의 사이즈보다 작거나, 마지막 페이지면 카운트 쿼리를 호출 안한다.
카운트 쿼리가 필요하면 날리고, 아니면 안날린다.
컨트롤러 개발
http://localhost:8080/v3/members?page=1&size=5
@GetMapping("/v2/members")
public Page<MemberTeamDto> searchMemberV2(MemberSearchCondition condition, Pageable pageable) {
return memberRepository.searchPageSimple(condition, pageable);
}
@GetMapping("/v3/members")
public Page<MemberTeamDto> searchMemberV3(MemberSearchCondition condition, Pageable pageable) {
return memberRepository.searchPageComplex(condition, pageable);
}
인프런 - 실전! Querydsl
'BACK > JPA' 카테고리의 다른 글
[JPA] JPA 시작하기, 동작 방식 (1) | 2024.03.31 |
---|---|
[JPA] JPA 소개, 사용 이유 (0) | 2024.03.30 |
[JPA] 실무 활용 - 순수 JPA 와 Querydsl (0) | 2024.03.15 |
[JPA] Querydsl 중급 문법 (프로젝션, 동적 쿼리, 벌크 연산, SQL function) (1) | 2024.03.15 |
[JPA] Querydsl 기본 문법 (검색, 조회, 정렬, 페이징, 집합, 조인등) (0) | 2024.03.15 |