Skip to content

Commit

Permalink
[BE] 리뷰 작성 시 제품 통계 정보의 정합성을 맞추도록 수정 (#784)
Browse files Browse the repository at this point in the history
* feat: 제품 통계 정합성 맞추는 쿼리 작성

* refactor: 서비스에서 제품 통계 업데이트를 쿼리를 직접 사용하도록 수정
  • Loading branch information
Ohzzi authored Oct 17, 2022
1 parent ac0a85c commit 3fc5376
Show file tree
Hide file tree
Showing 7 changed files with 131 additions and 89 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand All @@ -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();
}
Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Product, Long>, ProductRepositoryCustom {

List<Product> 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);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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<Review> slice = new SliceImpl<>(List.of(review), pageable, true);

given(productRepository.existsById(productId))
Expand Down Expand Up @@ -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<Review> slice = new SliceImpl<>(List.of(review), pageable, true);

given(productRepository.existsById(productId))
Expand Down Expand Up @@ -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<Review> slice = new SliceImpl<>(List.of(review), pageable, true);

given(productRepository.existsById(productId))
Expand Down Expand Up @@ -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<Review> slice = new SliceImpl<>(List.of(review1, review2), pageable, true);

given(reviewRepository.findPageBy(pageable))
Expand All @@ -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))
Expand All @@ -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)
);
}

Expand Down Expand Up @@ -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())
);
}

Expand All @@ -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))
Expand All @@ -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())
);
}

Expand Down Expand Up @@ -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());

Expand All @@ -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))
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -330,7 +330,6 @@ class ReviewRepositoryTest {
}

private Review 리뷰_저장(Review review) {
review.reflectToProductWhenWritten();
return reviewRepository.save(review);
}
}
Loading

0 comments on commit 3fc5376

Please sign in to comment.