Querydsl

이동언·2024년 10월 10일

new world

목록 보기
53/62
post-thumbnail

10.10(목)

1. JPA 설계의 원칙

  1. ERD를 참고하여 구성하자
  2. 관계의 주어는 FK
  3. 단방향참조 / 양방향참조가 있는데, 무조건 단방향참조를 생각하자.
  4. ToString 주의

EX) 상품에 대한 테이블이 존재할때, 상품의 이미지와 상품의 리뷰에 대한 데이터가 있다면 대부분 이미지와 리뷰는 비슷한 개념으로 테이블을 따로 빼버리는데 이는 잘못된 설계이다.

왜냐하면 이미지는 게시글을 작성하는 시점에 등록이 되는것이지만, 리뷰는 각각의 리뷰어들이 다른 시점에 등록이 되는것이므로 리뷰는 별도의 테이블(domain)으로 엔티티 설계를 해줘야한다.

1-1. Review

package org.zerock.api2.product.domain;

import jakarta.persistence.*;
import lombok.ToString;

@Entity
@ToString(exclude = "product")
@Table(name = "tbl_review")
public class Review {


    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long rno;

    private String reviewer;

    private int score;

    @ManyToOne(fetch = FetchType.LAZY)
    private Product product;
}

👉 이렇게 product와 별개로 Review Domain을 만들어줬을때의 중요한점이 있는데, 게시글 - 댓글관계이다. 하나의 게시글에 여러개의 댓글이 달리므로 일대다 관계라고 생각하지만, 위에 언급했던 2번째를 보면 관계의 주어는 FK라고 적혀있다. 이는 현재 Review테이블 이므로 Review테이블의 입장으로 봤을때 Product테이블은 다대일 관계이므로 @ManyToOne이 되어야한다.

👉 또한, 4번째 조건으로 ToString을 주의하여야하는것인데, 만약 review 내부에 product가 있을때, review를 가지고올때 product도 가져오고 review를 가져올때 또 prodcut를 가져와야하는 무한루프가 발생되므로 toString을 사용할때는 @ToString(exclude = "product")을 작성하여 product를 제외하고 ToString을 실행해야한다.




2. QueryDsl을 이용한 Join방식

2-1. Review - domain

package org.zerock.api2.product.domain;

import jakarta.persistence.*;
import lombok.ToString;

@Entity
@ToString(exclude = "product")
@Table(name = "tbl_review")
public class Review {


    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long rno;

    private String reviewer;

    private int score;

    @ManyToOne(fetch = FetchType.LAZY)
    private Product product;
}




2-2. ReviewRepository

package org.zerock.api2.product.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import org.zerock.api2.product.domain.Review;

public interface ReviewRepository extends JpaRepository<Review, Long> {
}




2-3. ProductSearch

package org.zerock.api2.product.repository.search;

import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.zerock.api2.product.domain.Product;
import org.zerock.api2.product.domain.ProductStatus;

public interface ProductSearch {

    Page<Product> list1(Pageable pageable);

    Page<Product> list2(String keyword, ProductStatus status, Pageable pageable);

    Page<Product> listWithReplyCount(Pageable pageable);
}




2-4. ProductSearchImpl

package org.zerock.api2.product.repository.search;

import com.querydsl.core.Tuple;
import com.querydsl.jpa.JPQLQuery;
import lombok.extern.log4j.Log4j2;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.support.QuerydslRepositorySupport;
import org.zerock.api2.product.domain.Product;
import org.zerock.api2.product.domain.ProductStatus;
import org.zerock.api2.product.domain.QProduct;
import org.zerock.api2.product.domain.QReview;

import java.util.List;

@Log4j2
public class ProductSearchImpl extends QuerydslRepositorySupport implements ProductSearch {

    public ProductSearchImpl() {
        super(Product.class);
    }

    @Override
    public Page<Product> list1(Pageable pageable) {

        QProduct product = QProduct.product; // product엔티티에 대한 메타데이터제공
        JPQLQuery<Product> query = from(product);
        query.where(product.status.eq(ProductStatus.SALE)); // 상품의 상태가 sale인지 확인
        query.where(product.pno.gt(0L)); // pno가 0보다 큰 값

        this.getQuerydsl().applyPagination(pageable, query); //qetQuerydsl에서 제공하는 applyPagination메서드는 pageable을 통해 limit설정, query를 통해 where절

        List<Product> list = query.fetch(); // product객체가 db에서 조회되어 List<Product>형태로 반환
        long total = query.fetchCount();

        return new PageImpl<>(list, pageable, total);

    }

    @Override
    public Page<Product> list2(String keyword, ProductStatus status, Pageable pageable) {
        QProduct product = QProduct.product;
        JPQLQuery<Product> query = from(product);
        query.where(product.status.eq(ProductStatus.SALE)); // 상품의 상태가 sale인지 확인
        query.where(product.pno.gt(0L)); // pno가 0보다 큰 값

        if(keyword != null) {
            query.where(product.pno.like("%" + keyword + "%"));
        }
        if(status != null) {
            query.where(product.status.eq(status));
        }

        this.getQuerydsl().applyPagination(pageable, query);

        List<Product> list = query.fetch();
        long total = query.fetchCount();

        return new PageImpl<>(list, pageable, total);

    }

    @Override
    public Page<Product> listWithReplyCount(Pageable pageable) {

        QProduct product = QProduct.product; // querydsl을 통해 자동으로 생성
        QReview review = QReview.review; // querydsl을 통해 자동으로 생성

        JPQLQuery<Product> query = from(product); 
        // 쿼리문의 주체가 product이고 product의 값을 가져오는것 이므로 JPQL쿼리문을 사용

        query.leftJoin(review).on(review.product.eq(product)); 
        // product기준으로 leftJoin - 즉 특정제품에 대한 리뷰
        query.groupBy(product);

        this.getQuerydsl().applyPagination(pageable, query);

        JPQLQuery<Tuple> tupleJPQLQuery = query.select
                (product.pno, product.pname, product.price, review.count(), review.score.avg());
                // 반환값이 tuple

        log.info("============================");
        log.info(tupleJPQLQuery);

        List<Tuple> tupleList = tupleJPQLQuery.fetch();

        Long total = tupleJPQLQuery.fetchCount();


        return null;
    }
}




3. Impl의 query / Repository의 query

👉 Impl의 query는 querydsl를 이용하여 복잡한 로직의 쿼리문을 작성할때 사용되고 페이징처리와 같이 page데이터타입의 형태로 반환할수 있다.

👉 Repository에서 바로 @Query를 이용하여 작성되는 쿼리문은 간단한 CRUD작성 및 조회를 할때 사용되고, Optional 및 List형태로 반환된다.




4. 데이터 이동단계

4-1. productController

package org.zerock.api2.controller;


import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.zerock.api2.common.dto.PageRequestDTO;
import org.zerock.api2.common.dto.PageResponseDTO;
import org.zerock.api2.product.dto.ProductListDTO;
import org.zerock.api2.service.ProductService;

@RestController
@RequestMapping("/api/product")
@RequiredArgsConstructor
@Log4j2
public class ProductController {

    private final ProductService productService;

    @GetMapping("list")
    public PageResponseDTO<ProductListDTO> list(PageRequestDTO pageRequestDTO){
        return productService.getList(pageRequestDTO);
    }

}

👉 컨트롤러에서 service의 getList메서드를 호출하며 매개변수에 requestDTO를 넣는다.

4-2. ProductService

package org.zerock.api2.service;

import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.zerock.api2.common.dto.PageRequestDTO;
import org.zerock.api2.common.dto.PageResponseDTO;
import org.zerock.api2.product.dto.ProductListDTO;
import org.zerock.api2.product.repository.ProductRepository;

@Service
@Transactional
@RequiredArgsConstructor
@Log4j2
public class ProductService {

    private final ProductRepository productRepository;

    public PageResponseDTO<ProductListDTO> getList(PageRequestDTO pageRequestDTO){
        return productRepository.list(pageRequestDTO);
    }
}

👉 service에서는 controller에서 받은 requestDTO를 넣고, repository의 list메서드를 호출한다.

4-3. ProductRepository

package org.zerock.api2.product.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.zerock.api2.product.domain.Product;
import org.zerock.api2.product.dto.ProductReadDTO;
import org.zerock.api2.product.repository.search.ProductSearch;

import java.util.Optional;

public interface ProductRepository extends JpaRepository<Product, Long>, ProductSearch {

    @Query("select " +
                "new org.zerock.api2.product.dto.ProductReadDTO(p.pno, p.pname, p.price) " +
                "from Product p where p.pno = :pno")
    Optional<ProductReadDTO> read(@Param("pno") Long pno);

}

👉 repository에는 productSearch가 extends되어 있는데 해당 search의 list를 호출

4-4. productSearch

package org.zerock.api2.product.repository.search;

import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.zerock.api2.common.dto.PageRequestDTO;
import org.zerock.api2.common.dto.PageResponseDTO;
import org.zerock.api2.product.domain.Product;
import org.zerock.api2.product.domain.ProductStatus;
import org.zerock.api2.product.dto.ProductListDTO;

public interface ProductSearch {

    Page<Product> list1(Pageable pageable);

    Page<Product> list2(String keyword, ProductStatus status, Pageable pageable);

    Page<Product> listWithReplyCount(Pageable pageable);

    PageResponseDTO<ProductListDTO> list(PageRequestDTO pageRequestDTO);
}

👉 list를 호출하면

4-5. ProductSearchImpl

@Override
    public PageResponseDTO<ProductListDTO> list(PageRequestDTO pageRequestDTO) {

        Pageable pageable = PageRequest.of(
                pageRequestDTO.getPage()-1,
                pageRequestDTO.getSize(),
                Sort.by("pno").descending());

        QProduct product = QProduct.product;
        QReview review = QReview.review;

        JPQLQuery<Product> query = from(product);
        query.leftJoin(review).on(review.product.eq(product));
        query.groupBy(product);

        this.getQuerydsl().applyPagination(pageable, query);

        JPQLQuery<ProductListDTO> dtoJPQLQuery = query.select(
                Projections.bean(ProductListDTO.class,
                        product.pno,
                        product.pname,
                        product.price,
                        review.count().as("reviewCnt"),
                        review.score.avg().coalesce(0.0).as("avgScore")
                )
        );

        java.util.List<ProductListDTO> dtoList = dtoJPQLQuery.fetch();

        long total = dtoJPQLQuery.fetchCount();


        return  PageResponseDTO.<ProductListDTO>withAll()
                .dtoList(dtoList)
                .pageRequestDTO(pageRequestDTO)
                .totalCount(total)
                .build();
    }
}

👉 해당 메서드가 호출되고, requestDTO값을 이용하여 pageable객체를 만들고, JPQLQuery를 사용하여 ProdutClistDTO의 형태로 엔티티값을 변경하여 LIST를 구성하고, JPQLQuery의 total을 이용하여 total값을 만들어 responseDTO의 매개변수값을 다 만들어 객체를 만들어 return한다.




6. ProductControllerAdvice

package org.zerock.api2.controller.advice;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import java.util.HashMap;
import java.util.Map;

@RestControllerAdvice
public class ProductControllerAdvice {
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<Map<String, String>> methodArgEx(MethodArgumentNotValidException EX) {
        Map<String, String> map = new HashMap<>();
        map.put("message", "Parameter Type check plz...");
        return ResponseEntity.status(500).body(map);
    }
}

👉 예외처리가 필요한 코드로 500에러가 발생하는 파라미터가 없을때 에러페이지가 아닌 customErrorPage를 만들어준다.

0개의 댓글