중급자를 위해 준비한
[백엔드, 웹 개발] 강의입니다.
Querydsl의 기초부터 실무 활용까지, 한번에 해결해보세요!
✍️
이런 걸
배워요!
Querydsl을 기초부터 실무활용까지 한번에 배울 수 있습니다.
단순한 기능 설명을 넘어 실무활용 노하우를 배울 수 있습니다.
JPA를 사용할 때 동적 쿼리와 복잡한 쿼리 문제를 해결할 수 있습니다.
복잡한 쿼리, 동적 쿼리는 이제 안녕!
Querydsl로 자바 백엔드 기술을 단단하게.
???? 본 강의는 로드맵 과정입니다.
- 본 강의는 자바 백엔드 개발의 실전 코스를 완성하는 마지막 강의입니다. 스프링 부트와 JPA 실무 완전 정복 로드맵을 우선 확인해주세요. (링크)
강좌
https://www.inflearn.com/course/querydsl-실전#
강의자료
https://github.com/braverokmc79/jpa-basic-lecture-file2
소스 :
https://github.com/braverokmc79/jpa-querydsl
[7] 실무 활용 - 스프링 데이터 JPA와 Querydsl
34.스프링 데이터 JPA 리포지토리로 변경
강의 :
https://www.inflearn.com/course/lecture?courseSlug=querydsl-실전&unitId=30149&tab=curriculum
스프링 데이터 JPA - MemberRepository 생성
package study.querydsl.repository; import org.springframework.data.jpa.repository.JpaRepository; import study.querydsl.entity.Member; import java.util.List; public interface MemberRepository extends JpaRepository<Member, Long> { List<Member> findByUsername(String username); }
스프링 데이터 JPA 테스트
package study.querydsl.repository; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.transaction.annotation.Transactional; import study.querydsl.entity.Member; import javax.persistence.EntityManager; import java.util.List; @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(); Assertions.assertThat(findMember).isEqualTo(member); List<Member> result1 = memberRepository.findAll(); Assertions.assertThat(result1).containsExactly(member); List<Member> result2=memberRepository.findByUsername("member1"); Assertions.assertThat(result2).containsExactly(member); } }
35.사용자 정의 리포지토리
강의 :
https://www.inflearn.com/course/lecture?courseSlug=querydsl-실전&unitId=30149&tab=curriculum
사용자 정의 리포지토리 사용법
1. 사용자 정의 인터페이스 작성
2. 사용자 정의 인터페이스 impl 구현
3. 스프링 데이터 리포지토리에 사용자 정의 인터페이스 상속
4. 굳이 사용자 정의 인터페이스 구현하지 않고 @Repository 해서 개별 구현해도 된다.
1) 사용자 정의 인터페이스 작성
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); }
2. 사용자 정의 인터페이스 Impl 구현
package study.querydsl.repository; import com.querydsl.core.types.dsl.BooleanExpression; import com.querydsl.jpa.impl.JPAQueryFactory; import study.querydsl.dto.MemberSearchCondition; import study.querydsl.dto.MemberTeamDto; import study.querydsl.dto.QMemberTeamDto; import javax.persistence.EntityManager; import java.util.List; import static org.springframework.util.StringUtils.hasText; import static study.querydsl.entity.QMember.member; import static study.querydsl.entity.QTeam.team; public class MemberRepositoryImpl implements MemberRepositoryCustom{ 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()), ageGoeEq(condition.getAgeGoe()), ageLoeEq(condition.getAgeLoe()) ) .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 ageGoeEq(Integer ageGoe) { return ageGoe!=null? member.age.goe(ageGoe) : null; } private BooleanExpression ageLoeEq(Integer ageLoe) { return ageLoe!=null? member.age.loe(ageLoe) : null; } }
3) 스프링 데이터 리포지토리에 사용자 정의 인터페이스 상속
package study.querydsl.repository; import org.springframework.data.jpa.repository.JpaRepository; import study.querydsl.entity.Member; import java.util.List; public interface MemberRepository extends JpaRepository<Member, Long> , MemberRepositoryCustom{ List<Member> findByUsername(String username); }
커스텀 리포지토리 동작 테스트 추가
package study.querydsl.repository; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.transaction.annotation.Transactional; import study.querydsl.dto.MemberSearchCondition; import study.querydsl.dto.MemberTeamDto; import study.querydsl.entity.Member; import study.querydsl.entity.Team; import javax.persistence.EntityManager; import java.util.List; import static org.junit.jupiter.api.Assertions.*; @SpringBootTest @Transactional class MemberRepositoryImplTest { @Autowired EntityManager em; @Autowired MemberRepository memberRepository; @Test public void search(){ 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(member3); em.persist(member4); MemberSearchCondition condition=new MemberSearchCondition(); condition.setAgeGoe(35); condition.setAgeLoe(40); condition.setTeamName("teamB"); List<MemberTeamDto> result = memberRepository.search(condition); Assertions.assertThat(result).extracting("username").containsExactly("member4"); } }
4) 굳이 사용자 정의 인터페이스 구현하지 않고 @Repository 해서 개별 구현해도 된다.
package study.querydsl.repository; import com.querydsl.core.types.dsl.BooleanExpression; import com.querydsl.jpa.impl.JPAQueryFactory; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Repository; import study.querydsl.dto.MemberSearchCondition; import study.querydsl.dto.MemberTeamDto; import study.querydsl.dto.QMemberTeamDto; import java.util.List; import static org.springframework.util.StringUtils.hasText; import static study.querydsl.entity.QMember.member; import static study.querydsl.entity.QTeam.team; @Repository @RequiredArgsConstructor public class MemberQueryRepository { private final JPAQueryFactory queryFactory; 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()), ageGoeEq(condition.getAgeGoe()), ageLoeEq(condition.getAgeLoe()) ) .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 ageGoeEq(Integer ageGoe) { return ageGoe!=null? member.age.goe(ageGoe) : null; } private BooleanExpression ageLoeEq(Integer ageLoe) { return ageLoe!=null? member.age.loe(ageLoe) : null; } }
MemberQueryRepositoryTest
package study.querydsl.repository; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.transaction.annotation.Transactional; import study.querydsl.dto.MemberSearchCondition; import study.querydsl.dto.MemberTeamDto; import study.querydsl.entity.Member; import study.querydsl.entity.Team; import javax.persistence.EntityManager; import java.util.List; @SpringBootTest @Transactional class MemberQueryRepositoryTest { @Autowired EntityManager em; @Autowired MemberQueryRepository memberQueryRepository; @Test public void search(){ 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(member3); em.persist(member4); MemberSearchCondition condition=new MemberSearchCondition(); condition.setAgeGoe(35); condition.setAgeLoe(40); condition.setTeamName("teamB"); List<MemberTeamDto> result = memberQueryRepository.search(condition); Assertions.assertThat(result).extracting("username").containsExactly("member4"); } }
36.스프링 데이터 페이징 활용1 - Querydsl 페이징 연동
강의 :
https://www.inflearn.com/course/lecture?courseSlug=querydsl-실전&unitId=30151&tab=curriculum
스프링 데이터의 Page, Pageable을 활용해보자.
전체 카운트를 한번에 조회하는 단순한 방법
데이터 내용과 전체 카운트를 별도로 조회하는 방법
사용자 정의 인터페이스에 페이징 2가지 추가
package study.querydsl.repository; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import study.querydsl.dto.MemberSearchCondition; import study.querydsl.dto.MemberTeamDto; import java.util.List; public interface MemberRepositoryCustom { List<MemberTeamDto> search(MemberSearchCondition condition); Page<MemberTeamDto> searchPageSimple(MemberSearchCondition condition, Pageable pageable); Page<MemberTeamDto> searchPageComplex(MemberSearchCondition condition, Pageable pageable); }
1) 전체 카운트를 한번에 조회하는 단순한 방법
searchPageSimple(), fetchResults() 사용
/** * 단순한 페이징, fetchResults() 사용 */ @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()), ageGoeEq(condition.getAgeGoe()), ageLoeEq(condition.getAgeLoe()) ) .offset(pageable.getOffset()) .limit(pageable.getPageSize()) .fetchResults(); List<MemberTeamDto> content = results.getResults(); long total=results.getTotal(); return new PageImpl<>(content,pageable,total); }
Querydsl이 제공하는 fetchResults() 를 사용하면 내용과 전체 카운트를 한번에 조회할 수 있다.(실제 쿼리는 2번 호출)
fetchResult() 는 카운트 쿼리 실행시 필요없는 order by 는 제거한다.
2) 데이터 내용과 전체 카운트를 별도로 조회하는 방법
searchPageComplex()
/** * 복잡한 페이징 * 데이터 조회 쿼리와, 전체 카운트 쿼리를 분리 */ @Override public Page<MemberTeamDto> searchPageComplex(MemberSearchCondition condition, Pageable pageable) { List<MemberTeamDto> content = 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()), ageGoeEq(condition.getAgeGoe()), ageLoeEq(condition.getAgeLoe()) ) .offset(pageable.getOffset()) .limit(pageable.getPageSize()) .fetch(); long total =queryFactory .selectFrom(member) // .leftJoin(member.team, team) .where( usernameEq(condition.getUsername()), teamNameEq(condition.getTeamName()), ageGoeEq(condition.getAgeGoe()), ageLoeEq(condition.getAgeLoe()) ) .fetchCount(); return new PageImpl<>(content, pageable, total); } 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 ageGoeEq(Integer ageGoe) { return ageGoe!=null? member.age.goe(ageGoe) : null; } private BooleanExpression ageLoeEq(Integer ageLoe) { return ageLoe!=null? member.age.loe(ageLoe) : null; }
전체 카운트를 조회 하는 방법을 최적화 할 수 있으면 이렇게 분리하면 된다.
(예를 들어서 전체 카운트를 조회할 때 조인 쿼리를 줄일 수 있다면 상당한 효과가 있다.)
코드를 리펙토링해서 내용 쿼리과 전체 카운트 쿼리를 읽기 좋게 분리하면 좋다.
테스트
@Test public void searchPageSimpleAndComplexTest(){ 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(member3); em.persist(member4); MemberSearchCondition condition=new MemberSearchCondition(); PageRequest pageRequest =PageRequest.of(0,3); // Page<MemberTeamDto> result=memberRepository.searchPageSimple(condition, pageRequest); Page<MemberTeamDto> result=memberRepository.searchPageComplex(condition, pageRequest); Assertions.assertThat(result.getSize()).isEqualTo(3); Assertions.assertThat(result.getContent()).extracting("username").containsExactly("member1", "member2", "member3"); }
★★★37.스프링 데이터 페이징 활용2 - CountQuery 최적화
필수적용할것 페이징 최적화
강의 :
https://www.inflearn.com/course/lecture?courseSlug=querydsl-실전&unitId=30152&tab=curriculum
/** * 복잡한 페이징 * 데이터 조회 쿼리와, 전체 카운트 쿼리를 분리 */ @Override public Page<MemberTeamDto> searchPageComplex(MemberSearchCondition condition, Pageable pageable) { List<MemberTeamDto> content = 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()), ageGoeEq(condition.getAgeGoe()), ageLoeEq(condition.getAgeLoe()) ) .offset(pageable.getOffset()) .limit(pageable.getPageSize()) .fetch(); JPAQuery<Member> countQuery = queryFactory .selectFrom(member) // .leftJoin(member.team, team) .where( usernameEq(condition.getUsername()), teamNameEq(condition.getTeamName()), ageGoeEq(condition.getAgeGoe()), ageLoeEq(condition.getAgeLoe()) ); // .fetchCount(); //첫페이지가 페이지 사이즈 content 사이즈 보다 작을 경우 countQuery.fetchCount() 작동하지 않고 해당 content 사이즈로 처리 //마지막 페이지일경우 countQuery 실행되지 않고, offset + 컨텐츠 사이즈를 더해서 전체 사이즈 구함 /** * count 쿼리가 생략 가능한 경우 생략해서 처리 * 페이지 시작이면서 컨텐츠 사이즈가 페이지 사이즈보다 작을 때 * 마지막 페이지 일 때 (offset + 컨텐츠 사이즈를 더해서 전체 사이즈 구함) */ // return PageableExecutionUtils.getPage(content,pageable , ()->countQuery.fetchCount() ); return PageableExecutionUtils.getPage(content,pageable , countQuery::fetchCount ); //return new PageImpl<>(content, pageable, total); }
스프링 데이터 라이브러리가 제공
count 쿼리가 생략 가능한 경우 생략해서 처리
페이지 시작이면서 컨텐츠 사이즈가 페이지 사이즈보다 작을 때
마지막 페이지 일 때 (offset + 컨텐츠 사이즈를 더해서 전체 사이즈 구함)
38.스프링 데이터 페이징 활용3 - 컨트롤러 개발
강의 :
https://www.inflearn.com/course/lecture?courseSlug=querydsl-실전&unitId=30153&tab=curriculum
실제 컨트롤러
package study.querydsl.controller; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import study.querydsl.dto.MemberSearchCondition; import study.querydsl.dto.MemberTeamDto; import study.querydsl.repository.MemberJpaRepository; import study.querydsl.repository.MemberRepository; import java.util.List; @RestController @RequiredArgsConstructor public class MemberController { private final MemberJpaRepository memberJpaRepository; private final MemberRepository memberRepository; @GetMapping("/v1/members") public List<MemberTeamDto> searchMemberV1(MemberSearchCondition condition){ return memberJpaRepository.search(condition); } //http://localhost:8080/v2/members?page=0&size=20 @GetMapping("/v2/members") public Page<MemberTeamDto> searchMemberV2(MemberSearchCondition condition, Pageable pageable){ return memberRepository.searchPageSimple(condition, pageable); } //http://localhost:8080/v3/members?size=5&page=2 //http://localhost:8080/v3/members?page=0&size=20 @GetMapping("/v3/members") public Page<MemberTeamDto> searchMemberV3(MemberSearchCondition condition, Pageable pageable){ return memberRepository.searchPageComplex(condition, pageable); } }
스프링 데이터 JPA는 자신의 정렬(Sort)을 Querydsl의 정렬(OrderSpecifier)로 편리하게 변경하는
기능을 제공한다. 이 부분은 뒤에 스프링 데이터 JPA가 제공하는 Querydsl 기능에서 살펴보겠다.
스프링 데이터의 정렬을 Querydsl의 정렬로 직접 전환하는 방법은 다음 코드를 참고하자.
스프링 데이터 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 를 사용하기 보다는
파라미터를 받아서 직접 처리하는 것을 권장한다.
댓글 ( 4)
댓글 남기기