From 353086bf97f36844e4cdd6fb833576bc5575399c Mon Sep 17 00:00:00 2001 From: yeonnseok Date: Sat, 26 Sep 2020 15:13:28 +0900 Subject: [PATCH 1/4] refactor: QuestionHashtag cascade=ALL --- .../underdogs/devbie/question/domain/QuestionHashtags.java | 3 ++- .../devbie/question/service/QuestionHashtagService.java | 3 +-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/underdogs/devbie/question/domain/QuestionHashtags.java b/src/main/java/underdogs/devbie/question/domain/QuestionHashtags.java index 5f777d03..918d3167 100644 --- a/src/main/java/underdogs/devbie/question/domain/QuestionHashtags.java +++ b/src/main/java/underdogs/devbie/question/domain/QuestionHashtags.java @@ -1,5 +1,6 @@ package underdogs.devbie.question.domain; +import static javax.persistence.CascadeType.*; import static javax.persistence.FetchType.*; import java.util.LinkedHashSet; @@ -23,7 +24,7 @@ @ToString public class QuestionHashtags { - @OneToMany(fetch = LAZY, orphanRemoval = true, mappedBy = "question") + @OneToMany(fetch = LAZY, cascade = ALL, orphanRemoval = true, mappedBy = "question") private Set questionHashtags = new LinkedHashSet<>(); public static QuestionHashtags from(Set questionHashtags) { diff --git a/src/main/java/underdogs/devbie/question/service/QuestionHashtagService.java b/src/main/java/underdogs/devbie/question/service/QuestionHashtagService.java index 8bd22e76..7f174d64 100644 --- a/src/main/java/underdogs/devbie/question/service/QuestionHashtagService.java +++ b/src/main/java/underdogs/devbie/question/service/QuestionHashtagService.java @@ -49,12 +49,11 @@ private QuestionHashtags mapToQuestionHashtags(Question question, Set ha } private QuestionHashtag findOrCreateQuestionHashtag(Question question, Hashtag hashtag) { - QuestionHashtag questionHashtag = questionHashtagRepository.findByQuestionIdAndHashtagId(question.getId(), hashtag.getId()) + return questionHashtagRepository.findByQuestionIdAndHashtagId(question.getId(), hashtag.getId()) .orElse(QuestionHashtag.builder() .question(question) .hashtag(hashtag) .build()); - return questionHashtagRepository.save(questionHashtag); } public List findIdsByHashtagName(String hashtag) { From 2da1c37d91da75dc507ac3cabe46d2f1f5a320f0 Mon Sep 17 00:00:00 2001 From: yeonnseok Date: Sat, 26 Sep 2020 15:17:50 +0900 Subject: [PATCH 2/4] =?UTF-8?q?feat:=20=EB=B9=84=EB=8F=99=EA=B8=B0=20?= =?UTF-8?q?=EC=B2=98=EB=A6=AC=EB=A5=BC=20=EC=9C=84=ED=95=9C=20AsyncConfig?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/underdogs/devbie/config/AsyncConfig.java | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 src/main/java/underdogs/devbie/config/AsyncConfig.java diff --git a/src/main/java/underdogs/devbie/config/AsyncConfig.java b/src/main/java/underdogs/devbie/config/AsyncConfig.java new file mode 100644 index 00000000..c2b1b315 --- /dev/null +++ b/src/main/java/underdogs/devbie/config/AsyncConfig.java @@ -0,0 +1,9 @@ +package underdogs.devbie.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.EnableAsync; + +@EnableAsync +@Configuration +public class AsyncConfig { +} From ff47eec81d5869c198f2d40aa573d7979a058d3b Mon Sep 17 00:00:00 2001 From: yeonnseok Date: Sat, 26 Sep 2020 20:31:31 +0900 Subject: [PATCH 3/4] =?UTF-8?q?feat:=20=EC=B0=B8=EC=A1=B0=ED=95=98?= =?UTF-8?q?=EA=B3=A0=20=EC=9E=88=EB=8A=94=20=EC=A7=88=EB=AC=B8=EC=9D=B4=20?= =?UTF-8?q?=EC=97=86=EB=8A=94=20Hashtag=20=EC=82=AD=EC=A0=9C=20=EC=9D=B4?= =?UTF-8?q?=EB=B2=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../underdogs/devbie/config/AsyncConfig.java | 15 +++++++ .../devbie/question/domain/Hashtag.java | 8 ++++ .../question/domain/QuestionHashtags.java | 6 +++ .../domain/repository/HashtagRepository.java | 2 +- .../repository/HashtagRepositoryCustom.java | 10 +++++ .../repository/HashtagRepositoryImpl.java | 38 ++++++++++++++++ .../question/service/HashtagService.java | 8 +++- .../question/service/QuestionService.java | 5 +++ .../service/event/QuestionDeleteEvent.java | 17 +++++++ .../service/event/QuestionDeleteListener.java | 23 ++++++++++ .../repository/HashtagRepositoryImplTest.java | 45 +++++++++++++++++++ .../question/service/QuestionServiceTest.java | 8 +++- 12 files changed, 181 insertions(+), 4 deletions(-) create mode 100644 src/main/java/underdogs/devbie/question/domain/repository/HashtagRepositoryCustom.java create mode 100644 src/main/java/underdogs/devbie/question/domain/repository/HashtagRepositoryImpl.java create mode 100644 src/main/java/underdogs/devbie/question/service/event/QuestionDeleteEvent.java create mode 100644 src/main/java/underdogs/devbie/question/service/event/QuestionDeleteListener.java create mode 100644 src/test/java/underdogs/devbie/question/domain/repository/HashtagRepositoryImplTest.java diff --git a/src/main/java/underdogs/devbie/config/AsyncConfig.java b/src/main/java/underdogs/devbie/config/AsyncConfig.java index c2b1b315..60d68a37 100644 --- a/src/main/java/underdogs/devbie/config/AsyncConfig.java +++ b/src/main/java/underdogs/devbie/config/AsyncConfig.java @@ -1,9 +1,24 @@ package underdogs.devbie.config; +import java.util.concurrent.Executor; + +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; @EnableAsync @Configuration public class AsyncConfig { + + @Bean(name = "threadPoolTaskExecutor") + public Executor threadPoolTaskExecutor() { + ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor(); + taskExecutor.setCorePoolSize(3); + taskExecutor.setMaxPoolSize(30); + taskExecutor.setQueueCapacity(10); + taskExecutor.setThreadNamePrefix("Executor-"); + taskExecutor.initialize(); + return taskExecutor; + } } diff --git a/src/main/java/underdogs/devbie/question/domain/Hashtag.java b/src/main/java/underdogs/devbie/question/domain/Hashtag.java index 1ff220d6..6714946e 100644 --- a/src/main/java/underdogs/devbie/question/domain/Hashtag.java +++ b/src/main/java/underdogs/devbie/question/domain/Hashtag.java @@ -1,12 +1,17 @@ package underdogs.devbie.question.domain; +import static javax.persistence.CascadeType.*; + +import java.util.LinkedHashSet; import java.util.Objects; +import java.util.Set; import javax.persistence.Embedded; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; +import javax.persistence.OneToMany; import lombok.AccessLevel; import lombok.Builder; @@ -29,6 +34,9 @@ public class Hashtag extends BaseTimeEntity { @Embedded private TagName tagName; + @OneToMany(mappedBy = "hashtag", cascade = ALL) + private Set questionHashtags = new LinkedHashSet<>(); + @Builder public Hashtag(Long id, TagName tagName) { validateParameters(tagName); diff --git a/src/main/java/underdogs/devbie/question/domain/QuestionHashtags.java b/src/main/java/underdogs/devbie/question/domain/QuestionHashtags.java index 918d3167..143223de 100644 --- a/src/main/java/underdogs/devbie/question/domain/QuestionHashtags.java +++ b/src/main/java/underdogs/devbie/question/domain/QuestionHashtags.java @@ -46,4 +46,10 @@ public void setHashtags(Set questionHashtags) { this.questionHashtags.clear(); this.questionHashtags.addAll(questionHashtags); } + + public List toPureHashtags() { + return this.questionHashtags.stream() + .map(QuestionHashtag::getHashtag) + .collect(Collectors.toList()); + } } diff --git a/src/main/java/underdogs/devbie/question/domain/repository/HashtagRepository.java b/src/main/java/underdogs/devbie/question/domain/repository/HashtagRepository.java index dab2d6c7..073bc8af 100644 --- a/src/main/java/underdogs/devbie/question/domain/repository/HashtagRepository.java +++ b/src/main/java/underdogs/devbie/question/domain/repository/HashtagRepository.java @@ -7,7 +7,7 @@ import underdogs.devbie.question.domain.Hashtag; import underdogs.devbie.question.domain.TagName; -public interface HashtagRepository extends JpaRepository { +public interface HashtagRepository extends JpaRepository, HashtagRepositoryCustom { Optional findByTagName(TagName tagName); } diff --git a/src/main/java/underdogs/devbie/question/domain/repository/HashtagRepositoryCustom.java b/src/main/java/underdogs/devbie/question/domain/repository/HashtagRepositoryCustom.java new file mode 100644 index 00000000..64a13e49 --- /dev/null +++ b/src/main/java/underdogs/devbie/question/domain/repository/HashtagRepositoryCustom.java @@ -0,0 +1,10 @@ +package underdogs.devbie.question.domain.repository; + +import java.util.List; + +import underdogs.devbie.question.domain.Hashtag; + +public interface HashtagRepositoryCustom { + + void deleteEmptyRefHashtag(List hashtags); +} diff --git a/src/main/java/underdogs/devbie/question/domain/repository/HashtagRepositoryImpl.java b/src/main/java/underdogs/devbie/question/domain/repository/HashtagRepositoryImpl.java new file mode 100644 index 00000000..4b51e3ec --- /dev/null +++ b/src/main/java/underdogs/devbie/question/domain/repository/HashtagRepositoryImpl.java @@ -0,0 +1,38 @@ +package underdogs.devbie.question.domain.repository; + +import static underdogs.devbie.question.domain.QHashtag.*; +import static underdogs.devbie.question.domain.QQuestionHashtag.*; + +import java.util.List; + +import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.core.types.dsl.Expressions; +import com.querydsl.jpa.impl.JPAQueryFactory; +import underdogs.devbie.question.domain.Hashtag; + +public class HashtagRepositoryImpl implements HashtagRepositoryCustom { + + private final JPAQueryFactory jpaQueryFactory; + + public HashtagRepositoryImpl(JPAQueryFactory jpaQueryFactory) { + this.jpaQueryFactory = jpaQueryFactory; + } + + @Override + public void deleteEmptyRefHashtag(List hashtags) { + hashtags.forEach(h -> + jpaQueryFactory + .delete(hashtag) + .where(neverReferenced(h), hashtag.id.eq(h.getId())) + .execute() + ); + } + + private BooleanExpression neverReferenced(Hashtag hashtag) { + long refCount = jpaQueryFactory + .selectFrom(questionHashtag) + .where(questionHashtag.hashtag.id.eq(hashtag.getId())) + .fetchCount(); + return Expressions.asBoolean(refCount == 0).isTrue(); + } +} diff --git a/src/main/java/underdogs/devbie/question/service/HashtagService.java b/src/main/java/underdogs/devbie/question/service/HashtagService.java index d3af62f5..2cf45c6a 100644 --- a/src/main/java/underdogs/devbie/question/service/HashtagService.java +++ b/src/main/java/underdogs/devbie/question/service/HashtagService.java @@ -10,8 +10,9 @@ import lombok.RequiredArgsConstructor; import underdogs.devbie.question.domain.Hashtag; -import underdogs.devbie.question.domain.repository.HashtagRepository; +import underdogs.devbie.question.domain.QuestionHashtags; import underdogs.devbie.question.domain.TagName; +import underdogs.devbie.question.domain.repository.HashtagRepository; import underdogs.devbie.question.dto.HashtagCreateRequest; import underdogs.devbie.question.dto.HashtagResponse; import underdogs.devbie.question.dto.HashtagResponses; @@ -78,4 +79,9 @@ public Hashtag findOrCreateHashtag(String tagName) { .build()); return hashtagRepository.save(hashtag); } + + public void deleteEmptyRefHashtag(QuestionHashtags questionHashtags) { + List hashtags = questionHashtags.toPureHashtags(); + hashtagRepository.deleteEmptyRefHashtag(hashtags); + } } diff --git a/src/main/java/underdogs/devbie/question/service/QuestionService.java b/src/main/java/underdogs/devbie/question/service/QuestionService.java index f4c9137a..3bcc427a 100644 --- a/src/main/java/underdogs/devbie/question/service/QuestionService.java +++ b/src/main/java/underdogs/devbie/question/service/QuestionService.java @@ -2,6 +2,7 @@ import java.util.List; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; @@ -17,6 +18,7 @@ import underdogs.devbie.question.dto.QuestionUpdateRequest; import underdogs.devbie.question.exception.NotMatchedQuestionAuthorException; import underdogs.devbie.question.exception.QuestionNotExistedException; +import underdogs.devbie.question.service.event.QuestionDeleteEvent; import underdogs.devbie.recommendation.domain.RecommendationType; import underdogs.devbie.user.domain.User; import underdogs.devbie.user.service.UserService; @@ -29,6 +31,7 @@ public class QuestionService { private final UserService userService; private final QuestionHashtagService questionHashtagService; private final QuestionRepository questionRepository; + private final ApplicationEventPublisher eventPublisher; @Transactional public Long save(Long userId, QuestionCreateRequest request) { @@ -87,6 +90,8 @@ public void delete(User user, Long questionId) { validateQuestionAuthorOrAdmin(user, question); questionRepository.deleteById(questionId); + + eventPublisher.publishEvent(new QuestionDeleteEvent(this, question.getHashtags())); } private void validateQuestionAuthorOrAdmin(User user, Question question) { diff --git a/src/main/java/underdogs/devbie/question/service/event/QuestionDeleteEvent.java b/src/main/java/underdogs/devbie/question/service/event/QuestionDeleteEvent.java new file mode 100644 index 00000000..f8ade7f1 --- /dev/null +++ b/src/main/java/underdogs/devbie/question/service/event/QuestionDeleteEvent.java @@ -0,0 +1,17 @@ +package underdogs.devbie.question.service.event; + +import org.springframework.context.ApplicationEvent; + +import lombok.Getter; +import underdogs.devbie.question.domain.QuestionHashtags; + +@Getter +public class QuestionDeleteEvent extends ApplicationEvent { + + private final QuestionHashtags questionHashtags; + + public QuestionDeleteEvent(Object source, QuestionHashtags questionHashtags) { + super(source); + this.questionHashtags = questionHashtags; + } +} diff --git a/src/main/java/underdogs/devbie/question/service/event/QuestionDeleteListener.java b/src/main/java/underdogs/devbie/question/service/event/QuestionDeleteListener.java new file mode 100644 index 00000000..4362047a --- /dev/null +++ b/src/main/java/underdogs/devbie/question/service/event/QuestionDeleteListener.java @@ -0,0 +1,23 @@ +package underdogs.devbie.question.service.event; + +import static org.springframework.transaction.event.TransactionPhase.*; + +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; +import org.springframework.transaction.event.TransactionalEventListener; + +import lombok.RequiredArgsConstructor; +import underdogs.devbie.question.service.HashtagService; + +@Component +@RequiredArgsConstructor +public class QuestionDeleteListener { + + private final HashtagService hashtagService; + + @Async("threadPoolTaskExecutor") + @TransactionalEventListener(phase = AFTER_COMMIT, fallbackExecution = true) + public void handleQuestionDelete(QuestionDeleteEvent event) { + hashtagService.deleteEmptyRefHashtag(event.getQuestionHashtags()); + } +} diff --git a/src/test/java/underdogs/devbie/question/domain/repository/HashtagRepositoryImplTest.java b/src/test/java/underdogs/devbie/question/domain/repository/HashtagRepositoryImplTest.java new file mode 100644 index 00000000..0f76cc66 --- /dev/null +++ b/src/test/java/underdogs/devbie/question/domain/repository/HashtagRepositoryImplTest.java @@ -0,0 +1,45 @@ +package underdogs.devbie.question.domain.repository; + +import static org.assertj.core.api.Assertions.*; + +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.transaction.annotation.Transactional; + +import underdogs.devbie.question.domain.Hashtag; +import underdogs.devbie.question.domain.TagName; + +@SpringBootTest +@Transactional +class HashtagRepositoryImplTest { + + @Autowired + private HashtagRepository hashtagRepository; + + @BeforeEach + public void setUp() { + Hashtag hashtag1 = Hashtag.builder().id(10L).tagName(TagName.from("java")).build(); + Hashtag hashtag2 = Hashtag.builder().id(11L).tagName(TagName.from("network")).build(); + + hashtagRepository.save(hashtag1); + hashtagRepository.save(hashtag2); + } + + @DisplayName("참조하고 있는 질문이 없을 경우 해시태그 자동 삭제") + @Test + public void deleteEmptyRefHashtags() { + List hashtags = hashtagRepository.findAll(); + + hashtagRepository.deleteEmptyRefHashtag(hashtags); + + List afterDeletedHashtags = hashtagRepository.findAll(); + + assertThat(afterDeletedHashtags).isEmpty(); + } + +} \ No newline at end of file diff --git a/src/test/java/underdogs/devbie/question/service/QuestionServiceTest.java b/src/test/java/underdogs/devbie/question/service/QuestionServiceTest.java index 31d99d28..0a6e116c 100644 --- a/src/test/java/underdogs/devbie/question/service/QuestionServiceTest.java +++ b/src/test/java/underdogs/devbie/question/service/QuestionServiceTest.java @@ -19,6 +19,7 @@ import org.mockito.Mock; import org.mockito.internal.util.collections.Sets; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.Pageable; @@ -29,9 +30,9 @@ import underdogs.devbie.question.domain.QuestionContent; import underdogs.devbie.question.domain.QuestionHashtag; import underdogs.devbie.question.domain.QuestionHashtags; -import underdogs.devbie.question.domain.repository.QuestionRepository; import underdogs.devbie.question.domain.QuestionTitle; import underdogs.devbie.question.domain.TagName; +import underdogs.devbie.question.domain.repository.QuestionRepository; import underdogs.devbie.question.dto.HashtagResponse; import underdogs.devbie.question.dto.QuestionCreateRequest; import underdogs.devbie.question.dto.QuestionPageRequest; @@ -61,6 +62,9 @@ public class QuestionServiceTest { @Mock private QuestionRepository questionRepository; + @Mock + private ApplicationEventPublisher publisher; + private User user; private Question question; @@ -69,7 +73,7 @@ public class QuestionServiceTest { @BeforeEach void setUp() { - questionService = new QuestionService(userService, questionHashtagService, questionRepository); + questionService = new QuestionService(userService, questionHashtagService, questionRepository, publisher); user = User.builder() .id(1L) From 1f27a744a72b30080bb22b256eb6213186d40d05 Mon Sep 17 00:00:00 2001 From: yeonnseok Date: Sat, 26 Sep 2020 21:01:01 +0900 Subject: [PATCH 4/4] =?UTF-8?q?test:=20=EA=B9=A8=EC=A7=80=EB=8A=94=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/QuestionHashtagServiceTest.java | 22 ++++++------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/src/test/java/underdogs/devbie/question/service/QuestionHashtagServiceTest.java b/src/test/java/underdogs/devbie/question/service/QuestionHashtagServiceTest.java index af54020e..48cddc2b 100644 --- a/src/test/java/underdogs/devbie/question/service/QuestionHashtagServiceTest.java +++ b/src/test/java/underdogs/devbie/question/service/QuestionHashtagServiceTest.java @@ -8,6 +8,7 @@ import java.util.ArrayList; import java.util.LinkedHashSet; import java.util.List; +import java.util.Optional; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -21,9 +22,9 @@ import underdogs.devbie.question.domain.Hashtag; import underdogs.devbie.question.domain.Question; import underdogs.devbie.question.domain.QuestionHashtag; -import underdogs.devbie.question.domain.repository.QuestionHashtagRepository; import underdogs.devbie.question.domain.QuestionHashtags; import underdogs.devbie.question.domain.TagName; +import underdogs.devbie.question.domain.repository.QuestionHashtagRepository; @ExtendWith(MockitoExtension.class) class QuestionHashtagServiceTest { @@ -61,13 +62,6 @@ void setUp() { @Test void saveHashtags() { given(hashtagService.findOrCreateHashtag(anyString())).willReturn(hashtag); - given(questionHashtagRepository.save(any(QuestionHashtag.class))).willReturn( - QuestionHashtag.builder() - .id(1L) - .question(question) - .hashtag(hashtag) - .build() - ); questionHashtagService.saveHashtags(question, Sets.newSet("java")); @@ -86,14 +80,12 @@ void updateHashtags() { .id(3L) .tagName(TagName.from("kotlin")) .build(); + QuestionHashtag questionHashtag = QuestionHashtag.builder() + .question(question) + .hashtag(updateHashtag) + .build(); given(hashtagService.findOrCreateHashtag(anyString())).willReturn(hashtag); - given(questionHashtagRepository.save(any(QuestionHashtag.class))).willReturn( - QuestionHashtag.builder() - .id(1L) - .question(question) - .hashtag(updateHashtag) - .build() - ); + given(questionHashtagRepository.findByQuestionIdAndHashtagId(any(), any())).willReturn(Optional.of(questionHashtag)); questionHashtagService.updateHashtags(question, Sets.newSet("kotlin"));