From 3fc537614268555379871fe4e053b3175e85e037 Mon Sep 17 00:00:00 2001 From: Jihoon Oh Date: Mon, 17 Oct 2022 17:16:37 +0900 Subject: [PATCH] =?UTF-8?q?[BE]=20=EB=A6=AC=EB=B7=B0=20=EC=9E=91=EC=84=B1?= =?UTF-8?q?=20=EC=8B=9C=20=EC=A0=9C=ED=92=88=20=ED=86=B5=EA=B3=84=20?= =?UTF-8?q?=EC=A0=95=EB=B3=B4=EC=9D=98=20=EC=A0=95=ED=95=A9=EC=84=B1?= =?UTF-8?q?=EC=9D=84=20=EB=A7=9E=EC=B6=94=EB=8F=84=EB=A1=9D=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20(#784)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 제품 통계 정합성 맞추는 쿼리 작성 * refactor: 서비스에서 제품 통계 업데이트를 쿼리를 직접 사용하도록 수정 --- .../f12/application/review/ReviewService.java | 11 ++- .../f12/domain/product/ProductRepository.java | 26 ++++++ .../woowacourse/f12/domain/review/Review.java | 12 --- .../application/review/ReviewServiceTest.java | 32 +++---- .../domain/product/ProductRepositoryTest.java | 85 ++++++++++++++++++- .../domain/review/ReviewRepositoryTest.java | 1 - .../f12/domain/review/ReviewTest.java | 53 +----------- 7 files changed, 131 insertions(+), 89 deletions(-) diff --git a/backend/src/main/java/com/woowacourse/f12/application/review/ReviewService.java b/backend/src/main/java/com/woowacourse/f12/application/review/ReviewService.java index 46de62cf..ed3fbff7 100644 --- a/backend/src/main/java/com/woowacourse/f12/application/review/ReviewService.java +++ b/backend/src/main/java/com/woowacourse/f12/application/review/ReviewService.java @@ -52,6 +52,7 @@ public Long saveReviewAndInventoryProduct(final Long productId, final Long membe .orElseThrow(ProductNotFoundException::new); final Long reviewId = saveReview(reviewRequest, member, product); saveInventoryProduct(member, product); + productRepository.updateProductStatisticsForReviewInsert(product.getId(), reviewRequest.getRating()); return reviewId; } @@ -64,7 +65,6 @@ private void validateRegisterCompleted(final Member member) { private Long saveReview(final ReviewRequest reviewRequest, final Member member, final Product product) { validateNotWritten(member, product); final Review review = reviewRequest.toReview(product, member); - review.reflectToProductWhenWritten(); return reviewRepository.save(review) .getId(); } @@ -111,18 +111,23 @@ public ReviewWithAuthorAndProductPageResponse findPage(final Pageable pageable) public void update(final Long reviewId, final Long memberId, final ReviewRequest updateRequest) { final Review target = findTarget(reviewId, memberId); final Review updateReview = updateRequest.toReview(target.getProduct(), target.getMember()); + int ratingGap = updateReview.getRating() - target.getRating(); target.update(updateReview); + reviewRepository.flush(); + productRepository.updateProductStatisticsForReviewUpdate(target.getProduct().getId(), ratingGap); } @Transactional public void delete(final Long reviewId, final Long memberId) { final Review review = findTarget(reviewId, memberId); - review.reflectToProductBeforeDelete(); - reviewRepository.delete(review); final InventoryProduct inventoryProduct = inventoryProductRepository.findWithProductByMemberAndProduct( review.getMember(), review.getProduct()) .orElseThrow(InventoryProductNotFoundException::new); inventoryProductRepository.delete(inventoryProduct); + reviewRepository.delete(review); +// inventoryProductRepository.flush(); + reviewRepository.flush(); + productRepository.updateProductStatisticsForReviewDelete(review.getProduct().getId(), review.getRating()); } private Review findTarget(final Long reviewId, final Long memberId) { diff --git a/backend/src/main/java/com/woowacourse/f12/domain/product/ProductRepository.java b/backend/src/main/java/com/woowacourse/f12/domain/product/ProductRepository.java index ce9dc8e0..7d275596 100644 --- a/backend/src/main/java/com/woowacourse/f12/domain/product/ProductRepository.java +++ b/backend/src/main/java/com/woowacourse/f12/domain/product/ProductRepository.java @@ -2,8 +2,34 @@ import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; public interface ProductRepository extends JpaRepository, ProductRepositoryCustom { List findByReviewCountGreaterThanEqualAndRatingGreaterThanEqual(int reviewCount, double rating); + + @Modifying(clearAutomatically = true) + @Query(value = "update Product p " + + "set p.rating = (p.totalRating + :reviewRating) / cast((p.reviewCount + 1) as double), " + + "p.reviewCount = p.reviewCount + 1, " + + "p.totalRating = p.totalRating + :reviewRating " + + "where p.id = :productId") + void updateProductStatisticsForReviewInsert(Long productId, int reviewRating); + + @Modifying(clearAutomatically = true) + @Query(value = "update Product p " + + "set p.rating = case p.reviewCount when 1 then 0 " + + "else ((p.totalRating - :reviewRating) / cast((p.reviewCount - 1) as double)) end , " + + "p.reviewCount = p.reviewCount - 1, " + + "p.totalRating = p.totalRating - :reviewRating " + + "where p.id = :productId") + void updateProductStatisticsForReviewDelete(Long productId, int reviewRating); + + @Modifying(clearAutomatically = true) + @Query(value = "update Product p " + + "set p.rating = (p.totalRating + :ratingGap) / cast(p.reviewCount as double), " + + "p.totalRating = p.totalRating + :ratingGap " + + "where p.id = :productId") + void updateProductStatisticsForReviewUpdate(Long productId, int ratingGap); } diff --git a/backend/src/main/java/com/woowacourse/f12/domain/review/Review.java b/backend/src/main/java/com/woowacourse/f12/domain/review/Review.java index cf12e7a9..bd220926 100644 --- a/backend/src/main/java/com/woowacourse/f12/domain/review/Review.java +++ b/backend/src/main/java/com/woowacourse/f12/domain/review/Review.java @@ -88,25 +88,13 @@ private void validateRating(final int rating) { } } - public void reflectToProductWhenWritten() { - product.increaseReviewCount(); - product.increaseRating(rating); - } - - public void reflectToProductBeforeDelete() { - product.decreaseReviewCount(); - product.decreaseRating(rating); - } - public boolean isWrittenBy(final Member member) { return this.member.equals(member); } public void update(final Review updateReview) { - product.decreaseRating(rating); content = updateReview.getContent(); rating = updateReview.getRating(); - product.increaseRating(rating); } @Override diff --git a/backend/src/test/java/com/woowacourse/f12/application/review/ReviewServiceTest.java b/backend/src/test/java/com/woowacourse/f12/application/review/ReviewServiceTest.java index dcbc38c5..72e4dbcc 100644 --- a/backend/src/test/java/com/woowacourse/f12/application/review/ReviewServiceTest.java +++ b/backend/src/test/java/com/woowacourse/f12/application/review/ReviewServiceTest.java @@ -12,6 +12,7 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.assertAll; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.refEq; import static org.mockito.BDDMockito.any; @@ -100,9 +101,10 @@ class ReviewServiceTest { // then assertAll( () -> assertThat(reviewId).isEqualTo(1L), -// () -> assertThat(product.getReviewCount()).isOne(), () -> verify(productRepository).findById(productId), () -> verify(memberRepository).findById(memberId), + () -> verify(productRepository).updateProductStatisticsForReviewInsert(productId, + reviewRequest.getRating()), () -> verify(reviewRepository).save(any(Review.class)), () -> verify(inventoryProductRepository).existsByMemberAndProduct(member, product), () -> verify(inventoryProductRepository).save(inventoryProduct) @@ -207,7 +209,6 @@ class ReviewServiceTest { Member member = CORINNE.생성(1L); Pageable pageable = PageRequest.of(0, 1, Sort.by(Order.desc("createdAt"))); Review review = REVIEW_RATING_5.작성(1L, product, member); - review.reflectToProductWhenWritten(); Slice slice = new SliceImpl<>(List.of(review), pageable, true); given(productRepository.existsById(productId)) @@ -237,7 +238,6 @@ class ReviewServiceTest { Member member = CORINNE.생성(1L); Pageable pageable = PageRequest.of(0, 1, Sort.by(Order.desc("createdAt"))); Review review = REVIEW_RATING_5.작성(1L, product, member); - review.reflectToProductWhenWritten(); Slice slice = new SliceImpl<>(List.of(review), pageable, true); given(productRepository.existsById(productId)) @@ -269,7 +269,6 @@ class ReviewServiceTest { Member member = CORINNE.생성(1L); Pageable pageable = PageRequest.of(0, 1, Sort.by(Order.desc("createdAt"))); Review review = REVIEW_RATING_5.작성(1L, product, member); - review.reflectToProductWhenWritten(); Slice slice = new SliceImpl<>(List.of(review), pageable, true); given(productRepository.existsById(productId)) @@ -313,9 +312,7 @@ class ReviewServiceTest { Pageable pageable = PageRequest.of(0, 2, Sort.by(Order.desc("createdAt"))); Member member = CORINNE.생성(1L); Review review1 = REVIEW_RATING_5.작성(3L, KEYBOARD_1.생성(), member); - review1.reflectToProductWhenWritten(); Review review2 = REVIEW_RATING_5.작성(2L, KEYBOARD_2.생성(), member); - review2.reflectToProductWhenWritten(); Slice slice = new SliceImpl<>(List.of(review1, review2), pageable, true); given(reviewRepository.findPageBy(pageable)) @@ -342,9 +339,8 @@ class ReviewServiceTest { Long reviewId = 1L; Long memberId = 1L; Member member = CORINNE.생성(memberId); - Product product = KEYBOARD_1.생성(); + Product product = KEYBOARD_1.생성(1L); Review review = REVIEW_RATING_5.작성(reviewId, product, member); - review.reflectToProductWhenWritten(); ReviewRequest updateRequest = new ReviewRequest("수정할 내용", 4); given(memberRepository.findById(memberId)) @@ -360,9 +356,9 @@ class ReviewServiceTest { () -> assertThat(review).usingRecursiveComparison() .comparingOnlyFields("content", "rating") .isEqualTo(updateRequest.toReview(product, member)), - () -> assertThat(product.getRating()).isEqualTo(4.0), () -> verify(memberRepository).findById(memberId), - () -> verify(reviewRepository).findById(reviewId) + () -> verify(reviewRepository).findById(reviewId), + () -> verify(productRepository).updateProductStatisticsForReviewUpdate(product.getId(), -1) ); } @@ -428,7 +424,8 @@ class ReviewServiceTest { () -> assertThatThrownBy(() -> reviewService.update(reviewId, notAuthorId, updateRequest)) .isExactlyInstanceOf(NotAuthorException.class), () -> verify(memberRepository).findById(notAuthorId), - () -> verify(reviewRepository).findById(reviewId) + () -> verify(reviewRepository).findById(reviewId), + () -> verify(productRepository, times(0)).updateProductStatisticsForReviewUpdate(anyLong(), anyInt()) ); } @@ -441,7 +438,6 @@ class ReviewServiceTest { Member member = CORINNE.생성(memberId); Product product = KEYBOARD_1.생성(productId); Review review = REVIEW_RATING_5.작성(reviewId, product, member); - review.reflectToProductWhenWritten(); InventoryProduct inventoryProduct = SELECTED_INVENTORY_PRODUCT.생성(1L, member, product); given(memberRepository.findById(memberId)) @@ -458,12 +454,12 @@ class ReviewServiceTest { // when, then assertAll( () -> assertDoesNotThrow(() -> reviewService.delete(reviewId, memberId)), - () -> assertThat(product.getReviewCount()).isZero(), () -> verify(memberRepository).findById(memberId), () -> verify(reviewRepository).findById(reviewId), - () -> verify(reviewRepository).delete(review), () -> verify(inventoryProductRepository).findWithProductByMemberAndProduct(member, product), - () -> verify(inventoryProductRepository).delete(inventoryProduct) + () -> verify(inventoryProductRepository).delete(inventoryProduct), + () -> verify(reviewRepository).delete(review), + () -> verify(productRepository).updateProductStatisticsForReviewDelete(productId, review.getRating()) ); } @@ -544,8 +540,6 @@ class ReviewServiceTest { .willReturn(Optional.of(member)); given(reviewRepository.findById(reviewId)) .willReturn(Optional.of(review)); - willDoNothing().given(reviewRepository) - .delete(any(Review.class)); given(inventoryProductRepository.findWithProductByMemberAndProduct(member, product)) .willReturn(Optional.empty()); @@ -555,9 +549,9 @@ class ReviewServiceTest { .isExactlyInstanceOf(InventoryProductNotFoundException.class), () -> verify(memberRepository).findById(memberId), () -> verify(reviewRepository).findById(reviewId), - () -> verify(reviewRepository).delete(any(Review.class)), () -> verify(inventoryProductRepository).findWithProductByMemberAndProduct(member, product), - () -> verify(inventoryProductRepository, times(0)).delete(any(InventoryProduct.class)) + () -> verify(inventoryProductRepository, times(0)).delete(any(InventoryProduct.class)), + () -> verify(reviewRepository, times(0)).delete(any(Review.class)) ); } diff --git a/backend/src/test/java/com/woowacourse/f12/domain/product/ProductRepositoryTest.java b/backend/src/test/java/com/woowacourse/f12/domain/product/ProductRepositoryTest.java index 729308e5..ee06fee9 100644 --- a/backend/src/test/java/com/woowacourse/f12/domain/product/ProductRepositoryTest.java +++ b/backend/src/test/java/com/woowacourse/f12/domain/product/ProductRepositoryTest.java @@ -48,7 +48,7 @@ class ProductRepositoryTest { Member member2 = memberRepository.save(MINCHO.생성()); Review review1 = REVIEW_RATING_4.작성(product, member1); Review review2 = REVIEW_RATING_5.작성(product, member2); - + 리뷰_저장(review1); 리뷰_저장(review2); @@ -212,12 +212,93 @@ class ProductRepositoryTest { .containsOnly(keyboard1); } + @Test + void 리뷰_작성에_맞게_제품_정합성을_맞춘다() { + // given + Long productId = 제품_저장(KEYBOARD_1.생성()).getId(); + + // when + productRepository.updateProductStatisticsForReviewInsert(productId, 1); + productRepository.updateProductStatisticsForReviewInsert(productId, 2); + + // then + Product actual = productRepository.findById(productId) + .orElseThrow(); + + assertAll( + () -> assertThat(actual.getReviewCount()).isEqualTo(2), + () -> assertThat(actual.getRating()).isEqualTo(1.5), + () -> assertThat(actual.getTotalRating()).isEqualTo(3) + ); + } + + @Test + void 리뷰_삭제에_맞게_제품_정합성을_맞춘다() { + // given + Long productId = 제품_저장(KEYBOARD_1.생성()).getId(); + productRepository.updateProductStatisticsForReviewInsert(productId, 1); + productRepository.updateProductStatisticsForReviewInsert(productId, 2); + + // when + productRepository.updateProductStatisticsForReviewDelete(productId, 1); + + // then + Product actual = productRepository.findById(productId) + .orElseThrow(); + + assertAll( + () -> assertThat(actual.getReviewCount()).isOne(), + () -> assertThat(actual.getRating()).isEqualTo(2.0), + () -> assertThat(actual.getTotalRating()).isEqualTo(2) + ); + } + + @Test + void 리뷰_삭제로_리뷰_개수가_0개일_때_제품_정합성을_맞춘다() { + // given + Long productId = 제품_저장(KEYBOARD_1.생성()).getId(); + productRepository.updateProductStatisticsForReviewInsert(productId, 1); + + // when + productRepository.updateProductStatisticsForReviewDelete(productId, 1); + + // then + Product actual = productRepository.findById(productId) + .orElseThrow(); + + assertAll( + () -> assertThat(actual.getReviewCount()).isZero(), + () -> assertThat(actual.getRating()).isEqualTo(0.0), + () -> assertThat(actual.getTotalRating()).isEqualTo(0) + ); + } + + @Test + void 리뷰_수정에_맞게_제품_정합성을_맞춘다() { + // given + Long productId = 제품_저장(KEYBOARD_1.생성()).getId(); + productRepository.updateProductStatisticsForReviewInsert(productId, 5); + + // when + productRepository.updateProductStatisticsForReviewUpdate(productId, -2); + + // then + Product actual = productRepository.findById(productId) + .orElseThrow(); + + assertAll( + () -> assertThat(actual.getReviewCount()).isOne(), + () -> assertThat(actual.getRating()).isEqualTo(3.0), + () -> assertThat(actual.getTotalRating()).isEqualTo(3) + ); + } + private Product 제품_저장(Product product) { return productRepository.save(product); } private Review 리뷰_저장(Review review) { - review.reflectToProductWhenWritten(); + productRepository.updateProductStatisticsForReviewInsert(review.getProduct().getId(), review.getRating()); return reviewRepository.save(review); } } diff --git a/backend/src/test/java/com/woowacourse/f12/domain/review/ReviewRepositoryTest.java b/backend/src/test/java/com/woowacourse/f12/domain/review/ReviewRepositoryTest.java index ccccba48..631ad5ff 100644 --- a/backend/src/test/java/com/woowacourse/f12/domain/review/ReviewRepositoryTest.java +++ b/backend/src/test/java/com/woowacourse/f12/domain/review/ReviewRepositoryTest.java @@ -330,7 +330,6 @@ class ReviewRepositoryTest { } private Review 리뷰_저장(Review review) { - review.reflectToProductWhenWritten(); return reviewRepository.save(review); } } diff --git a/backend/src/test/java/com/woowacourse/f12/domain/review/ReviewTest.java b/backend/src/test/java/com/woowacourse/f12/domain/review/ReviewTest.java index 0a21472d..b35423ea 100644 --- a/backend/src/test/java/com/woowacourse/f12/domain/review/ReviewTest.java +++ b/backend/src/test/java/com/woowacourse/f12/domain/review/ReviewTest.java @@ -126,7 +126,6 @@ class ReviewTest { .rating(5) .product(product) .build(); - review.reflectToProductWhenWritten(); Review updateReview = Review.builder() .id(2L) @@ -143,57 +142,7 @@ class ReviewTest { () -> assertThat(review.getId()).isEqualTo(1L), () -> assertThat(review).usingRecursiveComparison() .ignoringFields("id", "product") - .isEqualTo(updateReview), - () -> assertThat(review.getProduct().getReviewCount()).isOne(), - () -> assertThat(review.getProduct().getTotalRating()).isEqualTo(4), - () -> assertThat(review.getProduct().getRating()).isEqualTo(4.0) - ); - } - - @Test - void 리뷰_작성_시_리뷰_대상_제품의_리뷰_개수와_평점을_증가시킨다() { - // given - Product product = Product.builder() - .build(); - - Review review = Review.builder() - .content("내용") - .rating(5) - .product(product) - .build(); - - // when - review.reflectToProductWhenWritten(); - - // then - assertAll( - () -> assertThat(product.getReviewCount()).isOne(), - () -> assertThat(product.getTotalRating()).isEqualTo(5), - () -> assertThat(product.getRating()).isEqualTo(5.0) - ); - } - - @Test - void 리뷰_삭제_전에_리뷰_대상_제품의_리뷰_개수와_평점을_감소시킨다() { - // given - Product product = Product.builder() - .build(); - - Review review = Review.builder() - .content("내용") - .rating(5) - .product(product) - .build(); - review.reflectToProductWhenWritten(); - - // when - review.reflectToProductBeforeDelete(); - - // then - assertAll( - () -> assertThat(product.getReviewCount()).isZero(), - () -> assertThat(product.getTotalRating()).isEqualTo(0), - () -> assertThat(product.getRating()).isEqualTo(0) + .isEqualTo(updateReview) ); } }