Skip to content

Commit

Permalink
[BYOB-233] 전체 노트 조회 시 발생하는 상속 구조 엔티티에서의 N+1 문제 해결 (#76)
Browse files Browse the repository at this point in the history
* feat: Note 도메인 repository 메서드 추가

* refactor: 상속 엔티티 구조에서의 N+1 문제 해결을 위한 NoteService의 getNotesById 메서드 로직 변경

* refactor: 유형별 노트 상세조회 메서드 getMergedChildNoteList 분리 및 일괄 적용

* refactor: 노트 단건 상세조회 API N+1 문제 해결

* fix: BatchSize 수정
  • Loading branch information
y-ngm-n authored Nov 18, 2024
1 parent a0616f0 commit b1c6bf9
Showing 6 changed files with 139 additions and 72 deletions.
Original file line number Diff line number Diff line change
@@ -29,7 +29,7 @@ public abstract class Note extends BaseTimeEntity {
private NewLiquor liquor;

@OneToMany(mappedBy = "note", fetch = FetchType.LAZY)
@BatchSize(size = 100)
@BatchSize(size = 20)
private List<NoteImage> noteImages = new ArrayList<>();

protected Note() {}
Original file line number Diff line number Diff line change
@@ -31,7 +31,7 @@ public class TastingNote extends Note {
private String finish;

@OneToMany(mappedBy = "note", fetch = FetchType.LAZY)
@BatchSize(size = 100)
@BatchSize(size = 20)
private List<NoteAroma> noteAromas = new ArrayList<>();

protected TastingNote() {}
Original file line number Diff line number Diff line change
@@ -4,7 +4,6 @@
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import team_alcoholic.jumo_server.v1.liquor.domain.Liquor;
import team_alcoholic.jumo_server.v2.liquor.domain.NewLiquor;
import team_alcoholic.jumo_server.v2.note.domain.Note;
import team_alcoholic.jumo_server.v2.user.domain.NewUser;
@@ -15,36 +14,35 @@
public interface NoteRepository extends JpaRepository<Note, Long> {

/**
* 노트 상세 조회
* 노트 단건 조회
* @param id 노트 id
*/
@EntityGraph(attributePaths = {"user", "liquor", "noteImages"})
@Query("select n from Note n left join fetch n.noteImages ni where n.id=:id order by ni.id")
Optional<Note> findDetailById(Long id);
@Query("select n from Note n where n.id=:id")
Optional<Note> findById(Long id);

/**
* 최신순 노트 페이지네이션 조회
* @param cursor 마지막으로 조회한 노트의 id
* 최신순 노트 페이지네이션 조회: 첫 페이지
* 유형별 노트 상세 조회 전 간략한 목록 조회
* @param pageable paging
*/
@EntityGraph(attributePaths = {"user", "liquor", "noteImages"})
@Query("select n from Note n left join fetch n.noteImages ni where n.id < :cursor order by n.id desc, ni.id")
List<Note> findListByCursor(Long cursor, Pageable pageable);
@Query("select n from Note n order by n.id desc")
List<Note> findList(Pageable pageable);

/**
* 최신순 노트 페이지네이션 조회: 첫 페이지
* 최신순 노트 페이지네이션 조회
* 유형별 노트 상세 조회 전 간략한 목록 조회
* @param cursor 마지막으로 조회한 노트의 id
* @param pageable paging
*/
@EntityGraph(attributePaths = {"user", "liquor", "noteImages"})
@Query("select n from Note n left join fetch n.noteImages ni order by n.id desc, ni.id")
List<Note> findList(Pageable pageable);
@Query("select n from Note n where n.id < :cursor order by n.id desc")
List<Note> findListByCursor(Long cursor, Pageable pageable);

/**
* 사용자별 노트 조회
* 유형별 노트 상세 조회 전 간략한 목록 조회
* @param user 사용자
*/
@EntityGraph(attributePaths = {"user", "liquor", "noteImages"})
@Query("select n from Note n left join fetch n.noteImages ni where n.user = :user order by n.id desc, ni.id")
@Query("select n from Note n where n.user = :user order by n.id desc")
List<Note> findListByUser(NewUser user);

/**
Original file line number Diff line number Diff line change
@@ -16,15 +16,23 @@ public interface PurchaseNoteRepository extends JpaRepository<PurchaseNote, Long
* @param cursor 마지막으로 조회한 노트의 id
* @param pageable paging
*/
@EntityGraph(attributePaths = {"user", "liquor", "noteImages"})
@EntityGraph(attributePaths = {"user", "liquor"})
@Query("select pn from PurchaseNote pn where pn.id < :cursor order by pn.id desc")
List<Note> findListByCursor(Long cursor, Pageable pageable);

/**
* 최신순 노트 페이지네이션 조회
* @param pageable paging
*/
@EntityGraph(attributePaths = {"user", "liquor", "noteImages"})
@EntityGraph(attributePaths = {"user", "liquor"})
@Query("select pn from PurchaseNote pn order by pn.id desc")
List<Note> findList(Pageable pageable);

/**
* noteId 리스트로 구매 노트 리스트 조회
* @param idList noteId 리스트
*/
@EntityGraph(attributePaths = {"user", "liquor", "noteImages"})
@Query("select pn from PurchaseNote pn left join fetch pn.noteImages ni where pn.id in :idList order by pn.id desc, ni.id")
List<PurchaseNote> findListByIdList(List<Long> idList);
}
Original file line number Diff line number Diff line change
@@ -8,9 +8,18 @@
import team_alcoholic.jumo_server.v2.note.domain.TastingNote;

import java.util.List;
import java.util.Optional;

public interface TastingNoteRepository extends JpaRepository<TastingNote, Long> {

/**
* noteId로 테이스팅 노트 조회
* @param id noteId
*/
@EntityGraph(attributePaths = {"user", "liquor", "noteAromas.aroma"})
@Query("select tn from tasting_note_new tn where tn.id = :id order by tn.id desc")
Optional<TastingNote> findById(Long id);

/**
* 최신순 노트 페이지네이션 조회
* @param cursor 마지막으로 조회한 노트의 id
@@ -27,4 +36,12 @@ public interface TastingNoteRepository extends JpaRepository<TastingNote, Long>
@EntityGraph(attributePaths = {"user", "liquor", "noteAromas.aroma"})
@Query("select tn from tasting_note_new tn order by tn.id desc")
List<Note> findList(Pageable pageable);

/**
* noteId 리스트로 테이스팅 노트 리스트 조회
* @param idList noteId 리스트
*/
@EntityGraph(attributePaths = {"user", "liquor", "noteAromas.aroma"})
@Query("select tn from tasting_note_new tn where tn.id in :idList order by tn.id desc")
List<TastingNote> findListByIdList(List<Long> idList);
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package team_alcoholic.jumo_server.v2.note.service;

import jakarta.persistence.DiscriminatorValue;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.PageRequest;
import org.springframework.security.oauth2.core.user.OAuth2User;
@@ -8,7 +9,6 @@
import org.springframework.web.multipart.MultipartFile;
import team_alcoholic.jumo_server.global.common.service.CommonUtilService;
import team_alcoholic.jumo_server.global.error.exception.UnauthorizedException;
import team_alcoholic.jumo_server.v1.liquor.domain.Liquor;
import team_alcoholic.jumo_server.v1.liquor.exception.LiquorNotFoundException;
import team_alcoholic.jumo_server.v2.liquor.domain.NewLiquor;
import team_alcoholic.jumo_server.v2.liquor.repository.NewLiquorRepository;
@@ -30,10 +30,7 @@
import team_alcoholic.jumo_server.v2.user.repository.UserRepository;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import java.util.*;

@Service
@RequiredArgsConstructor
@@ -159,7 +156,15 @@ public TastingNoteRes updateTastingNote(OAuth2User oAuth2User, Long noteId, Tast
*/
@Transactional
public GeneralNoteRes getNoteById(Long id) {
Note note = noteRepository.findDetailById(id).orElseThrow(() -> new NoteNotFoundException(id));
// 부모 엔티티 Note로 먼저 조회
Note simpleNote = noteRepository.findById(id).orElseThrow(() -> new NoteNotFoundException(id));
String type = simpleNote.getClass().getAnnotation(DiscriminatorValue.class).value();

// 노트 유형에 맞게 상세 조회
Note note;
if ("PURCHASE".equals(type)) { note = purchaseNoteRepository.findById(simpleNote.getId()).orElseThrow(() -> new NoteNotFoundException(simpleNote.getId())); }
else { note = tastingNoteRepository.findById(simpleNote.getId()).orElseThrow(() -> new NoteNotFoundException(simpleNote.getId())); }

return GeneralNoteRes.from(note);
}

@@ -170,36 +175,49 @@ public GeneralNoteRes getNoteById(Long id) {
* @param type 조회하려는 노트의 종류
*/
public NoteListRes getNotesById(Long cursor, int limit, String type) {
// 노트 목록 조회
List<Note> notes;
if (cursor == null) {
notes = switch (type) {
case "PURCHASE" -> purchaseNoteRepository.findList(PageRequest.of(0, limit + 1));
case "TASTING" -> tastingNoteRepository.findList(PageRequest.of(0, limit + 1));
case "ALL" -> noteRepository.findList(PageRequest.of(0, limit + 1));
default -> throw new IllegalArgumentException("Invalid type");
};
// 전체 노트 목록 조회인 경우
if ("ALL".equals(type)) {
// 부모 엔티티 Note에 대해 우선 페이지네이션 조회
List<Note> simpleNotes;
if (cursor == null) { simpleNotes = noteRepository.findList(PageRequest.of(0, limit+1)); }
else { simpleNotes = noteRepository.findListByCursor(cursor, PageRequest.of(0, limit+1)); }

// 페이지네이션 관련 정보
boolean eof = simpleNotes.size() < limit + 1;
if (!eof) { simpleNotes.remove(limit); }
Long newCursor = simpleNotes.isEmpty() ? -1 : (simpleNotes.get(simpleNotes.size() - 1).getId());

// 노트 유형별 상세 조회 후 dto로 변환한 리스트
List<GeneralNoteRes> results = getMergedChildNoteList(simpleNotes);

return NoteListRes.of(newCursor, eof, results);
}
// 구매/테이스팅 노트 목록 조회인 경우
else {
notes = switch (type) {
case "PURCHASE" -> purchaseNoteRepository.findListByCursor(cursor, PageRequest.of(0, limit + 1));
case "TASTING" -> tastingNoteRepository.findListByCursor(cursor, PageRequest.of(0, limit + 1));
case "ALL" -> noteRepository.findListByCursor(cursor, PageRequest.of(0, limit + 1));
default -> throw new IllegalArgumentException("Invalid type");
};
}
List<Note> notes;
if (cursor == null) {
notes = ("PURCHASE".equals(type)) ?
purchaseNoteRepository.findList(PageRequest.of(0, limit + 1)) :
tastingNoteRepository.findList(PageRequest.of(0, limit + 1));
}
else {
notes = ("PURCHASE".equals(type)) ?
purchaseNoteRepository.findListByCursor(cursor, PageRequest.of(0, limit + 1)) :
tastingNoteRepository.findListByCursor(cursor, PageRequest.of(0, limit + 1));
}

// 페이지네이션 관련 정보
boolean eof = notes.size() < limit + 1;
if (!eof) { notes.remove(limit); }
Long newCursor = notes.isEmpty() ? -1 : (notes.get(notes.size() - 1).getId());
// 페이지네이션 관련 정보
boolean eof = notes.size() < limit + 1;
if (!eof) { notes.remove(limit); }
Long newCursor = notes.isEmpty() ? -1 : (notes.get(notes.size() - 1).getId());

// dto로 변환
ArrayList<GeneralNoteRes> noteResList = new ArrayList<>();
for (Note note : notes) {
noteResList.add(GeneralNoteRes.from(note));
// response 변환
List<GeneralNoteRes> noteResList = new ArrayList<>();
for (Note note : notes) {
noteResList.add(GeneralNoteRes.from(note));
}
return NoteListRes.of(newCursor, eof, noteResList);
}
return NoteListRes.of(newCursor, eof, noteResList);
}

/**
@@ -211,20 +229,15 @@ public List<GeneralNoteRes> getNotesByUser(UUID userUuid) {
NewUser user = userRepository.findByUserUuid(userUuid);
if (user == null) { throw new UserNotFoundException(userUuid); }

// note 조회
List<Note> notes = noteRepository.findListByUser(user);

// dto로 변환
ArrayList<GeneralNoteRes> noteResList = new ArrayList<>();
for (Note note : notes) {
noteResList.add(GeneralNoteRes.from(note));
}
// user를 통해 부모 엔티티 Note 목록 우선 조회
List<Note> simpleNotes = noteRepository.findListByUser(user);

return noteResList;
// 노트 유형별 상세 조회 후 dto로 변환한 리스트 반환
return getMergedChildNoteList(simpleNotes);
}

/**
* 사용자별 노트 조회 메서드
* 사용자별 및 주류별 노트 조회 메서드
* @param userUuid 사용자 uuid
* @param liquorId 주류 id
*/
@@ -237,15 +250,10 @@ public List<GeneralNoteRes> getNotesByUserAndLiquor(UUID userUuid, Long liquorId
NewLiquor liquor = liquorRepository.findById(liquorId).orElseThrow(() -> new LiquorNotFoundException(liquorId));

// note 조회
List<Note> notes = noteRepository.findListByUserAndLiquor(user, liquor);
List<Note> simpleNotes = noteRepository.findListByUserAndLiquor(user, liquor);

// dto로 변환
ArrayList<GeneralNoteRes> noteResList = new ArrayList<>();
for (Note note : notes) {
noteResList.add(GeneralNoteRes.from(note));
}

return noteResList;
// 노트 유형별 상세 조회 후 dto로 변환한 리스트 반환
return getMergedChildNoteList(simpleNotes);
}

/**
@@ -258,11 +266,47 @@ public List<GeneralNoteRes> getNotesByLiquor(Long liquorId) {
.orElseThrow(() -> new LiquorNotFoundException(liquorId));

// note 조회
List<Note> notes = noteRepository.findListByLiquor(liquor);
List<Note> simpleNotes = noteRepository.findListByLiquor(liquor);

// dto로 변환
ArrayList<GeneralNoteRes> noteResList = new ArrayList<>();
// 노트 유형별 상세 조회 후 dto로 변환한 리스트 반환
return getMergedChildNoteList(simpleNotes);
}

/**
* Note 엔티티 리스트를 받아서, 각 Note의 type별로 상세 조회를 수행한 뒤 dto 리스트로 변환하는 메서드
* Note type과 관계 없이 Note 목록을 조회할 때 사용
* @param simpleNotes 노트 type별로 상세 조회할 Note 엔티티 리스트
*/
private List<GeneralNoteRes> getMergedChildNoteList(List<Note> simpleNotes) {
// 노트 타입에 따라 noteId 리스트 분리
List<Long> purchaseNotesIdList = new ArrayList<>();
List<Long> tastingNotesIdList = new ArrayList<>();
for (Note note : simpleNotes) {
if ("PURCHASE".equals(note.getClass().getAnnotation(DiscriminatorValue.class).value())) {
purchaseNotesIdList.add(note.getId());
}
else { tastingNotesIdList.add(note.getId()); }
}

// 노트 타입에 따라 noteId 리스트를 통해 자식 엔티티 조회
List<PurchaseNote> purchaseNotes = (purchaseNotesIdList.isEmpty()) ? new ArrayList<>() : purchaseNoteRepository.findListByIdList(purchaseNotesIdList);
List<TastingNote> tastingNotes = (tastingNotesIdList.isEmpty()) ? new ArrayList<>() : tastingNoteRepository.findListByIdList(tastingNotesIdList);

// 분리된 리스트 다시 통합
List<Note> notes = new ArrayList<>();
notes.addAll(purchaseNotes);
notes.addAll(tastingNotes);

// 통합된 리스트 HashMap으로 변환
Map<Long, Note> results = new HashMap<>();
for (Note note : notes) {
results.put(note.getId(), note);
}

// response로 변환: 처음 조회했던 Note 리스트의 순서에 맞게 HashMap에서 가져와 변환
List<GeneralNoteRes> noteResList = new ArrayList<>();
for (Note simpleNote : simpleNotes) {
Note note = results.get(simpleNote.getId());
noteResList.add(GeneralNoteRes.from(note));
}

0 comments on commit b1c6bf9

Please sign in to comment.