From e872b07bce59d135858d75eed6f333fcb6b13d13 Mon Sep 17 00:00:00 2001 From: Jeong Wonju Date: Mon, 9 Dec 2024 14:59:44 +0900 Subject: [PATCH] =?UTF-8?q?MATE-103=20:=20[REFACTOR]=20=ED=9A=8C=EC=9B=90?= =?UTF-8?q?=20=ED=94=84=EB=A1=9C=ED=95=84=20=ED=83=80=EC=9E=84=EB=9D=BC?= =?UTF-8?q?=EC=9D=B8=20=EC=A1=B0=ED=9A=8C=20=EC=84=B1=EB=8A=A5=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0=20(#112)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * MATE-103 : [REFACTOR] QueryDSL을 활용하여 가져오던 참여 직관 목록, 경기 정보를 VisitRepository JPQL로 수정 * MATE-103 : [REFACTOR] 타임라인 조회 SQL 쿼리 및 성능 개선 * MATE-103 : [TEST] 타임라인 조회 서비스 테스트 --- .../mate/domain/match/entity/Match.java | 17 ++--- .../match/repository/MatchRepository.java | 3 + .../mate/repository/MateRepository.java | 9 +-- .../mate/repository/MateReviewRepository.java | 16 ++++- .../mate/repository/VisitPartRepository.java | 1 + .../mate/repository/VisitRepository.java | 28 ++++++++ .../repository/TimelineRepositoryCustom.java | 10 --- .../TimelineRepositoryCustomImpl.java | 51 -------------- .../domain/member/service/ProfileService.java | 60 +++++++++------- .../member/service/ProfileServiceTest.java | 68 +++++++++++++------ 10 files changed, 134 insertions(+), 129 deletions(-) delete mode 100644 src/main/java/com/example/mate/domain/member/repository/TimelineRepositoryCustom.java delete mode 100644 src/main/java/com/example/mate/domain/member/repository/TimelineRepositoryCustomImpl.java diff --git a/src/main/java/com/example/mate/domain/match/entity/Match.java b/src/main/java/com/example/mate/domain/match/entity/Match.java index 4d393433..0705a9af 100644 --- a/src/main/java/com/example/mate/domain/match/entity/Match.java +++ b/src/main/java/com/example/mate/domain/match/entity/Match.java @@ -1,23 +1,14 @@ package com.example.mate.domain.match.entity; import com.example.mate.domain.constant.StadiumInfo; -import jakarta.persistence.CascadeType; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.EnumType; -import jakarta.persistence.Enumerated; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.OneToOne; -import jakarta.persistence.Table; -import java.time.LocalDateTime; +import jakarta.persistence.*; import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import java.time.LocalDateTime; + @Entity @Table(name = "`match`") // 테이블 이름을 backtick(`)으로 감싸서 사용 @Getter @@ -48,7 +39,7 @@ public class Match { private Integer homeScore; private Integer awayScore; - @OneToOne(cascade = CascadeType.ALL, orphanRemoval = true) + @OneToOne(cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY) @JoinColumn(name = "weather_id") private Weather weather; diff --git a/src/main/java/com/example/mate/domain/match/repository/MatchRepository.java b/src/main/java/com/example/mate/domain/match/repository/MatchRepository.java index 64552765..10dac76b 100644 --- a/src/main/java/com/example/mate/domain/match/repository/MatchRepository.java +++ b/src/main/java/com/example/mate/domain/match/repository/MatchRepository.java @@ -14,7 +14,9 @@ @Repository public interface MatchRepository extends JpaRepository { List findTop5ByOrderByMatchTimeDesc(); + List findTop3ByHomeTeamIdOrAwayTeamIdOrderByMatchTimeDesc(Long homeTeamId, Long awayTeamId); + @Query("SELECT m FROM Match m " + "WHERE (m.status = :status1 AND m.homeTeamId = :homeTeamId) " + "OR (m.status = :status2 AND m.awayTeamId = :awayTeamId) " + @@ -26,6 +28,7 @@ List findRecentCompletedMatches( @Param("status2") MatchStatus status2, @Param("awayTeamId") Long awayTeamId ); + @Query("SELECT m FROM Match m WHERE (m.homeTeamId = :teamId OR m.awayTeamId = :teamId) " + "AND m.matchTime BETWEEN :startDate AND :endDate " + "AND m.status = 'SCHEDULED' " + diff --git a/src/main/java/com/example/mate/domain/mate/repository/MateRepository.java b/src/main/java/com/example/mate/domain/mate/repository/MateRepository.java index 7fcd36b7..6937d4c1 100644 --- a/src/main/java/com/example/mate/domain/mate/repository/MateRepository.java +++ b/src/main/java/com/example/mate/domain/mate/repository/MateRepository.java @@ -1,16 +1,16 @@ package com.example.mate.domain.mate.repository; -import com.example.mate.domain.match.entity.Match; import com.example.mate.domain.mate.entity.MatePost; import com.example.mate.domain.mate.entity.Status; -import java.time.LocalDateTime; -import java.util.List; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; +import java.time.LocalDateTime; +import java.util.List; + @Repository public interface MateRepository extends JpaRepository, MateRepositoryCustom { @Query(""" @@ -21,7 +21,4 @@ public interface MateRepository extends JpaRepository, MateRepos """) List findMainPagePosts(@Param("teamId") Long teamId, @Param("now") LocalDateTime now, @Param("statuses") List statuses, Pageable pageable); - - @Query("SELECT m.match FROM MatePost m WHERE m.id = :matePostId") - Match findMatchByMatePostId(@Param("matePostId") Long matePostId); } diff --git a/src/main/java/com/example/mate/domain/mate/repository/MateReviewRepository.java b/src/main/java/com/example/mate/domain/mate/repository/MateReviewRepository.java index 544d0c78..1d3d8998 100644 --- a/src/main/java/com/example/mate/domain/mate/repository/MateReviewRepository.java +++ b/src/main/java/com/example/mate/domain/mate/repository/MateReviewRepository.java @@ -1,13 +1,23 @@ package com.example.mate.domain.mate.repository; import com.example.mate.domain.mate.entity.MateReview; -import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; public interface MateReviewRepository extends JpaRepository { int countByRevieweeId(Long revieweeId); - Optional findMateReviewByVisitIdAndReviewerIdAndRevieweeId(Long visitId, Long reviewerId, - Long revieweeId); + @Query(""" + SELECT mr + FROM MateReview mr + WHERE mr.visit.id = :visitId + AND mr.reviewer.id = :reviewerId + ORDER BY mr.reviewee.id ASC + """) + List findMateReviewsByVisitIdAndReviewerId(@Param("visitId") Long visitId, + @Param("reviewerId") Long reviewerId); } diff --git a/src/main/java/com/example/mate/domain/mate/repository/VisitPartRepository.java b/src/main/java/com/example/mate/domain/mate/repository/VisitPartRepository.java index 18108d5d..7513705f 100644 --- a/src/main/java/com/example/mate/domain/mate/repository/VisitPartRepository.java +++ b/src/main/java/com/example/mate/domain/mate/repository/VisitPartRepository.java @@ -18,6 +18,7 @@ public interface VisitPartRepository extends JpaRepository findMembersByVisitIdExcludeMember(@Param("visitId") Long visitId, @Param("memberId") Long memberId); diff --git a/src/main/java/com/example/mate/domain/mate/repository/VisitRepository.java b/src/main/java/com/example/mate/domain/mate/repository/VisitRepository.java index 82bef2ee..3bf195b4 100644 --- a/src/main/java/com/example/mate/domain/mate/repository/VisitRepository.java +++ b/src/main/java/com/example/mate/domain/mate/repository/VisitRepository.java @@ -1,7 +1,35 @@ package com.example.mate.domain.mate.repository; +import com.example.mate.domain.match.entity.Match; import com.example.mate.domain.mate.entity.Visit; +import com.example.mate.domain.member.dto.response.MyTimelineResponse; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; public interface VisitRepository extends JpaRepository { + + @Query(""" + SELECT new com.example.mate.domain.member.dto.response.MyTimelineResponse(v.id, v.post.id, vp.member.id) + FROM Visit v + JOIN v.participants vp + WHERE vp.member.id = :memberId + ORDER BY v.id DESC + """) + Page findVisitsByMemberId(@Param("memberId") Long memberId, Pageable pageable); + + @Query(""" + SELECT m + FROM Visit v + JOIN v.post mp + JOIN mp.match m + JOIN v.participants vp + WHERE vp.member.id = :memberId + ORDER BY v.id DESC + """) + List findMatchesByMemberId(@Param("memberId") Long memberId); } diff --git a/src/main/java/com/example/mate/domain/member/repository/TimelineRepositoryCustom.java b/src/main/java/com/example/mate/domain/member/repository/TimelineRepositoryCustom.java deleted file mode 100644 index 9203ad08..00000000 --- a/src/main/java/com/example/mate/domain/member/repository/TimelineRepositoryCustom.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.example.mate.domain.member.repository; - -import com.example.mate.domain.member.dto.response.MyTimelineResponse; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; - -public interface TimelineRepositoryCustom { - - Page findVisitsById(Long memberId, Pageable pageable); -} diff --git a/src/main/java/com/example/mate/domain/member/repository/TimelineRepositoryCustomImpl.java b/src/main/java/com/example/mate/domain/member/repository/TimelineRepositoryCustomImpl.java deleted file mode 100644 index a8104133..00000000 --- a/src/main/java/com/example/mate/domain/member/repository/TimelineRepositoryCustomImpl.java +++ /dev/null @@ -1,51 +0,0 @@ -package com.example.mate.domain.member.repository; - -import static com.example.mate.domain.mate.entity.QVisit.visit; -import static com.example.mate.domain.mate.entity.QVisitPart.visitPart; -import static com.example.mate.domain.member.entity.QMember.member; - -import com.example.mate.domain.member.dto.response.MyTimelineResponse; -import com.querydsl.core.types.Projections; -import com.querydsl.jpa.impl.JPAQuery; -import com.querydsl.jpa.impl.JPAQueryFactory; -import java.util.List; -import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.data.support.PageableExecutionUtils; -import org.springframework.stereotype.Repository; - -@Repository -@RequiredArgsConstructor -public class TimelineRepositoryCustomImpl implements TimelineRepositoryCustom { - - private final JPAQueryFactory queryFactory; - - @Override - public Page findVisitsById(Long memberId, Pageable pageable) { - List results = queryFactory - .select(Projections.constructor( - MyTimelineResponse.class, - visit.id.as("visitId"), - visit.post.id.as("matePostId"), - visitPart.member.id.as("memberId") - )) - .from(visit) - .join(visit.participants, visitPart) // Visit -> VisitPart 조인 - .join(visitPart.member, member) // VisitPart -> Member 조인 - .where(member.id.eq(memberId)) - .orderBy(visit.id.desc()) - .offset(pageable.getOffset()) - .limit(pageable.getPageSize()) - .fetch(); - - JPAQuery total = queryFactory - .select(visit.count()) - .from(visit) - .join(visit.participants, visitPart) - .join(visitPart.member, member) - .where(member.id.eq(memberId)); - - return PageableExecutionUtils.getPage(results, pageable, total::fetchOne); - } -} diff --git a/src/main/java/com/example/mate/domain/member/service/ProfileService.java b/src/main/java/com/example/mate/domain/member/service/ProfileService.java index 0df2fe3e..6c3e237b 100644 --- a/src/main/java/com/example/mate/domain/member/service/ProfileService.java +++ b/src/main/java/com/example/mate/domain/member/service/ProfileService.java @@ -9,10 +9,10 @@ import com.example.mate.domain.goods.repository.GoodsReviewRepositoryCustom; import com.example.mate.domain.match.entity.Match; import com.example.mate.domain.mate.entity.MateReview; -import com.example.mate.domain.mate.repository.MateRepository; import com.example.mate.domain.mate.repository.MateReviewRepository; import com.example.mate.domain.mate.repository.MateReviewRepositoryCustom; import com.example.mate.domain.mate.repository.VisitPartRepository; +import com.example.mate.domain.mate.repository.VisitRepository; import com.example.mate.domain.member.dto.response.MyGoodsRecordResponse; import com.example.mate.domain.member.dto.response.MyReviewResponse; import com.example.mate.domain.member.dto.response.MyTimelineResponse; @@ -20,16 +20,17 @@ import com.example.mate.domain.member.dto.response.MyVisitResponse.MateReviewResponse; import com.example.mate.domain.member.entity.Member; import com.example.mate.domain.member.repository.MemberRepository; -import com.example.mate.domain.member.repository.TimelineRepositoryCustom; -import java.util.List; -import java.util.Optional; -import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + @Service @Transactional @RequiredArgsConstructor @@ -39,10 +40,9 @@ public class ProfileService { private final GoodsPostRepository goodsPostRepository; private final MateReviewRepositoryCustom mateReviewRepositoryCustom; private final GoodsReviewRepositoryCustom goodsReviewRepositoryCustom; - private final TimelineRepositoryCustom timelineRepositoryCustom; - private final MateRepository mateRepository; private final VisitPartRepository visitPartRepository; private final MateReviewRepository mateReviewRepository; + private final VisitRepository visitRepository; // 굿즈 판매기록 페이징 조회 @Transactional(readOnly = true) @@ -104,42 +104,52 @@ public PageResponse getGoodsReviewPage(Long memberId, Pageable return PageResponse.from(goodsReviewPage); } - // TODO : 쿼리가 너무 많이 나오는 문제 -> 멘토링 및 리팩토링 필요 // 직관 타임라인 페이징 조회 @Transactional(readOnly = true) public PageResponse getMyVisitPage(Long memberId, Pageable pageable) { validateMemberId(memberId); - // 회원이 참여한 직관을 페이징하여 가져오기 - Page visitsByIdPage = timelineRepositoryCustom.findVisitsById(memberId, pageable); + // 회원이 참여한 직관 목록을 페이지네이션 + Page visitsByMemberIdPage = visitRepository.findVisitsByMemberId(memberId, pageable); - // 응답 객체 생성 - List responses = visitsByIdPage.getContent().stream() - .map(response -> createVisitResponse(response, memberId)) - .collect(Collectors.toList()); + // 회원이 참여한 경기 정보 리스트 + List matchesByMatePostId = visitRepository.findMatchesByMemberId(memberId); + + // 각각의 직관 목록과 경기 정보에 따른 응답 객체 생성 및 주입 + List responses = new ArrayList<>(); + for (int i = 0; i < visitsByMemberIdPage.getContent().size(); i++) { + MyTimelineResponse response = visitsByMemberIdPage.getContent().get(i); + Match match = matchesByMatePostId.get(i); + responses.add(createVisitResponse(response, memberId, match)); + } - // 페이징 정보 처리 - return createPageResponse(visitsByIdPage, responses, pageable); + // 페이지네이션 + return createPageResponse(visitsByMemberIdPage, responses, pageable); } - private MyVisitResponse createVisitResponse(MyTimelineResponse response, Long memberId) { - // 경기 정보 가져오기 - Match match = mateRepository.findMatchByMatePostId(response.getMatePostId()); + private MyVisitResponse createVisitResponse(MyTimelineResponse response, Long memberId, Match match) { + // 회원이 참여한 직관에서 메이트에 남긴 모든 리뷰 리스트 + List existReviews = mateReviewRepository + .findMateReviewsByVisitIdAndReviewerId(response.getVisitId(), memberId); - // 회원 본인을 제외한 직관 참여 리스트 가져오기 + // 회원 본인을 제외한 직관 참여 메이트 리스트 List mates = visitPartRepository.findMembersByVisitIdExcludeMember(response.getVisitId(), memberId); - // 각 메이트에 대한 리뷰 생성 - List reviews = createMateReviews(response, mates, memberId); + // 각 메이트에 대한 리뷰 여부에 따른 응답 리뷰 리스트 + List reviews = createMateReviews(response, mates, memberId, existReviews); return MyVisitResponse.of(match, reviews, response.getMatePostId()); } - private List createMateReviews(MyTimelineResponse response, List mates, Long memberId) { + private List createMateReviews(MyTimelineResponse response, List mates, + Long memberId, List existReviews) { return mates.stream() .map(mate -> { - Optional mateReview = mateReviewRepository.findMateReviewByVisitIdAndReviewerIdAndRevieweeId( - response.getVisitId(), memberId, mate.getId()); + Optional mateReview = existReviews.stream() + .filter(review -> review.getVisit().getId().equals(response.getVisitId()) && + review.getReviewer().getId().equals(memberId) && + review.getReviewee().getId().equals(mate.getId())) + .findFirst(); // 해당 조건에 맞는 리뷰를 찾기 return mateReview.map(MateReviewResponse::from) // 해당 mate에 대한 리뷰가 있으면 리뷰 채워서 반환 .orElseGet(() -> MateReviewResponse.from(mate)); // 리뷰가 없으면 rating, content = null }) diff --git a/src/test/java/com/example/mate/domain/member/service/ProfileServiceTest.java b/src/test/java/com/example/mate/domain/member/service/ProfileServiceTest.java index 4133a195..a1dd901a 100644 --- a/src/test/java/com/example/mate/domain/member/service/ProfileServiceTest.java +++ b/src/test/java/com/example/mate/domain/member/service/ProfileServiceTest.java @@ -1,11 +1,5 @@ package com.example.mate.domain.member.service; -import static com.example.mate.common.error.ErrorCode.MEMBER_NOT_FOUND_BY_ID; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.verify; - import com.example.mate.common.error.CustomException; import com.example.mate.common.response.PageResponse; import com.example.mate.domain.constant.Gender; @@ -20,10 +14,11 @@ import com.example.mate.domain.goods.repository.GoodsReviewRepositoryCustom; import com.example.mate.domain.match.entity.Match; import com.example.mate.domain.mate.entity.MateReview; -import com.example.mate.domain.mate.repository.MateRepository; +import com.example.mate.domain.mate.entity.Visit; import com.example.mate.domain.mate.repository.MateReviewRepository; import com.example.mate.domain.mate.repository.MateReviewRepositoryCustom; import com.example.mate.domain.mate.repository.VisitPartRepository; +import com.example.mate.domain.mate.repository.VisitRepository; import com.example.mate.domain.member.dto.response.MyGoodsRecordResponse; import com.example.mate.domain.member.dto.response.MyReviewResponse; import com.example.mate.domain.member.dto.response.MyTimelineResponse; @@ -31,10 +26,6 @@ import com.example.mate.domain.member.dto.response.MyVisitResponse.MateReviewResponse; import com.example.mate.domain.member.entity.Member; import com.example.mate.domain.member.repository.MemberRepository; -import com.example.mate.domain.member.repository.TimelineRepositoryCustom; -import java.time.LocalDateTime; -import java.util.List; -import java.util.Optional; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; @@ -48,6 +39,16 @@ import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +import static com.example.mate.common.error.ErrorCode.MEMBER_NOT_FOUND_BY_ID; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.verify; + @ExtendWith(MockitoExtension.class) class ProfileServiceTest { @@ -66,18 +67,15 @@ class ProfileServiceTest { @Mock private GoodsReviewRepositoryCustom goodsReviewRepositoryCustom; - @Mock - private TimelineRepositoryCustom timelineRepositoryCustom; - - @Mock - private MateRepository mateRepository; - @Mock private VisitPartRepository visitPartRepository; @Mock private MateReviewRepository mateReviewRepository; + @Mock + private VisitRepository visitRepository; + private Member member1; private Member member2; private Member member3; @@ -402,13 +400,41 @@ void get_my_visit_page_success() { List myTimelineResponseList = List.of( new MyTimelineResponse(1L, 1L, 1L) ); - Page visitsByIdPage = new PageImpl<>(myTimelineResponseList, pageable, - myTimelineResponseList.size()); - given(timelineRepositoryCustom.findVisitsById(memberId, pageable)).willReturn(visitsByIdPage); + Page visitsByIdPage = new PageImpl<>( + myTimelineResponseList, pageable, myTimelineResponseList.size()); + given(visitRepository.findVisitsByMemberId(memberId, pageable)).willReturn(visitsByIdPage); + + // match mock 설정 + List matchesByMatePostId = List.of(Match.builder() + .id(1L) + .homeTeamId(1L) + .awayTeamId(2L) + .stadiumId(1L) + .build()); + given(visitRepository.findMatchesByMemberId(memberId)).willReturn(matchesByMatePostId); // mateRepository의 mock 설정 Match match = Match.builder().homeTeamId(1L).awayTeamId(2L).stadiumId(1L).build(); - given(mateRepository.findMatchByMatePostId(1L)).willReturn(match); + + Visit visit = Visit.builder().id(1L).build(); + + List existReviews = List.of( + MateReview.builder() + .id(1L) + .visit(visit) + .reviewer(member1) + .reviewee(member2) + .rating(Rating.GOOD) + .build(), + MateReview.builder() + .id(2L) + .visit(visit) + .reviewer(member1) + .reviewee(member3) + .rating(Rating.GOOD) + .build()); + given(mateReviewRepository.findMateReviewsByVisitIdAndReviewerId(1L, 1L)) + .willReturn(existReviews); // visitPartRepository의 mock 설정 given(visitPartRepository.findMembersByVisitIdExcludeMember(1L, memberId)).willReturn(