일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | ||||||
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 |
- 프로젝트 환경설정
- 김영한
- QueryDSL
- jpa 활용
- JPA 활용2
- 페이징
- 컬렉션 조회 최적화
- 스프링MVC
- 불변 객체
- 값 타입 컬렉션
- JPA
- 일론머스크
- 스프링 데이터 JPA
- JPQL
- 기본문법
- API 개발 고급
- 검증 애노테이션
- JPA 활용 2
- Spring Data JPA
- 타임리프 문법
- 로그인
- 벌크 연산
- 임베디드 타입
- 스프링
- 스프링 mvc
- 예제 도메인 모델
- 실무활용
- Bean Validation
- 타임리프
- 트위터
- Today
- Total
RE-Heat 개발자 일지
[Querydsl] [6] 실무 활용 - 스프링 데이터 JPA와 Querydsl 본문
실전! Querydsl - 인프런 | 강의
Querydsl의 기초부터 실무 활용까지, 한번에 해결해보세요!, 복잡한 쿼리, 동적 쿼리는 이제 안녕! Querydsl로 자바 백엔드 기술을 단단하게. 🚩 본 강의는 로드맵 과정입니다. 본 강의는 자바 백엔
www.inflearn.com
인프런 김영한 님의 강의를 듣고 작성한 글입니다.
[1] 스프링 데이터 JPA 리포지토리로 변경
MemberRepository - 스프링 데이터 JPA
public interface MemberRepository extends JpaRepository<Member, Long> {
//select m From Member m where m.username =:username
List<Member> findByUsername(String 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);
}
}
- findById와 findAll은 스프링 데이터 JPA에 등록된 것 사용
- findByUsername은 쿼리 메소드 기능 3가지 중 하나인 메소드 이름으로 쿼리 생성 방식으로 만듦.
- 자세한 내용은 [Spring Data JPA] [4] 쿼리 메소드 기능(상편) 참고
▶ Querydsl를 쓰려면 사용자 정의 리포지토리가 필요하다.
[2] 사용자 정의 리포지토리
■ 사용자 정의 리포지토리 사용법
① 사용자 정의 인터페이스 작성
② 사용자 정의 인터페이스 구현
③ 스프링 데이터 리포지토리에 사용자 정의 인터페이스 상속
■ 사용자 정의 리포지토리 구성
1. 사용자 정의 인터페이스 작성
public interface MemberRepositoryCustom {
public List<MemberTeamDto> search(MemberSearchCondition condition);
}
2. 사용자 정의 인터페이스 구현
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) {
List<Member> result =
queryFactory.select(member)
.from(member)
.leftJoin(member.team, team)
.where(
usernameEq(condition.getUsername()),
teamNameEq(condition.getTeamName()),
ageBetween(condition.getAgeLoe(), condition.getAgeGoe())
)
.fetch();
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()),
ageBetween(condition.getAgeLoe(), condition.getAgeGoe())
)
.fetch();
}
private BooleanExpression ageBetween(Integer ageLoe, Integer ageGoe) {
if (ageLoe != null & ageGoe != null) {
return ageLoe(ageLoe).and(ageGoe(ageGoe));
} else {
return null;
}
}
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;
}
}
- MemberRepositoryImpl에서 extends로 MemberRepositoryCustom 상속
- Querydsl where 파라미터 방식으로 동적 쿼리 작성
- 반환은 QMemberTeamDto로
- 명명 규칙이 있으므로 꼭 Impl로 만들자
3. 스프링 데이터 리포지토리에 사용자 정의 인터페이스 상속
public interface MemberRepository extends JpaRepository<Member, Long>, MemberRepositoryCustom {
//select m From Member m where m.username =:username
List<Member> findByUsername(String username);
}
- extends로 사용자 정의 인터페이스(MemberRepositoryCustom) 상속
테스트 코드
@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");
}
TIP : API나 화면에 너무 특화 돼 있으면 따로 MemberQueryRepository를 만들어 사용하는 편이 낫다.
자세한 내용은 [JPA 활용2] [3] API 개발 고급 - 지연 로딩과 조회 성능 최적화 : V4 단점 관련 해결책 부분 참고
[3] 스프링 데이터 페이징 활용1 - Querydsl 페이징 연동
스프링 데이터의 Page, Pageable을 활용해 보자
사용자 정의 인터페이스 - 페이징 2가지 추가
public interface MemberRepositoryCustom {
public List<MemberTeamDto> search(MemberSearchCondition condition);
public Page<MemberTeamDto> searchPageSimple(MemberSearchCondition condition, Pageable pageable);
public Page<MemberTeamDto> searchPageComplex(MemberSearchCondition condition, Pageable pageable);
}
■ 전체 카운트를 한 번에 조회하는 단순한 방법
사용자 정의 인터페이스 구현
@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()),
ageBetween(condition.getAgeLoe(), condition.getAgeGoe())
)
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetchResults();
List<MemberTeamDto> content = results.getResults();
long total = results.getTotal();
return new PageImpl<>(content, pageable, total);
}
offset과 limit를 지정해 준 뒤 fetchResults()를 호출해 준다. 이러면 자동으로 count 쿼리도 날아간다.
테스트 코드
@Test
public void searchPageSimpleTest() {
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();
PageRequest pageRequest = PageRequest.of(0, 3);
Page<MemberTeamDto> result = memberRepository.searchPageSimple(condition, pageRequest);
assertThat(result.getSize()).isEqualTo(3);
assertThat(result.getContent()).extracting("username").containsExactly("member1", "member2", "member3");
}
- PageRequset에 offset 0과 limit 3을 담아 보낸다.
■ 데이터 내용과 전체 카운트를 별도로 조회하는 방법
Join한 테이블이 많아질 때 기존 방식을 쓰면 CountQuery도 수많은 조인과 함께 날아간다. 이럴 필요가 없으므로 분리하는 방법을 배워보자.
사용자 인터페이스 구현 - 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()),
ageBetween(condition.getAgeLoe(), condition.getAgeGoe())
)
.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);}
- 전체 카운트를 조회하는 방법을 이런 식으로 분리하면 조회할 때 조인을 줄일 수 있다.
- 코드를 리팩토링 해 내용 쿼리와 전체 카운트 쿼리를 읽기 좋게 분리하면 된다.
[4] 스프링 데이터 페이징 활용2 - CountQuery 최적화
PageableExecutionUtils.getPage()로 CountQuery 최적화가 가능하다.
@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()),
ageBetween(condition.getAgeLoe(), condition.getAgeGoe())
)
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetch();
JPAQuery<Member> countQuery = queryFactory
.selectFrom(member)
.where(
usernameEq(condition.getUsername()),
teamNameEq(condition.getTeamName()),
ageBetween(condition.getAgeLoe(), condition.getAgeGoe())
);
return PageableExecutionUtils.getPage(content, pageable, () -> countQuery.fetchCount());
//return new PageImpl<>(content, pageable, total);
}
스프링 데이터가 PageableExecutionUtils 라이브러리 제공
count 쿼리가 생략 가능하면 자동으로 생략해서 처리하는 장점이 있다.
예시]
① 페이지 시작이면서 컨텐츠 사이즈가 페이지 사이즈보다 작을 때
- 페이지 시작이면서 컨텐츠 사이즈(3) < 페이지 사이즈(100)
- 이러면 전체 사이즈를 구할 때 컨텐츠 사이즈를 가져다 쓰면 되므로 count 쿼리를 날리지 않는다.
② 마지막 페이지일 때(offset + 컨텐츠 사이즈를 더해서 전체 사이즈를 구한다)
[5] 스프링 데이터 페이징 활용3 - 컨트롤러 개발
MemberController
@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);
}
@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);
}
}
실행화면
① 포스트맨
② SQL
count 쿼리가 날아가지 않는다. 왜냐면 제일 처음에 100개를 다 가져왔기 때문에 count 쿼리를 날릴 이유가 없다.
■ 스프링 데이터 정렬(Sort)
스프링 데이터 JPA는 자신의 정렬(Sort)을 Querydsl 정렬(OrderSepcifier)로 편리하게 변경하는 기능을 제공한다.
■ 스프링 데이터 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를 사용하기보다는 파라미터를 받아서 직접 처리하자!
'백엔드 > Querydsl' 카테고리의 다른 글
[Querydsl] [7] 스프링 데이터 JPA가 제공하는 Querydsl 기능 (1) | 2023.09.30 |
---|---|
[Querydsl] [5] 실무 활용 - 순수 JPA와 Querydsl (0) | 2023.09.30 |
[Querydsl] [4] 중급 문법 (하편) - 동적 쿼리·벌크 연산·SQL function (0) | 2023.09.30 |
[Querydsl] [4] 중급 문법 (상편) - 프로젝션 반환·@QueryProjection (0) | 2023.09.29 |
[Querydsl] [3] 기본 문법 (하편) (0) | 2023.09.17 |