Querydsl - (4)

bp.chys·2020년 6월 17일
1

JPA

목록 보기
14/15

동적 쿼리와 성능 최적화 조회

조회 최적화용 DTO

@AllArgsConstructor
@Data
public class MemberTeamDto {
    private Long memberId;
    private String username;
    private int age;
    private Long teamId;
    private String teamName;
}

회원 검색 조건 DTO

@Data
public class MemberSearchCondition {
    private String username;
    private String teamName;
    private Integer ageGoe;
    private Integer ageLoe;
}

where 다중 파라미터로 조회

public List<MemberTeamDto> search(MemberSearchCondition condition) {
    return queryFactory
            .select(Projections.constructor(MemberTeamDto.class,
                    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()))
            .fetch();
}


// 재사용 가능
private BooleanExpression usernameEq(String username) {
    return isEmpty(username) ? null : member.username.eq(username);
}

private BooleanExpression teamNameEq(String teamName) {
    retun isEmpty(teamName) ? null : team.name.eq(teamName);
}

private BooleanExpression ageGoe(Integer ageGoe) {
    return ageGoe == null ? null : member.age.goe(ageGoe);
}

private BooleanExpression ageLoe(Integer ageLoe) {
    return ageLoe == null ? null : member.age.loe(ageLoe);
}

사용자 정의 리포지토리

1. 사용자 정의 인터페이스 작성

public interface MemberRepositoryCustom {

    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) {
        return queryFactory
                .select(Projections.constructor(MemberTeamDto.class,
                        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)))
                .fetch();
                 
    }
    
    // 재사용 가능
    private BooleanExpression usernameEq(String username) {
        return isEmpty(username) ? null : member.username.eq(username);
    }

    private BooleanExpression teamNameEq(String teamName) {
        retun isEmpty(teamName) ? null : team.name.eq(teamName);
    }

    private BooleanExpression ageGoe(Integer ageGoe) {
        return ageGoe == null ? null : member.age.goe(ageGoe);
    }

    private BooleanExpression ageLoe(Integer ageLoe) {
        return ageLoe == null ? null : member.age.loe(ageLoe);
    }
}

3. 스프링 데이터 리포지토리에 사용자 정의 인터페이스 상속

public interface MemberRepository extends JpaRepository<Member, Long>, MemberRepositoryCustom {

    List<Member> findByUsername(String username);
}

spring data jpa - 페이징

  • page, pageable을 활용해보자.
  • 전체 카운트를 한번에 조회하는 단순한 방법
  • 데이터 내용과 전체 카운트를 별도로 조회하는 방법

사용자 정의 리파지토리에 메서드 추가

public interface MemberRepositoryCustom {

    List<MemberTeamDto> search(MemberSearchCondition condition);
    
    Page<MemberTeamDto> searchPageSimple(MemberSearchCondition condition, Pageable pageable);
    
    Page<MemberTeamDto> searchPageComplex(MemberSearchCondition condition, Pageable pageable);
}

전체 카운트를 한번에 조회 : fetchResults

@Override
public Page<MemberTeamDto> searchPageSimple(MemberSearchCondition condition, Pageable pageable) {
    
    QueryResults<MemberTeamDto> results = queryFactory
            .select(Projections.constructor(MemberTeamDto.class,
                    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();
    
    List<MemberTeamDto> content = results.getResults();
    long total = result.getTotal();
    
    // return new PageImpl<>(content, pageable, total);
    return PageableExecutionUtils.getPage(content, pageable, countQuery::fetchCount);
    
}
  • querydsl이 제공하는 fetchResults()를 사용하면 내용과 전체 카운트를 한번에 조회할 수 있다.
  • 실제 쿼리는 2번 호출된다.
  • fetchResults()는 카운트 쿼리 실행시 필요없는 order by는 제거한다.
  • count 쿼리가 생략 가능한 경우 생략해서 처리한다.
    • 페이지 시작이면서 컨텐츠 사이즈가 페이지 사이즈 보다 작을 때
    • 마지막 페이지 일때 (offset + 컨텐츠 사이즈를 더해서 전체 사이즈를 구함)

데이터 내용과 전체카운트를 별도로 조회하는 방법

@Override
public Page<MemberTeamDto> searchPageComplex(MemberSearchCondition condition, Pageable pageable) {
    
    List<MemberTeamDto> content = queryFactory
            .select(Projections.constructor(MemberTeamDto.class,
                    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())
            .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); 
}
  • Join이 복잡할 경우는 이 방법을 사용한다. (상당한 효과가 있다.)
  • 코드를 리팩토링해서 내용 쿼리와 전체 카운트 쿼리를 읽기 좋게 분리하면 좋다.

spring data jpa가 제공하는 querydsl 기능

인터페이스 지원 - QuerydslPredicateExecutor

public interface QuerydslPredicateExecutor<T> {

    Optional<T> findById(Predicate predicate);
    Iterable<T> findAll(Predicate predicate);
    long count(Predicate predicate);
    boolean exists(Predicate predicate);
// ... more functionality omitted.
}

public interface MemberRepository extends JpaRepository<User, Long>, QuerydslPredicateExecutor<User> {

}
  • 조인 x (묵시적 조인은 가능하지만, left join이 불가능하다.)
  • 클라이언트가 Querydsl에 의존해야 한다. 서비스 클래스가 Querydsl이라는 구현기술에 의존해야 한다.
  • 복잡한 실무환경에서는 사용하기에 한계가 명확하다.

리포지토리 지원 - QuerydslRepositorySupport

장점

  • getQuerydsl().applyPagination() : 스프링 데이터가 제공하는 페이징을 Querydsl로 편리하게 변환 가능
  • from()으로 시작 가능(최근에는 QueryFactory를 사용해서 select로 시작하는 것이 더 명시적)
  • EntityManager 제공
  • 융합해서 사용
  • sort 자동화도 된다.
public class MemberRepositoryImpl extends QuerydslRepositorySupport implements MemberRepositoryCustom {

    private final JPAQueryFactory jpaQueryFactory;

    public QuestionRepositoryImpl(JPAQueryFactory jpaQueryFactory) {
        super(Member.class);
        this.jpaQueryFactory = jpaQueryFactory;
    }
    
    ...
}


``` java
// Querydsl.java

...

public <T> JPQLQuery<T> applyPagination(Pageable pageable, JPQLQuery<T> query) {

    Assert.notNull(pageable, "Pageable must not be null!");
    Assert.notNull(query, "JPQLQuery must not be null!");

    if (pageable.isUnpaged()) {
		return query;		
    } 

    query.offset(pageable.getOffset());
    query.limit(pageable.getPageSize());

    return applySorting(pageable.getSort(), query);
}

public <T> JPQLQuery<T> applySorting(Sort sort, JPQLQuery<T> query) {

    Assert.notNull(sort, "Sort must not be null!");
    Assert.notNull(query, "Query must not be null!");

    if (sort.isUnsorted()) {
        return query;
    }

    if (sort instanceof QSort) {
        return addOrderByFrom((QSort) sort, query);
    }

    return addOrderByFrom(sort, query);
}
    
...
profile
하루에 한걸음씩, 꾸준히

1개의 댓글

comment-user-thumbnail
2021년 2월 4일

감사합니다. 도움 많이 되었습니다.

답글 달기