RE-Heat 개발자 일지

[Querydsl] [7] 스프링 데이터 JPA가 제공하는 Querydsl 기능 본문

백엔드/Querydsl

[Querydsl] [7] 스프링 데이터 JPA가 제공하는 Querydsl 기능

RE-Heat 2023. 9. 30. 21:57
 

실전! Querydsl - 인프런 | 강의

Querydsl의 기초부터 실무 활용까지, 한번에 해결해보세요!, 복잡한 쿼리, 동적 쿼리는 이제 안녕! Querydsl로 자바 백엔드 기술을 단단하게. 🚩 본 강의는 로드맵 과정입니다. 본 강의는 자바 백엔

www.inflearn.com

인프런 김영한 님의 강의를 듣고 작성한 글입니다.

 

여기서 소개하는 기능은 제약이 커서 복잡한 실무 환경에서 쓰기엔 많이 부족한 편이다.


[1] 인터페이스 지원 - QuerydslPredicateExecutor

QuerydslPredicateExecutor

출처 공식 URL : https://docs.spring.io/spring-data/jpa/docs/2.2.3.RELEASE/reference/html/#core.extensions.querydsl

 

리포지토리

public interface MemberRepository extends JpaRepository<Member, Long>, MemberRepositoryCustom, QuerydslPredicateExecutor<Member> {
    //select m From Member m where m.username =:username
    List<Member> findByUsername(String username);
}
  • QuerydslPredicateExecutor<>를 상속한 것을 알 수 있다.

 

테스트

@Test
public void querydslPredicateExecutorTest() throws Exception {
    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);

    QMember member = QMember.member;
    Iterable<Member> result = memberRepository.findAll(member.age.between(10, 40).and(member.username.eq("member1")));

    for (Member findMember : result) {
        System.out.println("findMember = " + findMember);
    }
}
  • QuerydslPredicateExecutor가 제공하는 findAll()을 사용하면 ()에 조건을 바로 넣을 수 있다

 

생성된 쿼리

 

■ 한계점

  • 조인을 쓸 수 없다는 게 가장 크다.(묵시적 조인은 가능하나 left join을 쓸 수 없다)
  • 클라이언트가 Querydsl에 의존해야 한다. 서비스 클래스가 Querydsl에 의존해야 한다.
  • 복잡한 실무 환경에서 사용하기엔 한계가 명확하다


[2] Querydsl Web 지원

공식 URL : https://docs.spring.io/spring-data/jpa/docs/2.2.3.RELEASE/reference/html/#core.web.type-safe

URL로 Querydsl을 만들 수 있다.

 

그러나

세팅이 매우 복잡해 오히려 이 방식을 쓰는 게 더 헷갈린다.

 

■ 한계점

  • 단순한 조건만 가능하다
  • 조건을 커스텀하는 기능이 복잡하고 명시적이지 않다
  • 컨트롤러가 Querydsl에 의존
  • 복잡한 실무 환경에선 쓰기 어렵다
영한님 : 이거 쓰면 득보다 실이 많다

 

 

[3] 리포지토리 지원 - QuerydslRepositorySupport

기존 방식과 QuerydslRepositorySupport를 적용한 방식(우)

우측엔 offset, limit가 생략된 것을 알 수 있다. 그러나 그렇게 큰 도움은 안 되는 것 같다

 

■ 장점

  • getQuerydsl().applyPagination() 스프링 데이터가 제공하는 페이징을 Querydsl로 편리하게 변환 가능하다. (단! Sort는 오류발생)
  • from() 으로 시작하는 것이 가능하다. (최근에는 QueryFactory를 사용해서 select() 로 시작하는 것이 더 명시적이다.)
  • EntityManager를 제공한다.

 

■ 한계점

  • Querydsl 3.x 버전을 대상으로 만들어서 Querydsl 4.x에 나온 JPAQueryFactory로 시작할 수 없다.
    • select로 시작할 수 없다. from으로 시작해야 한다.
    • offset, limit를 뺄 수 있으나 chain이 끊긴다.
  • QueryFactory를 제공하지 않는다.
  • 스프링 데이터 Sort 기능이 정상 동작하지 않는다.


[4] Querydsl 지원 클래스 직접 만들기

QuerydslRepositorySupport가 지닌 한계를 극복하기 위해 직접 Querydsl 지원 클래스를 만들어보자

Querydsl4RepositorySupport

/**
 * Querydsl 4.x 버전에 맞춘 Querydsl 지원 라이브러리
 *
 * @author Younghan Kim
 * @see org.springframework.data.jpa.repository.support.QuerydslRepositorySupport
 */
@Repository
public abstract class Querydsl4RepositorySupport {
    private final Class domainClass;
    private Querydsl querydsl;
    private EntityManager entityManager;
    private JPAQueryFactory queryFactory;

    public Querydsl4RepositorySupport(Class<?> domainClass) {
        Assert.notNull(domainClass, "Domain class must not be null!");
        this.domainClass = domainClass;
    }

    @Autowired
    public void setEntityManager(EntityManager entityManager) {
        Assert.notNull(entityManager, "EntityManager must not be null!");
        JpaEntityInformation entityInformation = JpaEntityInformationSupport.getEntityInformation(domainClass, entityManager);
        SimpleEntityPathResolver resolver = SimpleEntityPathResolver.INSTANCE;
        EntityPath path = resolver.createPath(entityInformation.getJavaType());
        this.entityManager = entityManager;
        this.querydsl = new Querydsl(entityManager, new PathBuilder<>(path.getType(), path.getMetadata()));
        this.queryFactory = new JPAQueryFactory(entityManager);
    }

    @PostConstruct
    public void validate() {
        Assert.notNull(entityManager, "EntityManager must not be null!");
        Assert.notNull(querydsl, "Querydsl must not be null!");
        Assert.notNull(queryFactory, "QueryFactory must not be null!");
    }

    protected JPAQueryFactory getQueryFactory() {
        return queryFactory;
    }

    protected Querydsl getQuerydsl() {
        return querydsl;
    }

    protected EntityManager getEntityManager() {
        return entityManager;
    }

    protected <T> JPAQuery<T> select(Expression<T> expr) {
        return getQueryFactory().select(expr);
    }

    protected <T> JPAQuery<T> selectFrom(EntityPath<T> from) {
        return getQueryFactory().selectFrom(from);
    }

    protected <T> Page<T> applyPagination(Pageable pageable, Function<JPAQueryFactory, JPAQuery> contentQuery) {
        JPAQuery jpaQuery = contentQuery.apply(getQueryFactory());
        List<T> content = getQuerydsl().applyPagination(pageable, jpaQuery).fetch();
        return PageableExecutionUtils.getPage(content, pageable,
                jpaQuery::fetchCount);
    }

    protected <T> Page<T> applyPagination(Pageable pageable, Function<JPAQueryFactory, JPAQuery> contentQuery, Function<JPAQueryFactory, JPAQuery> countQuery) {
        JPAQuery jpaContentQuery = contentQuery.apply(getQueryFactory());
        List<T> content = getQuerydsl().applyPagination(pageable, jpaContentQuery).fetch();
        JPAQuery countResult = countQuery.apply(getQueryFactory());
        return PageableExecutionUtils.getPage(content, pageable,
                countResult::fetchCount);
    }
}

 

사용코드

@Repository
public class MemberTestRepository extends Querydsl4RepositorySupport {

    public MemberTestRepository(Class<?> domainClass) {
        super(Member.class);
    }

    public List<Member> basicSelect() {
        return select(member)
                .from(member)
                .fetch();
    }

    public List<Member> basicSelectFrom() {
        return selectFrom(member)
                .fetch();
    }

    public Page<Member> searchPageByApplyPage(MemberSearchCondition condition, Pageable pageable) {
        JPAQuery<Member> query = selectFrom(member)
                .leftJoin(member.team, team)
                .where(
                        usernameEq(condition.getUsername()),
                        teamNameEq(condition.getTeamName()),
                        ageBetween(condition.getAgeLoe(), condition.getAgeGoe())

                );
        List<Member> content = getQuerydsl().applyPagination(pageable, query)
                .fetch();

        return PageableExecutionUtils.getPage(content, pageable, query::fetchCount);
    }

    public Page<Member> applyPagination(MemberSearchCondition condition, Pageable pageable) {
        return applyPagination(pageable, query ->
                query.selectFrom(member)
                        .leftJoin(member.team, team)
                        .where(usernameEq(condition.getUsername()),
                                teamNameEq(condition.getTeamName()),
                                ageBetween(condition.getAgeLoe(), condition.getAgeGoe())
                        )
        );
    }

    public Page<Member> applyPagination2(MemberSearchCondition condition, Pageable pageable) {
        return applyPagination(pageable,
                contentQuery -> contentQuery.selectFrom(member)
                        .leftJoin(member.team, team)
                        .where(usernameEq(condition.getUsername()),
                                teamNameEq(condition.getTeamName()),
                                ageBetween(condition.getAgeLoe(), condition.getAgeGoe())
                        )
                ,
                countQuery -> countQuery.select(member.id)
                        .from(member)
                        .leftJoin(member.team, team)
                        .where(usernameEq(condition.getUsername()),
                                teamNameEq(condition.getTeamName()),
                                ageBetween(condition.getAgeLoe(), condition.getAgeGoe())
                        )
        );
    }

    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;
    }
}

 

① applyPagination() : 기존 방식과 달리 체인이 끊기지 않고 람다식으로 전달

  • return applyPagination< 이 부분이 직접 만든 클래스 사용하는 부분이다.

② applyPagination2() : contentQuery와 CountQuery를 분리함