From cb540b416899c11af5e9e162a9e04d43caeb5f04 Mon Sep 17 00:00:00 2001 From: Jihoon Oh Date: Mon, 17 Oct 2022 21:01:34 +0900 Subject: [PATCH] =?UTF-8?q?[BE]=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=A0=95?= =?UTF-8?q?=ED=95=A9=EC=84=B1=EC=9D=84=20=EB=A7=9E=EC=B6=94=EB=8A=94=20?= =?UTF-8?q?=EC=8A=A4=EC=BC=80=EC=A4=84=EB=9F=AC=20=EC=9E=91=EC=84=B1=20(#7?= =?UTF-8?q?89)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 회원 팔로워 수의 데이터 정합성을 배치 처리하는 쿼리 작성 * refactor: 서비스 레이어에서 명시적 flush 대신 벌크 쿼리의 flushAutomatically 모드를 true로 설정 * feat: 1시간마다 회원의 팔로워 수 정합성을 맞추는 배치 쿼리를 실행하는 스케줄러 구현 --- .../f12/application/batch/BatchService.java | 20 +++++++++++++++ .../batch/FollowerCountBatchScheduler.java | 25 +++++++++++++++++++ .../f12/application/review/ReviewService.java | 3 --- .../f12/config/SchedulerConfig.java | 23 +++++++++++++++++ .../f12/domain/member/MemberRepository.java | 6 +++++ .../f12/domain/product/ProductRepository.java | 6 ++--- .../domain/member/MemberRepositoryTest.java | 23 +++++++++++++++++ 7 files changed, 100 insertions(+), 6 deletions(-) create mode 100644 backend/src/main/java/com/woowacourse/f12/application/batch/BatchService.java create mode 100644 backend/src/main/java/com/woowacourse/f12/application/batch/FollowerCountBatchScheduler.java create mode 100644 backend/src/main/java/com/woowacourse/f12/config/SchedulerConfig.java diff --git a/backend/src/main/java/com/woowacourse/f12/application/batch/BatchService.java b/backend/src/main/java/com/woowacourse/f12/application/batch/BatchService.java new file mode 100644 index 00000000..309f53cb --- /dev/null +++ b/backend/src/main/java/com/woowacourse/f12/application/batch/BatchService.java @@ -0,0 +1,20 @@ +package com.woowacourse.f12.application.batch; + +import com.woowacourse.f12.domain.member.MemberRepository; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +@Component +public class BatchService { + + private final MemberRepository memberRepository; + + public BatchService(final MemberRepository memberRepository) { + this.memberRepository = memberRepository; + } + + @Transactional + public void updateFollowerCount() { + memberRepository.updateFollowerCountBatch(); + } +} diff --git a/backend/src/main/java/com/woowacourse/f12/application/batch/FollowerCountBatchScheduler.java b/backend/src/main/java/com/woowacourse/f12/application/batch/FollowerCountBatchScheduler.java new file mode 100644 index 00000000..0caaed3a --- /dev/null +++ b/backend/src/main/java/com/woowacourse/f12/application/batch/FollowerCountBatchScheduler.java @@ -0,0 +1,25 @@ +package com.woowacourse.f12.application.batch; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Scheduled; + +@Slf4j +public class FollowerCountBatchScheduler { + + private static final String LOG_FORMAT = "Class : {}, Message : {}"; + + private final BatchService batchService; + + public FollowerCountBatchScheduler(final BatchService batchService) { + this.batchService = batchService; + } + + @Scheduled(cron = "0 0 0/1 1/1 * ? *") + public void execute() { + try { + batchService.updateFollowerCount(); + } catch (Exception e) { + log.error(LOG_FORMAT, e.getClass().getSimpleName(), e.getMessage()); + } + } +} 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 ed3fbff7..a3307577 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 @@ -113,7 +113,6 @@ public void update(final Long reviewId, final Long memberId, final ReviewRequest 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); } @@ -125,8 +124,6 @@ public void delete(final Long reviewId, final Long memberId) { .orElseThrow(InventoryProductNotFoundException::new); inventoryProductRepository.delete(inventoryProduct); reviewRepository.delete(review); -// inventoryProductRepository.flush(); - reviewRepository.flush(); productRepository.updateProductStatisticsForReviewDelete(review.getProduct().getId(), review.getRating()); } diff --git a/backend/src/main/java/com/woowacourse/f12/config/SchedulerConfig.java b/backend/src/main/java/com/woowacourse/f12/config/SchedulerConfig.java new file mode 100644 index 00000000..a8b9a0ff --- /dev/null +++ b/backend/src/main/java/com/woowacourse/f12/config/SchedulerConfig.java @@ -0,0 +1,23 @@ +package com.woowacourse.f12.config; + +import com.woowacourse.f12.application.batch.BatchService; +import com.woowacourse.f12.application.batch.FollowerCountBatchScheduler; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.EnableScheduling; + +@EnableScheduling +@Configuration +public class SchedulerConfig { + + private final BatchService batchService; + + public SchedulerConfig(final BatchService batchService) { + this.batchService = batchService; + } + + @Bean(name = "followerCountBatchScheduler") + public FollowerCountBatchScheduler followerCountBatchScheduler() { + return new FollowerCountBatchScheduler(batchService); + } +} diff --git a/backend/src/main/java/com/woowacourse/f12/domain/member/MemberRepository.java b/backend/src/main/java/com/woowacourse/f12/domain/member/MemberRepository.java index 1bc2584d..0f3beb74 100644 --- a/backend/src/main/java/com/woowacourse/f12/domain/member/MemberRepository.java +++ b/backend/src/main/java/com/woowacourse/f12/domain/member/MemberRepository.java @@ -2,8 +2,14 @@ import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; public interface MemberRepository extends JpaRepository, MemberRepositoryCustom { Optional findByGitHubId(String gitHubId); + + @Modifying(clearAutomatically = true, flushAutomatically = true) + @Query(value = "update Member m set m.followerCount = (select count(f) from Following f where f.followingId = m.id)") + void updateFollowerCountBatch(); } 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 7d275596..9217606d 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 @@ -9,7 +9,7 @@ public interface ProductRepository extends JpaRepository, Product List findByReviewCountGreaterThanEqualAndRatingGreaterThanEqual(int reviewCount, double rating); - @Modifying(clearAutomatically = true) + @Modifying(clearAutomatically = true, flushAutomatically = true) @Query(value = "update Product p " + "set p.rating = (p.totalRating + :reviewRating) / cast((p.reviewCount + 1) as double), " + "p.reviewCount = p.reviewCount + 1, " @@ -17,7 +17,7 @@ public interface ProductRepository extends JpaRepository, Product + "where p.id = :productId") void updateProductStatisticsForReviewInsert(Long productId, int reviewRating); - @Modifying(clearAutomatically = true) + @Modifying(clearAutomatically = true, flushAutomatically = 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 , " @@ -26,7 +26,7 @@ public interface ProductRepository extends JpaRepository, Product + "where p.id = :productId") void updateProductStatisticsForReviewDelete(Long productId, int reviewRating); - @Modifying(clearAutomatically = true) + @Modifying(clearAutomatically = true, flushAutomatically = true) @Query(value = "update Product p " + "set p.rating = (p.totalRating + :ratingGap) / cast(p.reviewCount as double), " + "p.totalRating = p.totalRating + :ratingGap " diff --git a/backend/src/test/java/com/woowacourse/f12/domain/member/MemberRepositoryTest.java b/backend/src/test/java/com/woowacourse/f12/domain/member/MemberRepositoryTest.java index bc07a1fa..5716c633 100644 --- a/backend/src/test/java/com/woowacourse/f12/domain/member/MemberRepositoryTest.java +++ b/backend/src/test/java/com/woowacourse/f12/domain/member/MemberRepositoryTest.java @@ -315,4 +315,27 @@ class MemberRepositoryTest { assertThatThrownBy(() -> memberRepository.save(member2)) .isInstanceOf(DataIntegrityViolationException.class); } + + @Test + void 회원의_팔로워_수의_정합성을_맞춘다() { + // given + Member member = CORINNE.생성(); + Member follower = MINCHO.생성(); + memberRepository.save(member); + memberRepository.save(follower); + Following following = Following.builder() + .followerId(follower.getId()) + .followingId(member.getId()) + .build(); + followingRepository.save(following); + + // when + memberRepository.updateFollowerCountBatch(); + + // then + Member actual = memberRepository.findById(member.getId()) + .orElseThrow(); + + assertThat(actual.getFollowerCount()).isOne(); + } }