JPA Join으로 두개 테이블의 데이터 조회하기
DB에서는 여러 테이블의 정보를 가져오기위해 외래키(Foreign Keys) 설정으로 테이블을 Join 하여 가져왔었는데요.
클래스를 테이블로 사용하는 JPA에서는 어떻게 외래키와 조인 설정이 가능할까요
Entity Class 외래키 설정
@Entity
@Table(name = "reviews")
public class Review {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
public Long id;
// productId 와 userId 는 외래키
// public Long productId; 가 아니라 아래와 같이 객체로 선언해야함.
// 클래스 자체로 포린키를 설정함
@ManyToOne
//리뷰테이블에서 product_id는 여러번 반복될 수 있음 but product 테이블에서는 product_id는 unique 함
// = 하나의 상품에 여러개의 리뷰가 달릴 수 있음. @ManyToOne
@JoinColumn(name = "product_id")
public Product product;
@ManyToOne
@JoinColumn(name = "user_id")
public User user;
@JoinColumn
어노테이션으로 외래키 설정이 가능합니다.
product_id 대신 product 객체 자체로 조인합니다.
@Data
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Table(name = "places")
public class Place {
// 아이디, 코스아이디, 이름, 주소, 방문순서(visit_order), 설명, 비용, 등록일
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
public Long id;
@ManyToOne //코스 하나에 장소는 여러개 등록가능 다대일관계
@JoinColumn(name = "course_id")
public Course course;
@Column(length = 100)
public String name;
@Column(length = 255)
public String address;
@Column
public Integer visitOrder;
@Column(length = 2000)
public String description;
@Column
public Integer cost;
@Column
public Instant createdAt;
@OneToMany(mappedBy = "place")
// place 컬럼에서 여러개의 사진이 올라갈 수 있음
// photo 클래스의 멤버변수 place 를 가리킴
public List<Photo> photoList = new ArrayList<>();
@PrePersist
public void prePersist() {
createdAt = Instant.now();
}
@ManyToOne
여행 정보 페이지를 생각해봅시다.
경주여행 코스에는 첨성대, 불국사, 동궁과월지같은 유적지 정보도 있고, 박물관 정보, 맛집 정보도 있을거에요.
이렇게 하나의 코스에 여러개의 여행지 정보가 있습니다.
이럴때 사용하는 어노테이션이 @ManyToOne 입니다.
관광명소 아이디는 여행코스 아이디 하나에 여러개 들어갈 수 있다!
@ManyToOne //코스 하나에 장소는 여러개 등록가능 다대일관계
@JoinColumn(name = "course_id")
public Course course;
@OneToMany
여행코스 입장에서는 하나의 코스에 여러개<리스트>의 관광명소들이 들어갈 수 있습니다.
ArrayList로 변수 선언을 하면
이후에 코스 정보를 불러올 때 편하게 관광명소 리스트를 불러 올 수 있습니다.
//placeEntity 의 course 멤버변수
@OneToMany(mappedBy = "course")
public List<Place> placeList = new ArrayList<>();
// Course 하나에 여러개의 place 가 올라갈 수 있음
상품 목록조회 API 만들기
ControllerClass
/// 상품 전체목록 조회
@GetMapping("/api/products")
public ResponseEntity<ProductListResponse> getAllProducts(@RequestParam int size, @RequestParam int page, @RequestParam(required = false) String category) {
ProductListResponse productListResponse = productService.getAllProducts(size, page, category);
return ResponseEntity.status(200).body(productListResponse);
}
카테고리는 선택사항이므로
@RequestParam(required = false) String category 로 처리합니다.
ServiceClass
카테고리가 있을 경우와 없을 경우를 if문으로 나누어 작성합니다.
카테고리가 없을경우
/// 상품 목록 조회
public ProductListResponse getAllProducts(int size, int page, String category) {
/// category 가 null 이면 전체 상품을 조회한다.
if (category == null) {
productRepository.findAll();
PageRequest pageRequest = PageRequest.of(page - 1, size);
Page<Product> productPage = productRepository.findAll(pageRequest);
//page 를 리턴 <Product>가 담겨있는 페이지
//Select * from product limit 0,10
페이징 처리를 위해 PageRequest.of(page - 1, size) 메서드 사용하여 pageRequest 생성
productRepository.findAll(pageRequest) 로 해당 페이지에 대한 전체 상품 목록을 가져와서 Page<Product> 리스트로 저장
(SQL문으로 보자면 Select * from product limit 0,10;)
ArrayList<ProductResponse> productList = new ArrayList<>();
for (Product product : productPage) {
//ReviewRepository 필요 리뷰를 조회해야함 >> Join
//for 로 한 행씩 가져와서 리뷰를 붙여줄것임
//리뷰 평점 평균
//findAverageRatingByProductId(product.id) 메서드 필요 > ReviewRepository
double averageRating = reviewRepository.findAverageRatingByProductId(product.id);
//.query.QueryTypeMismatchException - 쿼리를 직접 작성해줄 필요 있음
//리뷰 개수도 메서드 만들어줌 > ReviewRepository
int reviewCount = reviewRepository.countByProductId(product.id);
// 가져온 Entity 를 ProductResponse DTO 에 담아서 리턴
ProductResponse productResponse = new ProductResponse();
productResponse.id = product.id;
productResponse.name = product.name;
productResponse.price = product.price;
productResponse.category = product.category;
productResponse.stockQuantity = product.stockQuantity;
productResponse.averageRating = averageRating;
productResponse.reviewCount = reviewCount;
// 리스트에 저장 for 위에 ArrayList<ProductResponse> productList = new ArrayList<>();
productList.add(productResponse);
For( : )문으로 productPage에서 한 행씩 상품을 불러옵니다.
상품아이디에 따른 필요 리뷰정보(평균별점, 리뷰개수)를 조회할 수 있도록 ReviewRepository 준비
상품아이디의 평균 별점 findAverageRatingByProductId(product.id)
상품아이디의 전체개수 countByProductId(product.id) 는
JPA 기본제공메서드가 아니기때문에 ReviewRepository에 별도로 메서드를 만듭니다.
@Repository
public interface ReviewRepository extends JpaRepository<Review, Long> {
/// findAverageRatingByProductId(product.id)
@Query("SELECT avg(rating) FROM Review WHERE product.id = :productId")
public double findAverageRatingByProductId(@Param("productId") long productId);
// 특정 아이디의 평균 평점을 구하는 쿼리를 작성
//? 대신 :productId 를 넣어줘야함
//sql 이 아닌 jpql 은 테이블명 > 클래스이름, 컬럼명 > 필드명으로 작성해야함
//클래스를 테이블로 만든거라 Review 클래스 이름으로, productId 도 product 클래스의 id로 만들어줬기 때문에 product.id로 써줘야함
//맨 끝 ; 는 생략해야함
/// countByProductId(product.id)
public int countByProductId(long productId);
//JPA 의 쿼리 메소드 기능 - 메소드 이름을 분석하여 자동으로 적절한 쿼리를 생성
메서드 이름을 규칙에 따라 만들면 JPA에서 이름을 분석하여 JPQL을 생성하고 실행 하기도 하지만,
QueryTypeMismatchException가 나올 경우 직접 JPQL쿼리를 작성해줘야 합니다.
JPA에서 이름을 분석하여 JPQL을 생성 해줄 수 있는 메서드 이름 규칙(Query keywords reference)
Repository query keywords :: Spring Data JPA
The following table lists the predicate keywords generally supported by the Spring Data repository query derivation mechanism. However, consult the store-specific documentation for the exact list of supported keywords, because some keywords listed here mig
docs.spring.io
countByProductId()는 JPA에서 자동으로 쿼리를 만들어줬지만,
findAverageRatingByProductId()는 QueryTypeMismatchException이 나와 직접 쿼리를 작성했습니다.
JPQL문은 SQL문과 유사하지만 약간의 차이가 있습니다.
JPQL문이란?
JPQL(Java Persistence Query Language)은 JPA에서 엔티티 객체를 대상으로 쿼리를 작성하는 객체 지향 쿼리 언어
데이터베이스의 테이블과 컬럼이 아닌 자바 클래스와 변수(객체)에 작업을 수행
문장 마칠때 ; 사용하지 않음
JPQL문으로 작성
SELECT avg(rating) FROM Review WHERE product.id = :productId
SQL문으로 작성
SELECT avg(rating) FROM reviews r WHERE product_id =1;
productList.add(productResponse);
이제 불러온 리뷰 평균별점, 리뷰개수까지 포함한 product 엔티티 객체를 ProductResponse DTO 에 담아
ArrayList<ProductResponse> productList = new ArrayList<>();
ArrayList<ProductResponse> 리스트(productList)에 추가합니다.
ProductResponse와 함께 페이지정보도 리스폰 되도록 작업
ProdcutListResponse DTO를 만듭니다.
public class ProductListResponse {
public List<ProductResponse> content;
public Integer page;
public Integer size;
public Integer totalPages;
public Long totalElements;
// totalElements 는 Long
productList와 페이지정보를 담은 productListResponse를 컨트롤러에 리턴합니다.
// productList 를 pageInfo 와 함께
ProductListResponse productListResponse = new ProductListResponse();
productListResponse.content = productList;
productListResponse.page = page;
productListResponse.size = size;
productListResponse.totalElements = productPage.getTotalElements();
productListResponse.totalPages = productPage.getTotalPages();
return productListResponse;
카테고리가 있을경우
} else {
/// category 가 존재하면 해당 카테고리의 상품을 조회한다.
PageRequest pageRequest = PageRequest.of(page - 1, size);
Page<Product> productPage = productRepository.findAllByCategory(category, pageRequest);
ArrayList<ProductResponse> productList = new ArrayList<>();
for (Product product : productPage) {
//ReviewRepository 필요 리뷰를 조회해야함 >> Join
//for 로 한 행씩 가져와서 리뷰를 붙여줄것임
//리뷰 평점 평균
//findAverageRatingByProductId(product.id) 메서드 필요 > ReviewRepository
double averageRating = reviewRepository.findAverageRatingByProductId(product.id);
//.query.QueryTypeMismatchException - 쿼리를 직접 작성해줄 필요 있음
//리뷰 개수도 메서드 만들어줌 > ReviewRepository
int reviewCount = reviewRepository.countByProductId(product.id);
// 가져온 Entity 를 ProductResponse DTO 에 담아서 리턴
ProductResponse productResponse = new ProductResponse();
productResponse.id = product.id;
productResponse.name = product.name;
productResponse.price = product.price;
productResponse.category = product.category;
productResponse.stockQuantity = product.stockQuantity;
productResponse.averageRating = averageRating;
productResponse.reviewCount = reviewCount;
// 리스트에 저장 for 위에 ArrayList<ProductResponse> productList = new ArrayList<>();
productList.add(productResponse);
}
// pageInfo
ProductListResponse productListResponse = new ProductListResponse();
productListResponse.content = productList;
productListResponse.page = page;
productListResponse.size = size;
productListResponse.totalElements = productPage.getTotalElements();
productListResponse.totalPages = productPage.getTotalPages();
return productListResponse;
}
카테고리가 없는 경우와 메커니즘은 같으나
ProductRepository에 카테고리로 조회하는 메서드가 필요합니다.
@Repository
public interface ProductRepository extends JpaRepository<Product, Long> {
/// findAllByCategory(category)
public Page<Product> findAllByCategory(String category,Pageable pageable);
}
Repository query keywords으로 자동으로 JPQL문을 만들어 처리되었습니다.
SELECT * FROM product WHERE category = ? LIMIT ? OFFSET ?;
SQL문으로 보자면 위와 같은 쿼리입니다.
카테고리가 없는경우, 있는경우를 모두 처리할 수 있도록 If문 수정
/// 상품목록조회
public ProductListResponse getProductList(int page, int size, String category) {
// 페이지 요청 생성
PageRequest pageRequest = PageRequest.of(page - 1, size);
Page<Product> productPage;
// 카테고리가 없는 경우
if (category == null || category.isEmpty()) {
productPage = productRepository.findAll(pageRequest);
} else {
// 카테고리가 있는 경우
productPage = productRepository.findByCategory(category, pageRequest);
}
// ProductResponse 리스트 생성
List<ProductResponse> productResponses = new ArrayList<>();
for (Product product : productPage) {
// Product Entity 를 ProductResponse DTO 에 담아서 리턴
ProductResponse productResponse = new ProductResponse();
productResponse.id = product.id;
productResponse.name = product.name;
productResponse.price = product.price;
productResponse.category = product.category;
productResponse.stockQuantity = product.stockQuantity;
//리뷰 개수와 평점 추가
productResponse.reviewCount = reviewRepository.countByProductId(product.id);
productResponse.averageRating = reviewRepository.findAverageRatingByProductId(product.id);
// 리스트에 추가
productResponses.add(productResponse);
}
//PageInfo 추가
ProductListResponse productListResponse = new ProductListResponse();
productListResponse.content=productResponses;
productListResponse.page=page;
productListResponse.size=size;
productListResponse.totalPages=productPage.getTotalPages();
productListResponse.totalElements=productPage.getTotalElements();
return productListResponse; // 생성된 리스트 반환
}
ControllerClass
@RestController
public class ProductController {
@Autowired
ProductService productService;
/// 상품 전체목록 조회
@GetMapping("/api/products")
public ResponseEntity<ProductListResponse> getAllProducts(@RequestParam int size, @RequestParam int page, @RequestParam(required = false) String category) {
ProductListResponse productListResponse = productService.getAllProducts(size, page, category);
return ResponseEntity.status(200).body(productListResponse);
}
리퀘스트 확인