Skip to content

Commit 223f154

Browse files
authored
Refactor/refactoring feed (#110)
* Refactor: 답변 목록 조회 페이징 동적쿼리 Querydsl로 변경 * Refactor: 특정 사용자의 답변 목록 조회, 페이징 동적쿼리 Querydsl로 변경 * Refactor: 특정 사용자의 답변 갯수 조회, 동적쿼리 Querydsl로 변경 * Refactor: 인기 답변 조회, 동적쿼리 Querydsl로 변경 * Refactor: 연관관계 매핑 리팩토링 * Refactor: 동적 쿼리 리팩토링 * Refactor: 좋아요 누름 여부 체크 리팩토링
1 parent cef6089 commit 223f154

File tree

7 files changed

+113
-116
lines changed

7 files changed

+113
-116
lines changed

module-application/src/main/java/com/wsws/moduleapplication/feed/service/AnswerReadService.java

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
import com.wsws.moduledomain.feed.comment.AnswerComment;
1111
import com.wsws.moduledomain.feed.comment.repo.AnswerCommentRepository;
1212
import com.wsws.moduledomain.feed.dto.AnswerQuestionDTO;
13-
import com.wsws.moduledomain.socialnetwork.follow.aggregate.Follow;
13+
import com.wsws.moduledomain.feed.like.TargetType;
1414
import com.wsws.moduledomain.socialnetwork.follow.repo.FollowRepository;
1515
import com.wsws.moduledomain.feed.like.Like;
1616
import com.wsws.moduledomain.usercontext.user.aggregate.User;
@@ -24,6 +24,9 @@
2424
import java.util.*;
2525
import java.util.stream.Collectors;
2626

27+
import static com.wsws.moduledomain.feed.like.TargetType.ANSWER;
28+
import static com.wsws.moduledomain.feed.like.TargetType.ANSWER_COMMENT;
29+
2730
@Service
2831
@Transactional(readOnly = true)
2932
@RequiredArgsConstructor
@@ -160,7 +163,7 @@ private void buildAnswer(Answer answer, String reqUserId, AnswerFindServiceRespo
160163
.orElseThrow(() -> UserNotFoundException.EXCEPTION);
161164

162165
// 해당 사용자가 해당 답변에 좋아요를 눌렀는지
163-
boolean isLike = buildIsLike(reqUserId, answer.getAnswerId().getValue());
166+
boolean isLike = buildIsLike(reqUserId, answer.getAnswerId().getValue(), ANSWER);
164167

165168
// 해당 사용자가 특정 작성자(작성자 ID)를 팔로우 했는지 확인
166169
boolean isFollowing = buildIsFollowing(reqUserId, answerAuthor.getId().getValue());
@@ -222,13 +225,10 @@ private void buildAnswerComment(List<AnswerComment> parentComments, String reqUs
222225
/**
223226
* 좋아요 여부 정보를 세팅
224227
*/
225-
private boolean buildIsLike(String currentUserId, Long targetId) {
226-
// 조회 요청한 사용자가 좋아요 누른 답변 및 답변 댓글 정보 다 가져오기
227-
List<Like> likes = likeRepository.findByUserId(currentUserId);
228-
229-
// 해당 사용자가 해당 답변에 좋아요를 눌렀는지
230-
return likes.stream()
231-
.anyMatch(like -> like.getTargetId().getValue().equals(targetId));
228+
private boolean buildIsLike(String currentUserId, Long targetId, TargetType targetType) {
229+
// 특정 사용자가 특정 글에 좋아요를 눌렀는지
230+
return likeRepository
231+
.existsByUserEntityIdAndTargetIdAndTargetType(currentUserId, targetId, targetType);
232232
}
233233

234234
/**
@@ -249,7 +249,7 @@ private void buildParentComment(AnswerCommentFindServiceResponseBuilder commentR
249249
User commentAuthor = userRepository.findById(UserId.of(parent.getUserId().getValue()))
250250
.orElseThrow(() -> UserNotFoundException.EXCEPTION);
251251

252-
boolean isLike = buildIsLike(reqUserId, parent.getParentAnswerCommentId().getValue());
252+
boolean isLike = buildIsLike(reqUserId, parent.getParentAnswerCommentId().getValue(), ANSWER_COMMENT);
253253

254254
boolean isFollowing = buildIsFollowing(reqUserId, commentAuthor.getId().getValue());
255255

module-domain/src/main/java/com/wsws/moduledomain/feed/like/LikeRepository.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ public interface LikeRepository {
2323
*/
2424
List<Like> findByUserId(String userId);
2525

26+
/**
27+
* 특정 사용자가 특정 글에 좋아요를 눌렀는지
28+
*/
29+
boolean existsByUserEntityIdAndTargetIdAndTargetType(String userId, Long targetId, TargetType targetType);
2630
/**
2731
* 데이터베이스 즉시 반영하기
2832
*/

module-infra/src/main/java/com/wsws/moduleinfra/like/JpaLikeUserRepository.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,12 @@ public interface JpaLikeUserRepository extends JpaRepository<LikeEntity, Long> {
2525
/**
2626
* 특정 사용자가 누른 글
2727
*/
28-
@Query("SELECT l FROM LikeEntity l WHERE l.userEntity.id = :userId")
29-
List<LikeEntity> findByUserId(String userId);
28+
List<LikeEntity> findByUserEntityId(String userId);
29+
30+
/**
31+
* 특정 사용자가 특정 글에 좋아요를 눌렀는지
32+
*/
33+
boolean existsByUserEntityIdAndTargetIdAndTargetType(String userId, Long targetId, TargetType targetType);
3034

3135
@Override
3236
void flush();

module-infra/src/main/java/com/wsws/moduleinfra/like/LikeRepositoryImpl.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,18 @@ public void deleteByTargetIdAndUserId(Long targetId, String userId) {
4141

4242
@Override
4343
public List<Like> findByUserId(String userId) {
44-
return jpaLikeUserRepository.findByUserId(userId).stream()
44+
return jpaLikeUserRepository.findByUserEntityId(userId).stream()
4545
.map(LikeEntityMapper::toDomain)
4646
.toList();
4747
}
4848

49+
@Override
50+
public boolean existsByUserEntityIdAndTargetIdAndTargetType(String userId, Long targetId, TargetType targetType) {
51+
return jpaLikeUserRepository
52+
.existsByUserEntityIdAndTargetIdAndTargetType(userId, targetId, targetType);
53+
}
54+
55+
4956
@Override
5057
public void flush() {
5158
jpaLikeUserRepository.flush();

module-infra/src/main/java/com/wsws/moduleinfra/repo/feed/AnswerCommentRepositoryImpl.java

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -54,14 +54,13 @@ public int countParentCommentByAnswerId(Long answerId) {
5454
public AnswerComment save(AnswerComment answerComment) {
5555
AnswerCommentEntity answerCommentEntity = AnswerCommentEntityMapper.toEntity(answerComment);
5656

57-
AnswerEntity answerEntity = jpaAnswerRepository.findById(answerComment.getAnswerId().getValue()).orElse(null);
58-
answerCommentEntity.setAnswerEntity(answerEntity); // AnswerEntity 연관관계 설정
57+
jpaAnswerRepository.findById(answerComment.getAnswerId().getValue())
58+
.ifPresent(answerCommentEntity::setAnswerEntity);
5959

60+
// 부모 댓글이 있는 경우
6061
if (answerComment.getParentAnswerCommentId().getValue() != null) { // NPE 방지
61-
AnswerCommentEntity parentCommentEntity =
62-
jpaAnswerCommentRepository.findById(answerComment.getParentAnswerCommentId().getValue())
63-
.orElse(null);
64-
answerCommentEntity.setParentCommentEntity(parentCommentEntity);
62+
jpaAnswerCommentRepository.findById(answerComment.getParentAnswerCommentId().getValue())
63+
.ifPresent(answerCommentEntity::setParentCommentEntity);
6564
}
6665

6766
return AnswerCommentEntityMapper.toDomain(jpaAnswerCommentRepository.save(answerCommentEntity));

module-infra/src/main/java/com/wsws/moduleinfra/repo/feed/AnswerRepositoryImpl.java

Lines changed: 80 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
package com.wsws.moduleinfra.repo.feed;
22

3+
import com.querydsl.core.BooleanBuilder;
4+
import com.querydsl.core.types.dsl.BooleanExpression;
5+
import com.querydsl.jpa.impl.JPAQueryFactory;
36
import com.wsws.moduledomain.feed.answer.Answer;
47
import com.wsws.moduledomain.feed.answer.repo.AnswerRepository;
58
import com.wsws.moduledomain.feed.dto.AnswerQuestionDTO;
9+
import com.wsws.moduledomain.feed.question.vo.QuestionStatus;
610
import com.wsws.moduleinfra.entity.feed.AnswerEntity;
711
import com.wsws.moduleinfra.entity.feed.QuestionEntity;
812
import com.wsws.moduleinfra.entity.feed.mapper.AnswerEntityMapper;
@@ -11,17 +15,25 @@
1115
import org.springframework.data.domain.Pageable;
1216
import org.springframework.stereotype.Repository;
1317
import org.springframework.transaction.annotation.Transactional;
18+
import org.springframework.util.StringUtils;
1419

1520
import java.time.LocalDateTime;
1621
import java.util.List;
1722
import java.util.Optional;
23+
import java.util.function.Supplier;
24+
25+
import static com.wsws.moduledomain.feed.question.vo.QuestionStatus.ACTIVATED;
26+
import static com.wsws.moduleinfra.entity.feed.QAnswerEntity.answerEntity;
27+
import static com.wsws.moduleinfra.entity.feed.QQuestionEntity.questionEntity;
28+
import static org.springframework.util.StringUtils.hasText;
1829

1930
@Repository
2031
@RequiredArgsConstructor
2132
public class AnswerRepositoryImpl implements AnswerRepository {
2233

2334
private final JpaAnswerRepository jpaAnswerRepository;
2435
private final JpaQuestionRepository jpaQuestionRepository;
36+
private final JPAQueryFactory queryFactory;
2537

2638
/**
2739
* 답변을 Id를 기준으로 찾기
@@ -41,35 +53,54 @@ public Optional<Answer> findByIdWithLock(Long id) {
4153

4254
@Override
4355
public List<Answer> findAllByCategoryIdWithCursor(LocalDateTime cursor, int size, Long categoryId) {
44-
Pageable pageable = PageRequest.of(0, size); // 가져올 데이터 갯수 설정
45-
return categoryId == null
46-
?
47-
jpaAnswerRepository.findAllWithCursor(cursor, pageable).stream() // categoryId가 없다면 전체 조회
48-
.map(AnswerEntityMapper::toDomain)
49-
.toList()
50-
:jpaAnswerRepository.findAllByCategoryIdWithCursor(cursor, pageable, categoryId).stream() // categoryId가 있다면 해당 카테고리로 조회
56+
List<AnswerEntity> answerEntities = queryFactory
57+
.select(answerEntity)
58+
.from(answerEntity)
59+
.join(answerEntity.questionEntity, questionEntity)
60+
.where(
61+
categoryIdEq(categoryId),
62+
questionEntity.questionStatus.eq(ACTIVATED),
63+
answerEntity.createdAt.lt(cursor)
64+
).orderBy(answerEntity.createdAt.desc())
65+
.offset(0)
66+
.limit(size)
67+
.fetch();
68+
69+
return answerEntities.stream()
5170
.map(AnswerEntityMapper::toDomain)
5271
.toList();
5372
}
5473

5574
@Override
5675
public List<AnswerQuestionDTO> findAllByUserIdWithCursor(
5776
String userId, LocalDateTime cursor, int size, boolean isMine) {
58-
Pageable pageable = PageRequest.of(0, size); // 가져올 데이터 갯수 설정
59-
List<AnswerEntity> answerEntities = isMine
60-
? jpaAnswerRepository.findAllByUserIdWithCursor(userId, cursor, pageable)
61-
: jpaAnswerRepository.findAllByUserIdAndVisibilityTrueWithCursor(userId, cursor, pageable);
77+
78+
List<AnswerEntity> answerEntities = queryFactory
79+
.selectFrom(answerEntity)
80+
.join(answerEntity.questionEntity, questionEntity).fetchJoin()
81+
.where(
82+
visibilityEqTrue(isMine),
83+
answerEntity.userId.eq(userId),
84+
answerEntity.createdAt.lt(cursor)
85+
).orderBy(answerEntity.createdAt.desc())
86+
.offset(0)
87+
.limit(size)
88+
.fetch();
6289

6390
return answerEntities.stream()
6491
.map(AnswerEntityMapper::toJoinDto)
6592
.toList();
6693
}
6794

68-
6995
@Override
7096
public Long countByUserId(String userId, boolean isMine) {
71-
return isMine ? jpaAnswerRepository.countByUserId(userId) // 요청한 사용자의 질문이면 모든 Answer
72-
: jpaAnswerRepository.countByUserIdAndVisibilityTrue(userId); // 요청한 사용자의 질문이 아니면 visibility가 true인 Answer만
97+
return queryFactory
98+
.select(answerEntity.count())
99+
.from(answerEntity)
100+
.where(
101+
visibilityEqTrue(isMine),
102+
answerEntity.userId.eq(userId)
103+
).fetchFirst();
73104
}
74105

75106
@Override
@@ -78,18 +109,25 @@ public Optional<Answer> findAnswerByUserIdAndQuestionId(String userId, Long ques
78109
.map(AnswerEntityMapper::toDomain);
79110
}
80111

112+
// TODO: 동적 쿼리 Querydsl로 수정
81113
@Override
82114
public List<Answer> findAnswersByLikeCountAndCategoryIdWithCursor(Long categoryId, int limit) {
83-
Pageable pageable = PageRequest.of(0, limit); // 가져올 데이터 갯수 설정
84-
return categoryId == null
85-
? jpaAnswerRepository.findAllOrderByLikeCountDescWithCursor(pageable).stream() // categoryId가 없다면 전체 조회
86-
.map(AnswerEntityMapper::toDomain)
87-
.toList()
88-
: jpaAnswerRepository.findAllByCategoryIdOrderByLikeCountDescWithCursor(categoryId, pageable).stream() // categoryId가 있다면 해당 categoryId로 조회
115+
116+
List<AnswerEntity> answerEntities = queryFactory
117+
.selectFrom(answerEntity)
118+
.join(answerEntity.questionEntity, questionEntity)
119+
.where(
120+
categoryIdEq(categoryId),
121+
questionEntity.questionStatus.eq(ACTIVATED)
122+
).orderBy(answerEntity.likeCount.desc())
123+
.offset(0)
124+
.limit(limit)
125+
.fetch();
126+
127+
return answerEntities.stream()
89128
.map(AnswerEntityMapper::toDomain)
90129
.toList();
91130
}
92-
93131
/**
94132
* 답변 저장
95133
*/
@@ -98,8 +136,8 @@ public List<Answer> findAnswersByLikeCountAndCategoryIdWithCursor(Long categoryI
98136
public Answer save(Answer answer) {
99137
AnswerEntity answerEntity = AnswerEntityMapper.toEntity(answer);
100138

101-
QuestionEntity questionEntity = jpaQuestionRepository.findById(answer.getQuestionId().getValue()).orElse(null);
102-
answerEntity.setQuestionEntity(questionEntity); // Quesiton 연관관계 설정
139+
jpaQuestionRepository.findById(answer.getQuestionId().getValue())
140+
.ifPresent(answerEntity::setQuestionEntity); // Quesiton 연관관계 설정
103141

104142
AnswerEntity savedEntity = jpaAnswerRepository.save(answerEntity);// Answer를 엔티티로 변환하여 저장
105143
return AnswerEntityMapper.toDomain(savedEntity);
@@ -129,4 +167,23 @@ public void edit(Answer answer) {
129167
public void deleteById(Long id) {
130168
jpaAnswerRepository.deleteById(id);
131169
}
170+
171+
private BooleanBuilder categoryIdEq(Long categoryId) {
172+
return nullSafeBuilder(() -> questionEntity.categoryId.eq(categoryId), categoryId);
173+
}
174+
175+
private BooleanBuilder visibilityEqTrue(boolean isMine) {
176+
return nullSafeBuilder(() -> answerEntity.visibility.eq(true), null);
177+
}
178+
179+
private <T> BooleanBuilder nullSafeBuilder(Supplier<BooleanExpression> f, T value) {
180+
if(value instanceof String && !hasText((String) value)) {
181+
return new BooleanBuilder();
182+
}
183+
try {
184+
return new BooleanBuilder(f.get());
185+
} catch (Exception e) {
186+
return new BooleanBuilder();
187+
}
188+
}
132189
}

module-infra/src/main/java/com/wsws/moduleinfra/repo/feed/JpaAnswerRepository.java

Lines changed: 0 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,11 @@
22

33
import com.wsws.moduleinfra.entity.feed.AnswerEntity;
44
import jakarta.persistence.LockModeType;
5-
import org.springframework.data.domain.Pageable;
65
import org.springframework.data.jpa.repository.JpaRepository;
76
import org.springframework.data.jpa.repository.Lock;
87
import org.springframework.data.jpa.repository.Query;
98
import org.springframework.stereotype.Repository;
109

11-
import java.time.LocalDateTime;
12-
import java.util.List;
1310
import java.util.Optional;
1411

1512
@Repository
@@ -20,34 +17,6 @@ public interface JpaAnswerRepository extends JpaRepository<AnswerEntity, Long> {
2017
@Query("SELECT a FROM AnswerEntity a WHERE a.id = :id")
2118
Optional<AnswerEntity> findByIdWithLock(Long id);
2219

23-
// 카테고리 상관없이 답변 찾기
24-
@Query("""
25-
SELECT a
26-
FROM AnswerEntity a join a.questionEntity q
27-
WHERE q.questionStatus = 'ACTIVATED'
28-
AND a.createdAt < :answerCursor
29-
ORDER BY a.createdAt DESC
30-
""")
31-
List<AnswerEntity> findAllWithCursor(LocalDateTime answerCursor, Pageable pageable);
32-
33-
// 특정 categoryId의 답변 찾기
34-
@Query("""
35-
SELECT a
36-
FROM AnswerEntity a join a.questionEntity q
37-
WHERE q.questionStatus = 'ACTIVATED'
38-
AND q.categoryId = :categoryId
39-
AND a.createdAt < :answerCursor
40-
ORDER BY a.createdAt DESC
41-
""")
42-
List<AnswerEntity> findAllByCategoryIdWithCursor(LocalDateTime answerCursor, Pageable pageable, Long categoryId);
43-
44-
45-
// 특정 userId를 가진 답변의 갯수
46-
Long countByUserId(String userId);
47-
48-
// 특정 userId를 가지고 visibility가 true인 답변의 갯수
49-
Long countByUserIdAndVisibilityTrue(String userId);
50-
5120
// 특정 사용자의 특정 질문에 대한 답변
5221
@Query("""
5322
SELECT a
@@ -57,50 +26,7 @@ public interface JpaAnswerRepository extends JpaRepository<AnswerEntity, Long> {
5726
""")
5827
Optional<AnswerEntity> findAnswerByUserIdAndQuestionId(String userId, Long questionId);
5928

60-
61-
@Query("""
62-
SELECT a
63-
FROM AnswerEntity a join fetch a.questionEntity q
64-
WHERE a.userId = :userId
65-
AND a.createdAt < :answerCursor
66-
ORDER BY a.createdAt DESC
67-
""")
68-
List<AnswerEntity> findAllByUserIdWithCursor(String userId, LocalDateTime answerCursor, Pageable pageable);
69-
70-
@Query("""
71-
SELECT a
72-
FROM AnswerEntity a join fetch a.questionEntity q
73-
WHERE a.userId = :userId
74-
AND a.createdAt < :answerCursor
75-
AND a.visibility = true
76-
ORDER BY a.createdAt DESC
77-
""")
78-
List<AnswerEntity> findAllByUserIdAndVisibilityTrueWithCursor(String userId, LocalDateTime answerCursor, Pageable pageable);
79-
8029
@Query("SELECT COUNT(a) > 0 FROM AnswerEntity a WHERE a.userId = :userId AND a.questionEntity.id = :questionId")
8130
boolean existsByUserIdAndQuestionId(String userId, Long questionId);
8231

83-
/**
84-
* 좋아요 수가 많은 인기답변 n개 (카테고리 O)
85-
*/
86-
@Query("""
87-
SELECT a
88-
FROM AnswerEntity a join a.questionEntity q
89-
WHERE q.categoryId = :categoryId
90-
AND q.questionStatus = 'ACTIVATED'
91-
ORDER BY a.likeCount DESC
92-
""")
93-
List<AnswerEntity> findAllByCategoryIdOrderByLikeCountDescWithCursor(Long categoryId, Pageable pageable);
94-
95-
/**
96-
* 좋아요 수가 많은 인기답변 n개 (카테고리 X)
97-
*/
98-
@Query("""
99-
SELECT a
100-
FROM AnswerEntity a join a.questionEntity q
101-
WHERE q.questionStatus = 'ACTIVATED'
102-
ORDER BY a.likeCount DESC
103-
""")
104-
List<AnswerEntity> findAllOrderByLikeCountDescWithCursor(Pageable pageable);
105-
10632
}

0 commit comments

Comments
 (0)