Study/실전! QueryDSL
Day 5. Spring Data JPA와 QueryDSL
주지민
2021. 10. 6. 21:13
반응형
본 포스트는 인프런 - 실전! QueryDSL의 강의를 듣고 공부한 내용입니다
https://www.inflearn.com/course/Querydsl-%EC%8B%A4%EC%A0%84
1. 사용자 정의 레포지토리 ( 기존 JpaRepository와 통합 )
- 기존 Spring Data JPA를 사용하기 위해서는 JpaRepository 인터페이스를 상속받아 사용하기만 하면 됐지만, 추가적인 QueryDSL을 적용하기 위해서는 Custom Repository를 추가로 생성하여 구현해줘야한다
- 구현 클래스 다이어그램
- 예제 코드
// MemberRepository public interface MemberRepository extends JpaRepository<Member, Long>, CustomMemberRepository { List<Member> findByUserName(String userName); }
// CustomMemberRepository public interface CustomMemberRepository { List<MemberTeamDto> search(MemberSearchCondition condition); }
-
public class MemberRepositoryImpl implements CustomMemberRepository { private final JPAQueryFactory jpaQueryFactory; public MemberRepositoryImpl(EntityManager entityManager) { this.jpaQueryFactory = new JPAQueryFactory(entityManager); } @Override public List<MemberTeamDto> search(final MemberSearchCondition condition) { return jpaQueryFactory .select(new QMemberTeamDto( member.id.as("memberId"), member.userName, member.age, team.id.as("teamId"), team.name )) .from(member) .leftJoin(member.team, team) .where( userNameEq(condition.getUserName()), teamNameEq(condition.getTeamName()), ageGoe(condition.getAgeGoe()), ageLoe(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 ageGoe(Integer ageGoe) { return Objects.nonNull(ageGoe)? member.age.goe(ageGoe) : null; } private BooleanExpression ageLoe(Integer ageLoe) { return Objects.nonNull(ageLoe)? member.age.loe(ageLoe) : null; } }
- 구현 설명
- JpaRepository를 상속하여 MemberRepository에 사용할 Entity와 @Id값을 명시하고 메서드 이름으로 생성되는 JPQL인 findByUserName 메서드를 정의
- QueryDSL을 쓰기위해서는 구현체가 있는 Custom한 Repository가 필요, CustomMemberRepository를 생성한 후에 사용할 메서드 템플릿 생성(search)
- 그 후 커스텀 레포지토리를 상속받아 실제 사용할 Repository 이름 + Impl의 이름을 갖는 MemberRepositoryImpl 클래스(구현체)를 생성하여 QueryDSL 내용 추가 ( 구현 클래스 이름 규칙은 반드시 규칙에 따라야 한다 )
- 구현체 주입등은 Spring Data JPA가 알아서 해준다
- 사용 방식 분석
- 핵심 비즈니스 로직으로 재사용 가능성이 있고, 엔티티 검색하는 것들은 위의 방법처럼 통합 Repository로 정의
- 라이프 싸이클 자체가 특정 부분(UI 등등)이라면 그냥 Repository를 분리해서 Class(@Repository)를 정의하는 것도 좋을것 같다
2. QueryDSL 페이징
- 간단한 버전
- fetchResults() 메서드 활용
- 다만 fetchResults()는 count 조회 쿼리와 contents 조회 쿼리 두개가 만들어져 나가기 때문에 불필요한 성능의 조회 쿼리가 나갈 수 있다. 유의해야한다
- 예제 코드
// Simple Page @Override public Page<MemberTeamDto> searchPageSimple(MemberSearchCondition condition, Pageable pageable) { QueryResults<MemberTeamDto> queryResults = jpaQueryFactory .select(new QMemberTeamDto( member.id.as("memberId"), member.userName, member.age, team.id.as("teamId"), 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(); return new PageImpl<>(queryResults.getResults(), pageable, queryResults.getTotal()); }
- Pageable 클래스를 파라미터로 받아 offset과 limit를 연결해준다
- org.springframework.data.domain.Pageable
- 참고로 Spring Page 번호는 0부터 시작이다 ( 0 index )
- 반환형은 Page를 구현하고있는 PageImpl을 통해 Page 반환
- 테스트 코드
// Test @Test void searchPageSimpleTest() { // given MemberSearchCondition condition = new MemberSearchCondition(); PageRequest pageRequest = PageRequest.of(0, 3); // when Page<MemberTeamDto> actual = memberRepository.searchPageSimple(condition, pageRequest); // then assertThat(actual.getSize()).isEqualTo(3); assertThat(actual.getContent()) .extracting("userName") .containsExactly("memberA", "memberB", "memberC"); }
- SQL Query
# JPQL /* select count(member1) from Member member1 left join member1.team as team */ # SQL Query select count(member0_.member_id) as col_0_0_ from member member0_ left outer join team team1_ on member0_.team_id=team1_.id;
# JPQL /* select member1.id as memberId, member1.userName, member1.age, team.id as teamId, team.name from Member member1 left join member1.team as team */ # SQL Query select member0_.member_id as col_0_0_, member0_.user_name as col_1_0_, member0_.age as col_2_0_, team1_.id as col_3_0_, team1_.name as col_4_0_ from member member0_ left outer join team team1_ on member0_.team_id=team1_.id limit 3;
- totalCount와 contents를 따로 조회 ( fetchResults를 이용하지 않음 )
- 예제 코드
// totalCount + contents @Override public Page<MemberTeamDto> searchPageComplex(MemberSearchCondition condition, Pageable pageable) { // get TotalCount long totalCount = jpaQueryFactory .select(new QMemberTeamDto( member.id.as("memberId"), member.userName, member.age, team.id.as("teamId"), team.name )) .from(member) .leftJoin(member.team, team) .where( userNameEq(condition.getUserName()), teamNameEq(condition.getTeamName()), ageGoe(condition.getAgeGoe()), ageLoe(condition.getAgeLoe()) ) .fetchCount(); // get Contents List<MemberTeamDto> contents = jpaQueryFactory .select(new QMemberTeamDto( member.id.as("memberId"), member.userName, member.age, team.id.as("teamId"), 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(); return new PageImpl<>(contents, pageable, totalCount); }
- 위에서도 설명했다시피 fetchResults()는 불필요한 성능을 가지는 totalCount 조회 쿼리를 날릴 수 도 있다
- 그럴땐 totalCount와 contents 조회 로직을 분리하여 직접 호출하자
- 다만 totalCount를 조회하는데 큰 복잡함이 없음에도 분리하는 작업에 시간을 쏟진 말자
- 예제 코드
- 추가: totalCount 조회 성능 최적화
- 때에 따라 totalCount는 생략 가능하다
- 페이지의 시작이면서, contents 사이즈가 page 사이즈보다 작을때
- 마지막 페이지 일때 ( offset + contents size로 전체 사이즈를 구할 수 있음 )
- PageableExecutionUtils.getPage() 메서드를 이용하여 위 상황에서 정의된 totalCount 날릴지 말지 결정한다
- org.springframework.data.support.PageableExecutionUtils
- 수정한 코드
// Using PageableExecutionUtils @Override public Page<MemberTeamDto> searchPageComplex(MemberSearchCondition condition, Pageable pageable) { // get TotalCount JPAQuery<MemberTeamDto> totalCountQuery = jpaQueryFactory .select(new QMemberTeamDto( member.id.as("memberId"), member.userName, member.age, team.id.as("teamId"), team.name )) .from(member) .leftJoin(member.team, team) .where( userNameEq(condition.getUserName()), teamNameEq(condition.getTeamName()), ageGoe(condition.getAgeGoe()), ageLoe(condition.getAgeLoe()) ); // get Contents List<MemberTeamDto> contents = jpaQueryFactory .select(new QMemberTeamDto( member.id.as("memberId"), member.userName, member.age, team.id.as("teamId"), 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(); return PageableExecutionUtils.getPage(contents, pageable, () -> totalCountQuery.fetchCount()); }
- 때에 따라 totalCount는 생략 가능하다
728x90
반응형