인프런 커뮤니티 질문&답변

유요한님의 프로필 이미지
유요한

작성한 질문수

실전! Querydsl

최종 질문) 페이징 & 정렬

작성

·

448

0

제가 제대로 이해하고 있는지 제대로 잡고 넘어가려고 합니다.

 

질문1

먼저 단순한 제목 검색과 페이지처리와 정렬을 구현했을 때

fetchCount 대신에 fetchOne을 사용하기 위해서 별도로 count 쿼리를 생성해서 페이지 처리를 해줍니다.

페이지와 정렬을 처리할 때 동적 정렬을 처리하기 위해서 OrderSpecifier을 사용해서 동적 정렬 쿼리를 하기 위해서 다음과 같이 설정

http://localhost:9090/v2/members?page=1&sort=memberId,desc

 

Pageable로 페이지랑 정렬을 받아서 동적으로 뽑아와서 메소드를 만들고 orderBy에 넣었습니다. 이런식으로 처리하는게 맞나요?

질문2

@Repository
@Log4j2
public abstract class Querydsl4RepositorySupport {
    // 이 클래스가 다루는 도메인(엔터티)의 클래스
    private final Class domainClass;
    // 도메인 엔터티에 대한 Querydsl 쿼리를 생성하고 실행
    private Querydsl querydsl;
    // 데이터베이스와의 상호 작용을 담당하는 JPA의 핵심 객체
    private EntityManager entityManager;
    // queryFactory를 통해 Querydsl 쿼리를 생성하고 실행합니다.
    private JPAQueryFactory queryFactory;

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

    // Pageable안에 있는 Sort를 사용할 수 있도록 설정한 부분
    @Autowired
    public void setEntityManager(EntityManager entityManager) {
        Assert.notNull(entityManager, "EntityManager must not be null!");
        // JpaEntityInformation을 얻기 위해 JpaEntityInformationSupport를 사용합니다.
        // 이 정보는 JPA 엔터티에 대한 메타데이터 및 정보를 제공합니다.
        JpaEntityInformation entityInformation =
                JpaEntityInformationSupport.getEntityInformation(domainClass, entityManager);
        // 이는 Querydsl에서 엔터티의 경로를 생성하는 데 사용됩니다.
        SimpleEntityPathResolver resolver = SimpleEntityPathResolver.INSTANCE;
        // entityInformation을 기반으로 엔티티의 경로를 생성합니다.
        EntityPath path = resolver.createPath(entityInformation.getJavaType());
        this.entityManager = entityManager;
        // querydsl 객체를 생성합니다.
        // 이 객체는 Querydsl의 핵심 기능을 사용할 수 있도록 도와줍니다.
        // 엔터티의 메타모델 정보를 이용하여 Querydsl의 PathBuilder를 생성하고, 이를 이용하여 Querydsl 객체를 초기화합니다.
        this.querydsl = new Querydsl(entityManager, new
                PathBuilder<>(path.getType(), path.getMetadata()));
        this.queryFactory = new JPAQueryFactory(entityManager);
    }

    // 해당 클래스의 빈(Bean)이 초기화될 때 자동으로 실행되는 메서드
    @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!");
    }
    // 이 팩토리는 JPA 쿼리를 생성하는 데 사용됩니다.
    protected JPAQueryFactory getQueryFactory() {
        return queryFactory;
    }
    // 이 객체는 Querydsl의 핵심 기능을 사용하는 데 도움이 됩니다.
    protected Querydsl getQuerydsl() {
        return querydsl;
    }
    // EntityManager는 JPA 엔터티를 관리하고 JPA 쿼리를 실행하는 데 사용됩니다.
    protected EntityManager getEntityManager() {
        return entityManager;
    }
    // Querydsl을 사용하여 쿼리의 SELECT 절을 생성하는 메서드입니다.
    // expr은 선택할 엔터티나 엔터티의 속성에 대한 표현식입니다.
    protected <T> JPAQuery<T> select(Expression<T> expr) {
        return getQueryFactory().select(expr);
    }
    // Querydsl을 사용하여 쿼리의 FROM 절을 생성하는 메서드입니다.
    // from은 엔터티에 대한 경로 표현식입니다.
    protected <T> JPAQuery<T> selectFrom(EntityPath<T> from) {
        return getQueryFactory().selectFrom(from);
    }

    // 이 메서드는 주어진 contentQuery를 사용하여 Querydsl을 통해 JPA 쿼리를 생성하고 실행하고,
    // 그 결과를 Spring Data의 Page 객체로 변환하는 기능을 제공
    protected <T> Page<T> applyPagination(Pageable pageable,
                                          Function<JPAQueryFactory, JPAQuery> contentQuery) {
        // 1. contentQuery를 사용하여 JPAQuery 객체를 생성
        JPAQuery jpaQuery = contentQuery.apply(getQueryFactory());
        // 2. Querydsl을 사용하여 페이징 및 정렬된 결과를 가져옴
        List<T> content = getQuerydsl().applyPagination(pageable,
                jpaQuery).fetch();
        // 3. contentQuery를 다시 사용하여 countQuery를 생성
        JPAQuery<Long> countQuery = contentQuery.apply(getQueryFactory());
        // 4. countQuery를 실행하고 총 레코드 수를 얻음
        long total = countQuery.fetchOne();
        // 5. content와 pageable 정보를 사용하여 Spring Data의 Page 객체를 생성하고 반환
        return PageableExecutionUtils.getPage(content, pageable,
                () -> total);
    }
    // 이 메서드는 contentQuery와 함께 countQuery를 인자로 받아서 사용합니다.
    // contentQuery를 사용하여 페이징된 결과를 가져오고, countQuery를 사용하여 전체 레코드 수를 얻습니다.
    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<Long> countResult = countQuery.apply(getQueryFactory());
        log.info("countResult : " + countResult );

        Long total = countResult.fetchOne();
        return PageableExecutionUtils.getPage(content, pageable,
                () -> total);
    }
}
  • fetchCount() → fetchOne()으로 변경

  // count처리 까지 한것
    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()),
                                ageGoe(condition.getAgeGoe()),
                                ageLoe(condition.getAgeLoe())
                        ), countQuery -> countQuery
                .select(member.count())
                .from(member)
                .where(userNameEq(condition.getUserName()),
                        teamNameEq(condition.getTeamName()),
                        ageGoe(condition.getAgeGoe()),
                        ageLoe(condition.getAgeLoe()))
        );
    }

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

 

@Service
@RequiredArgsConstructor
public class MemberService {
    private final MemberTestRepository memberTestRepository;

    public Page<MemberTeamDTO> search(MemberSearchCondition condition, Pageable pageable) {

        Sort sort = pageable.getSort();
        PageRequest pageRequest = PageRequest.of(
                (int) pageable.getOffset(),
                pageable.getPageSize(),
                sort
        );

        Page<Member> resultPage = memberTestRepository.applyPagination2(condition, pageRequest);
        return resultPage.map(member -> MemberTeamDTO.builder()
                .memberId(member.getId())
                .age(member.getAge())
                .userName(member.getUserName())
                .teamId(member.getTeam().getId())
                .teamName(member.getTeam().getName())
                .build());
    }
}

Sort를 처리할 때 orderBy할 필요 없이 PageRequest.of로 보내주면 된다고 글을 봐서

동적으로 처리할 수 있도록

   Sort sort = pageable.getSort();
        PageRequest pageRequest = PageRequest.of(
                (int) pageable.getOffset(),
                pageable.getPageSize(),
                sort
        );

이렇게 처리했습니다. Post맨으로 돌려본 결과 제개 원하는 대로 페이징, 정렬이 되었는데 이런식으로 하는게 맞나 확인하고 싶어서 질문드립니다.

 

질문3

질문1 OrderSpecifier로 동적인 쿼리 처리하고 있는데 Querydsl4RepositorySupport이거를 사용한 이유가 유지보수와 가독성을 더 높여주는 방법이라 이해했는데 맞나요?

답변 2

2

안녕하세요. 유요한님, 공식 서포터즈 David입니다.

  1. 이 부분은 구현하기 나름입니다. 지금 작성하신대로 구현해도 무방하나 여러 컬럼의 정렬을 지원할 순 없을 것 같습니다.

  2. 본 강의 리포지토리 지원 - QuerydslRepositorySupport 편 6분 15초부터 보시면 applyPagination에 pageable을 그냥 넘길 수 있습니다. 이를 활용해보시면 좋을 것 같습니다.

  3. 네, 맞습니다.

감사합니다🙂

0

안녕하세요, 인프런 AI 인턴입니다.

Sort 관련하여 Querydsl을 활용한 페이징 및 정렬 처리 방법에 대한 질문으로 보입니다. 관련하여 강사님께서 제공하신 답변 중 일부가 여러분의 질문에 도움이 될 것 같습니다.

  1. 페이지네이션과 정렬을 적용하는 방법에 대한 답변은 아래 링크를 참고해 주세요.

강사님의 답변을 요약하자면, PageRequest 객체를 생성할 때 Sort.by("age")와 같이 정렬 조건을 포함해서 던지면 해당 조건이 함께 적용된다고 합니다. 별도로 orderBy에 적용하지 않아도 된다는 내용이 있습니다.

  1. 정렬 관련 예외처리에 대한 방법은 아래 링크를 참고해 주세요.

이 답변에서 강사님은 PageableSort 속성을 검사하는 로직에 대해 언급하고 있으며, 이를 통해 잘못된 정렬 프로퍼티를 요청했을 때 예외를 처리하는 것에 대해 설명하고 있습니다.

  1. Querydsl에서 Spring의 Sort를 변환하여 적용하는 방법에 대한 세부사항은 다음 링크에서 확인하실 수 있습니다.

여기서는 OrderSpecifier를 사용하여 동적 정렬을 적용하는 방법에 대해 논의되고 있습니다. 강사님께서는 직접 OrderSpecifier를 구성하여 적용하는 방법을 제안하고 있습니다.

강의 내용을 확인하시고 각 링크를 참고하시어 정렬 처리 로직을 개선하는데 도움이 되길 바랍니다.

유요한님의 프로필 이미지
유요한

작성한 질문수

질문하기