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

좋아요 수 및 좋아요 변경 이력 캐싱 #752

Merged
merged 8 commits into from
Nov 20, 2023
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,14 @@
import hanglog.community.dto.response.CommunityTripResponse;
import hanglog.community.dto.response.RecommendTripListResponse;
import hanglog.global.exception.BadRequestException;
import hanglog.like.domain.LikeCount;
import hanglog.like.domain.LikeInfo;
import hanglog.like.domain.MemberLike;
import hanglog.like.dto.LikeElement;
import hanglog.like.dto.LikeElements;
import hanglog.like.repository.LikeRepository;
import hanglog.like.domain.repository.LikeCountRepository;
import hanglog.like.domain.repository.LikeRepository;
import hanglog.like.domain.repository.MemberLikeRepository;
import hanglog.trip.domain.Trip;
import hanglog.trip.domain.repository.TripCityRepository;
import hanglog.trip.domain.repository.TripRepository;
Expand All @@ -25,6 +30,7 @@
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Pageable;
Expand All @@ -45,6 +51,8 @@ public class CommunityService {
private final CityRepository cityRepository;
private final RecommendStrategies recommendStrategies;
private final PublishedTripRepository publishedTripRepository;
private final LikeCountRepository likeCountRepository;
private final MemberLikeRepository memberLikeRepository;

@Transactional(readOnly = true)
public CommunityTripListResponse getCommunityTripsByPage(final Accessor accessor, final Pageable pageable) {
Expand Down Expand Up @@ -119,20 +127,31 @@ public TripDetailResponse getTripDetail(final Accessor accessor, final Long trip
final LocalDateTime publishedDate = publishedTripRepository.findByTripId(tripId)
.orElseThrow(() -> new BadRequestException(NOT_FOUND_TRIP_ID))
.getCreatedAt();
final LikeElements likeElements = new LikeElements(likeRepository.findLikeCountAndIsLikeByTripIds(
accessor.getMemberId(),
List.of(tripId)
));
final Map<Long, LikeInfo> likeInfoByTrip = likeElements.toLikeMap();

final LikeElement likeElement = getLikeElement(accessor.getMemberId(), tripId);
final Boolean isWriter = trip.isWriter(accessor.getMemberId());

return TripDetailResponse.publishedTrip(
trip,
cities,
isWriter,
isLike(likeInfoByTrip, tripId),
getLikeCount(likeInfoByTrip, tripId),
likeElement.isLike(),
likeElement.getLikeCount(),
publishedDate
);
}

private LikeElement getLikeElement(final Long memberId, final Long tripId) {
final Optional<LikeCount> likeCount = likeCountRepository.findById(tripId);
final Optional<MemberLike> memberLike = memberLikeRepository.findById(memberId);
if (likeCount.isPresent() && memberLike.isPresent()) {
final Map<Long, Boolean> tripLikeStatusMap = memberLike.get().getLikeStatusForTrip();
if (tripLikeStatusMap.containsKey(tripId)) {
return new LikeElement(tripId, likeCount.get().getCount(), tripLikeStatusMap.get(tripId));
}
return new LikeElement(tripId, likeCount.get().getCount(), false);
}
return likeRepository.findLikeCountAndIsLikeByTripId(memberId, tripId)
.orElseGet(() -> new LikeElement(tripId, 0, false));
}
}
17 changes: 17 additions & 0 deletions backend/src/main/java/hanglog/like/domain/LikeCount.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package hanglog.like.domain;

import lombok.AllArgsConstructor;
import lombok.Getter;
import org.springframework.data.annotation.Id;
import org.springframework.data.redis.core.RedisHash;

@Getter
@AllArgsConstructor
@RedisHash(value = "likeCount")
public class LikeCount {

@Id
private Long tripId;

private Long count;
}
18 changes: 18 additions & 0 deletions backend/src/main/java/hanglog/like/domain/MemberLike.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package hanglog.like.domain;

import java.util.Map;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.springframework.data.annotation.Id;
import org.springframework.data.redis.core.RedisHash;

@Getter
@AllArgsConstructor
@RedisHash(value = "memberLike", timeToLive = 5400)
public class MemberLike {

@Id
private Long memberId;

private Map<Long, Boolean> likeStatusForTrip;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package hanglog.like.domain.repository;

import hanglog.like.domain.Likes;
import java.util.List;

public interface CustomLikeRepository {

void saveAll(final List<Likes> likes);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package hanglog.like.domain.repository;

import hanglog.like.domain.LikeCount;
import java.util.List;
import org.springframework.data.repository.CrudRepository;

public interface LikeCountRepository extends CrudRepository<LikeCount, Long> {

List<LikeCount> findLikeCountsByTripIdIn(final List<Long> tripIds);
}
Original file line number Diff line number Diff line change
@@ -1,18 +1,16 @@
package hanglog.like.repository;
package hanglog.like.domain.repository;

import hanglog.like.domain.Likes;
import hanglog.like.dto.LikeElement;
import hanglog.like.dto.TripLikeCount;
import java.util.List;
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;

public interface LikeRepository extends JpaRepository<Likes, Long> {

boolean existsByMemberIdAndTripId(final Long memberId, final Long tripId);

void deleteByMemberIdAndTripId(final Long memberId, final Long tripId);

@Query("""
SELECT new hanglog.like.dto.LikeElement
(l.tripId, COUNT(l.memberId), EXISTS(SELECT 1 FROM Likes l_1 WHERE l_1.memberId = :memberId AND l_1.tripId = l.tripId))
Expand All @@ -22,4 +20,21 @@ public interface LikeRepository extends JpaRepository<Likes, Long> {
""")
List<LikeElement> findLikeCountAndIsLikeByTripIds(@Param("memberId") final Long memberId,
@Param("tripIds") final List<Long> tripIds);

@Query("""
SELECT new hanglog.like.dto.LikeElement
(l.tripId, COUNT(l.memberId), EXISTS(SELECT 1 FROM Likes l_1 WHERE l_1.memberId = :memberId AND l_1.tripId = l.tripId))
FROM Likes l
WHERE l.tripId = :tripId
GROUP BY l.tripId
""")
Optional<LikeElement> findLikeCountAndIsLikeByTripId(@Param("memberId") final Long memberId,
@Param("tripId") final Long tripId);

@Query("""
SELECT new hanglog.like.dto.TripLikeCount(l.tripId, COUNT(l.memberId))
FROM Likes l
GROUP BY l.tripId
""")
List<TripLikeCount> findCountByAllTrips();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package hanglog.like.domain.repository;

import hanglog.like.domain.MemberLike;
import org.springframework.data.repository.CrudRepository;

public interface MemberLikeRepository extends CrudRepository<MemberLike, Long> {
}
12 changes: 12 additions & 0 deletions backend/src/main/java/hanglog/like/dto/TripLikeCount.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package hanglog.like.dto;

import lombok.Getter;
import lombok.RequiredArgsConstructor;

@Getter
@RequiredArgsConstructor
public class TripLikeCount {

private final long tripId;
private final long count;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package hanglog.like.infrastrcutrue;

import hanglog.like.domain.Likes;
import hanglog.like.domain.repository.CustomLikeRepository;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.stereotype.Repository;

@RequiredArgsConstructor
@Repository
public class CustomLikeRepositoryImpl implements CustomLikeRepository {

private final NamedParameterJdbcTemplate namedParameterJdbcTemplate;

@Override
public void saveAll(final List<Likes> likes) {
final String sql = """
INSERT INTO likes (trip_id, member_id)
VALUES (:tripId, :memberId)
""";
namedParameterJdbcTemplate.batchUpdate(sql, getLikesToSqlParameterSources(likes));
}

private MapSqlParameterSource[] getLikesToSqlParameterSources(final List<Likes> likes) {
return likes.stream()
.map(this::getLikeToSqlParameterSource)
.toArray(MapSqlParameterSource[]::new);
}

private MapSqlParameterSource getLikeToSqlParameterSource(final Likes likes) {
return new MapSqlParameterSource()
.addValue("tripId", likes.getTripId())
.addValue("memberId", likes.getMemberId());
}
}
59 changes: 53 additions & 6 deletions backend/src/main/java/hanglog/like/service/LikeService.java
Original file line number Diff line number Diff line change
@@ -1,9 +1,22 @@
package hanglog.like.service;

import hanglog.like.domain.LikeCount;
import hanglog.like.domain.Likes;
import hanglog.like.domain.MemberLike;
import hanglog.like.dto.TripLikeCount;
import hanglog.like.dto.request.LikeRequest;
import hanglog.like.repository.LikeRepository;
import hanglog.like.domain.repository.CustomLikeRepository;
import hanglog.like.domain.repository.LikeCountRepository;
import hanglog.like.domain.repository.LikeRepository;
import hanglog.like.domain.repository.MemberLikeRepository;
import hanglog.member.domain.repository.MemberRepository;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Stream;
import lombok.RequiredArgsConstructor;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

Expand All @@ -13,15 +26,49 @@
public class LikeService {

private final LikeRepository likeRepository;
private final MemberLikeRepository memberLikeRepository;
private final LikeCountRepository likeCountRepository;
private final MemberRepository memberRepository;
private final CustomLikeRepository customLikeRepository;

public void update(final Long memberId, final Long tripId, final LikeRequest likeRequest) {
final boolean requestStatus = likeRequest.getIsLike();
Map<Long, Boolean> tripLikeStatusMap = new HashMap<>();
final Optional<MemberLike> memberLike = memberLikeRepository.findById(memberId);
if (memberLike.isPresent()) {
tripLikeStatusMap = memberLike.get().getLikeStatusForTrip();
}
tripLikeStatusMap.put(tripId, likeRequest.getIsLike());
memberLikeRepository.save(new MemberLike(memberId, tripLikeStatusMap));
updateLikeCountCache(tripId, likeRequest);
}

if (requestStatus && !likeRepository.existsByMemberIdAndTripId(memberId, tripId)) {
likeRepository.save(new Likes(tripId, memberId));
private void updateLikeCountCache(final Long tripId, final LikeRequest likeRequest) {
final Optional<LikeCount> likeCount = likeCountRepository.findById(tripId);
if (Boolean.TRUE.equals(likeRequest.getIsLike())) {
likeCount.ifPresent(count -> likeCountRepository.save(new LikeCount(tripId, count.getCount() + 1)));
return;
}
if (!requestStatus) {
likeRepository.deleteByMemberIdAndTripId(memberId, tripId);
likeCount.ifPresent(count -> likeCountRepository.save(new LikeCount(tripId, count.getCount() - 1)));
}

@Scheduled(cron = "0 0 * * * *")
public void writeBackMemberLikeCache() {
final List<Likes> likes = memberRepository.findAll().stream()
.flatMap(member -> memberLikeRepository.findById(member.getId())
.map(memberLike -> memberLike.getLikeStatusForTrip()
.entrySet().stream()
.filter(Map.Entry::getValue)
.map(entry -> new Likes(entry.getKey(), member.getId())))
.orElseGet(Stream::empty))
.toList();
customLikeRepository.saveAll(likes);
}

@Scheduled(cron = "0 0 0 * * *")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

굉장합니다!!!!!!!!신기합니다!!!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

홍고가 만들어 놓은 일일 환율 저장 로직 참고한건데요!!!!! 본인 칭찬인가요!!!!!!!!

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아니ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ 요 스케줄링 아래 캐싱로직말한거였는데 제가 이상한 포인트에서 댓글을 달았군요 ㅋ ㅋ ㅋ

public void cacheLikeCount() {
final List<TripLikeCount> tripLikeCounts = likeRepository.findCountByAllTrips();
for (final TripLikeCount tripLikeCount : tripLikeCounts) {
likeCountRepository.save(new LikeCount(tripLikeCount.getTripId(), tripLikeCount.getCount()));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,7 @@
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;

@Import({
TripService.class,
CommunityService.class,
LedgerService.class,
RecommendStrategies.class,
CustomDayLogRepositoryImpl.class,
CustomTripCityRepositoryImpl.class,
EventListenerTestConfig.class
})
class CommunityServiceIntegrationTest extends ServiceIntegrationTest {
class CommunityServiceIntegrationTest extends RedisServiceIntegrationTest {

@Autowired
private TripService tripService;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,14 @@
import static org.assertj.core.api.SoftAssertions.assertSoftly;

import hanglog.like.dto.request.LikeRequest;
import hanglog.like.repository.LikeRepository;
import hanglog.like.domain.repository.MemberLikeRepository;
import hanglog.like.service.LikeService;
import hanglog.trip.infrastructure.CustomDayLogRepositoryImpl;
import hanglog.trip.infrastructure.CustomTripCityRepositoryImpl;
import hanglog.trip.service.TripService;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Import;

@Import({
TripService.class,
LikeService.class,
CustomDayLogRepositoryImpl.class,
CustomTripCityRepositoryImpl.class
})
class LikeServiceIntegrationTest extends ServiceIntegrationTest {
class LikeServiceIntegrationTest extends RedisServiceIntegrationTest {

@Autowired
private TripService tripService;
Expand All @@ -29,7 +20,7 @@ class LikeServiceIntegrationTest extends ServiceIntegrationTest {
private LikeService likeService;

@Autowired
private LikeRepository likeRepository;
private MemberLikeRepository memberLikeRepository;

@DisplayName("해당 게시물의 좋아요 여부를 변경할 수 있다.")
@Test
Expand All @@ -42,11 +33,21 @@ void update() {

// when & then
assertSoftly(softly -> {
softly.assertThat(likeRepository.existsByMemberIdAndTripId(member.getId(), tripId)).isFalse();
softly.assertThat(memberLikeRepository.findById(member.getId())).isEmpty();

likeService.update(member.getId(), tripId, likeTrueRequest);
softly.assertThat(likeRepository.existsByMemberIdAndTripId(member.getId(), tripId)).isTrue();
softly.assertThat(memberLikeRepository.findById(member.getId())).isPresent();
softly.assertThat(memberLikeRepository.findById(member.getId())
.get()
.getLikeStatusForTrip()
.get(tripId)).isTrue();

likeService.update(member.getId(), tripId, likeFalseRequest);
softly.assertThat(likeRepository.existsByMemberIdAndTripId(member.getId(), tripId)).isFalse();
softly.assertThat(memberLikeRepository.findById(member.getId())).isPresent();
softly.assertThat(memberLikeRepository.findById(member.getId())
.get()
.getLikeStatusForTrip()
.get(tripId)).isFalse();
});
}
}
Loading