Programming/Spring

Spring - 페이징, Querydsl RE

잇(IT) 2023. 11. 14. 11:10
- Page
- Pageable
- PageRequest
- 페이징 구성 예시
- Querydsl
   - JpaQueryFactory
- Querydsl 추가 및 예시
- 페이징, Querydsl 적용

 

- Page

 

- Page는 페이징된 데이터와 페이징 정보를 포함하는 객체이다. 쿼리 결과의 일부 데이터를 나타내며, 페이지 정보, 총 항목 수, 페이지 번호, 데이터 목록 등을 포함한다.

getContent() 현재 페이지의 데이터 목록을 반환한다.
getTotalPages() 총 페이지 수를 반환한다.
getTotalElements() 전체 항목 수를 반환한다.
getNumber() 현재 페이지 번호를 반환한다.
getSize() 페이지 크기 (한 페이지당 항목 수)를 반환한다.

- Pageable

 

- Pagealbe은 페이징 및 정렬 정보를 포함하는 인터페이스이다. 페이징 처리 및 정렬 기준을 설정하는 데 사용된다.

getPageNumber() 현재 페이지 번호를 반환한다.
getPageSize() 페이지 크기 (한 페이지당 항목 수)를 반환한다.
getSort() 정렬 정보를 반환한다.

- PageRequest

 

- PageRequest는 Pageable 인터페이스를 구현한 구체적인 클래스로, 페이징과 정렬을 설정하는 데 사용된다. PageRequest 객체를 생성하여 원하는 페이지 번호, 페이지 크기 및 정렬을 지정할 수 있다.

PageRequest.of(int page, int size) 페이지 번호와 페이지 크기만 지정하는 생성자
PageRequest.of(int page, int size, Sort sort) 페이지 번호, 페이지 크기 및 정렬 정보를 지정하는 생성자

- 페이징 구성 예시

1. JPA Repository 인터페이스 생성
public interface UserRepository extends JpaRepository<User, Long> {
    Page<User> findAll(Pageable pageable);
}

 

1. JPA를 통해 생성한 Repository에서 Page 객체를 반환하는 메서드를 작성한다.

 

2. Pageable 객체 생성
Pageable pageable = PageRequest.of(pageNumber, pageSize, Sort.by("lastName").ascending());

1. pageNumber : 조회할 페이지 번호(0부터 시작)

2. pageSize : 한 페이지에 표시할 항목 수

3. Sort : 정렬 옵션을 설정

 

3. Repository 메서드 호출
Page<User> users = userRepository.findAll(pageable);

1. 위에서 생성한 pageable 객체를 파라미터 정보로 넘겨서 JpaRepository에서 생성한 메서드를 호출하게 된다.

 

- Page 객체의 users의 첫번째 page 즉, page=1 에는 아래와 같은 데이터들이 저장되어 있다.

{
    "content": [
        {
            "id": 6,
            "username": "user5",
            "teamName": null
        },
        {
            "id": 7,
            "username": "user6",
            "teamName": null
        },
        {
            "id": 8,
            "username": "user7",
            "teamName": null
        },
        {
            "id": 9,
            "username": "user8",
            "teamName": null
        },
        {
            "id": 10,
            "username": "user9",
            "teamName": null
        }
        ...
         "pageable": {
        "sort": {
            "empty": true,
            "sorted": false,
            "unsorted": true
        },
        "offset": 5,
        "pageNumber": 1,
        "pageSize": 5,
        "paged": true,
        "unpaged": false
    },
    "last": false,
    "totalElements": 100,
    "totalPages": 20,
    "size": 5,
    "number": 1,
    "sort": {
        "empty": true,
        "sorted": false,
        "unsorted": true
    },
    "first": false,
    "numberOfElements": 5,
    "empty": false
}

 

long totalElements = users.getTotalElements(); // 전체 항목 수
int totalPages = users.getTotalPages(); // 전체 페이지 수
List<User> content = users.getContent(); // 현재 페이지의 데이터

- 위와 같이 해당 Page 객체에 대해 원하는 정보를 얻어낼 수 있다.


- Querydsl

 

- Querydsl은 자바 언어로 SQL과 비슷한 DSL을 사용하여 데이터베이스 쿼리를 생성하는 데 사용되는 라이브러리이다. 아래와 같은 장점을 가진다.

 

1. 코드 기반 쿼리 작성

2. 타입 안전성

3. JPA 및 기타 ORM 프레임워크 통합

4. 동적 쿼리 생성

5. 집계 및 정렬

6. 쉬운 조인

7. 문자열 쿼리에 보다 나은 안전

 

- JpaQueryFactory

 

- Querydsl을 JPA와 함께 사용하려면 JPAQueryFactory를 사용하는 것이 일반적이다.

- JPAQueryFactory를 생성하려면 주로 Spring Data JPA 프로젝트에서 제공하는 JpaRepository를 사용하거나 직접 EntityManager를 이용하여 생성해야 한다.

package com.isbill.config;

import com.querydsl.jpa.impl.JPAQueryFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;

@Configuration
public class QuerydslConfig {

    @PersistenceContext
    public EntityManager em;

    @Bean
    public JPAQueryFactory jpaQueryFactory() {
        return new JPAQueryFactory(em);
    }
}

- 위와 같이 EntityManager를 이용하여 JPAQueryFactory를 생성해준다.

- @Bean에 등록하면 다른 클래스에서 편하게 해당 객체를 사용할 수 있다.


 

- Querydsl 추가

 

- build.gradle
// --------------- Querydsl 추가
    implementation 'com.querydsl:querydsl-core'
    implementation 'com.querydsl:querydsl-jpa'

    annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jpa"
    annotationProcessor 'jakarta.persistence:jakarta.persistence-api'
    annotationProcessor 'jakarta.annotation:jakarta.annotation-api'

- Querydsl을 사용하기 위해 위와 같이 의존성을 추가해준다.

- classes를 통해 컴파일한다. 컴파일된 클래스 파일은 build/classes 디렉터리에 저장된다.

- 위와 같이 Querydsl에 의해 Q도메인들이 생성된다.

 

- oooRepositoryCustomImpl.java
@RequiredArgsConstructor
public class BillRepositoryCustomImpl implements BillRepositoryCustom{

    private final JPAQueryFactory jpaQueryFactory;

    private BooleanExpression billName(String searchQuery) {
        return StringUtils.isEmpty(searchQuery) ? null :
                QBill.bill.name.like("%" + searchQuery + "%");
    }

    @Override
    public List<Bill> getBillList(BillSearchDto billSearchDto) {
        QueryResults<Bill> billQueryResults = jpaQueryFactory
                .selectFrom(QBill.bill)
                .where(billName(billSearchDto.getSearchQuery()))
                .fetchResults();

        List<Bill> results = billQueryResults.getResults();

        return results;
    }
}

1. 클래스명을 Impl을 붙이는 관례상 사용하는 것이다.

2. @Bean으로 등록한 JPAQueryFactory를 받아오고, BooleanExpression를 사용하여 쿼리에 사용될 조건을 작성해준다.

 2.1. BooleanExpression은 (1) 동적 검색 조건, (2) 복잡한 쿼리, (3) 코드 재사용을 위해 사용된다.

3. getBillList 메서드를 확인해보면 jpaQueryFactory를 builder 패턴에 의해 코드(쿼리)를 작성하게 된다.

4. Q도메인을 이용해서 엔티티를 불러오고, sql 쿼리와 동일하게 .을 이용하여 코드(쿼리)를 작성해주면 된다.

5. where문의 조건을 위해서 BooleanExpression을 통해 생성한 조건을 가져다가 사용할 수 있다.

6. 단건일 경우 fetch를, 다건일 경우 fetchResults를 이용하여 결과를 받아온다.


- 페이징, Querydsl 적용

 

- JpaRepository
public interface RegistreRepository extends JpaRepository<Registre, Long>,
        QuerydslPredicateExecutor<Registre>, RegistreRepositoryCustom {

    Registre findByMemberId(Long id);
}

1. JpaRepository를 이용한 인터페이스에 Querydsl 코드를 작성한 Custom 인터페이스를 상속 받는다 (Custom 인터페이스는 Impl을 통해 구현되어 있다)

 

- RepositoryCustomImpl
@Override
    public Page<Registre> getRegistreList(RegistreSearchDto registreSearchDto, Pageable pageable) {

        QueryResults<Registre> registreQueryResults = jpaQueryFactory
                .selectFrom(QRegistre.registre)
                .where(registreName(registreSearchDto.getSearchQuery()))
                .offset(pageable.getOffset())
                .limit(pageable.getPageSize())
                .fetchResults();

        List<Registre> content = registreQueryResults.getResults();
        long total = registreQueryResults.getTotal();

        return new PageImpl<>(content, pageable, total);
    }
}

1. Querydsl이 적용된 CustomImpl 클래스에 getRegistreList 메서드에 Querydsl를 통해 동적 쿼리를 통해 DB에서 해당 조건에 맞는 데이터를 가져오고, pageable 정보를 파라미터로 전달함으로서 Page 객체로 반환하도록 하고 있다.

2. 위 코드는 Querydsl과 페이징을 합쳐 동적쿼리를 통해 얻은 결과를 페이징 하는 코드이다.

 

- Service
public class RegistreService {

    private final RegistreRepository registreRepository;

    @Transactional(readOnly = true)
    public Page<Registre> getMainPage(RegistreSearchDto registreSearchDto, Pageable pageable) {
        return registreRepository.getRegistreList(registreSearchDto, pageable);
    }
}

1. Service 클래스에 Repository에서 Querydsl과 페이징을 이용한 메서드를 호출한다.

 

- Controller
   @GetMapping("/")
    public String main(RegistreSearchDto registreSearchDto, Optional<Integer> page, Model model) {
        Pageable pageable = PageRequest.of(page.isPresent() ? page.get() : 0, 5);
        List<Registre> registres = registreRepository.findAll();
        Page<Registre> mainPage = registreService.getMainPage(registreSearchDto, pageable);
        model.addAttribute("registreSearchDto", registreSearchDto);
        model.addAttribute("registres", mainPage);
        model.addAttribute("maxPage", 5);
        log.info("/ GetMapping 정상 동작------------------------------------------------------");
        return "main";
    }
}

1. Controller에서 조건이 담겨있는 DTO 파라미터를 전달 받고, 해당 파라미터와 pageable 객체를 생성하여 dto, pageable 객체를 파라미터로 넘겨준다.

2. 파라미터를 넘겨 받은 getMainPage 메서드는 위에서 생성한 Repository 메서드에서 해당 조건을 검색한 데이터들을 pagealbe 정보에 맞게 페이징 한 결과를 반환해준다.

728x90