Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat : 참조되지 않는 해시태그 제거 #312

Open
wants to merge 4 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions src/main/java/underdogs/devbie/config/AsyncConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +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;
}
}
8 changes: 8 additions & 0 deletions src/main/java/underdogs/devbie/question/domain/Hashtag.java
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -29,6 +34,9 @@ public class Hashtag extends BaseTimeEntity {
@Embedded
private TagName tagName;

@OneToMany(mappedBy = "hashtag", cascade = ALL)
private Set<QuestionHashtag> questionHashtags = new LinkedHashSet<>();

@Builder
public Hashtag(Long id, TagName tagName) {
validateParameters(tagName);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package underdogs.devbie.question.domain;

import static javax.persistence.CascadeType.*;
import static javax.persistence.FetchType.*;

import java.util.LinkedHashSet;
Expand All @@ -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<QuestionHashtag> questionHashtags = new LinkedHashSet<>();

public static QuestionHashtags from(Set<QuestionHashtag> questionHashtags) {
Expand All @@ -45,4 +46,10 @@ public void setHashtags(Set<QuestionHashtag> questionHashtags) {
this.questionHashtags.clear();
this.questionHashtags.addAll(questionHashtags);
}

public List<Hashtag> toPureHashtags() {
return this.questionHashtags.stream()
.map(QuestionHashtag::getHashtag)
.collect(Collectors.toList());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import underdogs.devbie.question.domain.Hashtag;
import underdogs.devbie.question.domain.TagName;

public interface HashtagRepository extends JpaRepository<Hashtag, Long> {
public interface HashtagRepository extends JpaRepository<Hashtag, Long>, HashtagRepositoryCustom {

Optional<Hashtag> findByTagName(TagName tagName);
}
Original file line number Diff line number Diff line change
@@ -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<Hashtag> hashtags);
}
Original file line number Diff line number Diff line change
@@ -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<Hashtag> 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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -78,4 +79,9 @@ public Hashtag findOrCreateHashtag(String tagName) {
.build());
return hashtagRepository.save(hashtag);
}

public void deleteEmptyRefHashtag(QuestionHashtags questionHashtags) {
List<Hashtag> hashtags = questionHashtags.toPureHashtags();
hashtagRepository.deleteEmptyRefHashtag(hashtags);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,11 @@ private QuestionHashtags mapToQuestionHashtags(Question question, Set<String> 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<Long> findIdsByHashtagName(String hashtag) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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());
}
}
Original file line number Diff line number Diff line change
@@ -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<Hashtag> hashtags = hashtagRepository.findAll();

hashtagRepository.deleteEmptyRefHashtag(hashtags);

List<Hashtag> afterDeletedHashtags = hashtagRepository.findAll();

assertThat(afterDeletedHashtags).isEmpty();
}

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

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -61,6 +62,9 @@ public class QuestionServiceTest {
@Mock
private QuestionRepository questionRepository;

@Mock
private ApplicationEventPublisher publisher;

private User user;

private Question question;
Expand All @@ -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)
Expand Down