From 8431474386be156d9759498ba27d0b1af80ded34 Mon Sep 17 00:00:00 2001 From: Seokmyung Ham Date: Fri, 23 Aug 2024 02:49:55 +0900 Subject: [PATCH] =?UTF-8?q?[BE]=20JPA=EC=9D=98=20saveAll,=20deleteAll?= =?UTF-8?q?=EB=A5=BC=20bulk=20query=EB=A1=9C=20=EA=B0=9C=EC=84=A0=20(#273)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(ScheduleBatchRepository): jdbcTemplate를 활용한 batchInsert 구현 * refactor(ScheduleRepository): jpql을 작성하여 delete 쿼리가 n건 나가는 문제 개선 * refactor: PreparedStatement 매개변수 NonNull 어노테이션 추가 * fix: 잘못된 batch size 수정 * refactor(ScheduleRepository): 쿼리 메소드 Transactional 어노테이션 추가 * refactor(ScheduleBatchRepository): 배치 업데이트 로직 개선 * feat(AvailableDateBatchRepository): 약속 생성 시 가능 날짜를 배치 처리하도록 개선 * refactor(ScheduleBatchRepository): 배치 사이즈를 500으로 조정 --- .../domain/availabledate/AvailableDate.java | 4 ++ .../AvailableDateBatchRepository.java | 47 +++++++++++++ .../kr/momo/domain/schedule/Schedule.java | 8 +++ .../schedule/ScheduleBatchRepository.java | 47 +++++++++++++ .../domain/schedule/ScheduleRepository.java | 8 ++- .../momo/service/meeting/MeetingService.java | 4 +- .../service/schedule/ScheduleService.java | 6 +- .../AvailableDateBatchRepositoryTest.java | 51 ++++++++++++++ .../schedule/ScheduleBatchRepositoryTest.java | 67 ++++++++++++++++++ .../schedule/ScheduleRepositoryTest.java | 68 +++++++++++++++++++ 10 files changed, 306 insertions(+), 4 deletions(-) create mode 100644 backend/src/main/java/kr/momo/domain/availabledate/AvailableDateBatchRepository.java create mode 100644 backend/src/main/java/kr/momo/domain/schedule/ScheduleBatchRepository.java create mode 100644 backend/src/test/java/kr/momo/domain/availabledate/AvailableDateBatchRepositoryTest.java create mode 100644 backend/src/test/java/kr/momo/domain/schedule/ScheduleBatchRepositoryTest.java create mode 100644 backend/src/test/java/kr/momo/domain/schedule/ScheduleRepositoryTest.java diff --git a/backend/src/main/java/kr/momo/domain/availabledate/AvailableDate.java b/backend/src/main/java/kr/momo/domain/availabledate/AvailableDate.java index 62e0c0719..adf04b689 100644 --- a/backend/src/main/java/kr/momo/domain/availabledate/AvailableDate.java +++ b/backend/src/main/java/kr/momo/domain/availabledate/AvailableDate.java @@ -55,4 +55,8 @@ public boolean isBefore(LocalDate other) { public boolean isEqual(LocalDate other) { return date.isEqual(other); } + + public Long meetingId() { + return meeting.getId(); + } } diff --git a/backend/src/main/java/kr/momo/domain/availabledate/AvailableDateBatchRepository.java b/backend/src/main/java/kr/momo/domain/availabledate/AvailableDateBatchRepository.java new file mode 100644 index 000000000..4fd9b56a0 --- /dev/null +++ b/backend/src/main/java/kr/momo/domain/availabledate/AvailableDateBatchRepository.java @@ -0,0 +1,47 @@ +package kr.momo.domain.availabledate; + +import java.sql.Date; +import java.sql.PreparedStatement; +import java.sql.Timestamp; +import java.time.LocalDateTime; +import java.util.Collection; +import lombok.RequiredArgsConstructor; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.ParameterizedPreparedStatementSetter; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; + +@Repository +@RequiredArgsConstructor +public class AvailableDateBatchRepository { + + private static final int BATCH_SIZE = 30; + + private final JdbcTemplate jdbcTemplate; + + @Transactional + public void batchInsert(Collection availableDates) { + String sql = """ + INSERT INTO available_date (date, meeting_id, created_at, modified_at) + VALUES (?, ?, ?, ?); + """; + + executeBatchUpdate(availableDates, sql); + } + + private void executeBatchUpdate(Collection availableDates, String sql) { + LocalDateTime now = LocalDateTime.now(); + Timestamp timestamp = Timestamp.valueOf(now); + + jdbcTemplate.batchUpdate(sql, availableDates, BATCH_SIZE, createPreparedStatementSetter(timestamp)); + } + + private ParameterizedPreparedStatementSetter createPreparedStatementSetter(Timestamp timestamp) { + return (PreparedStatement ps, AvailableDate availableDate) -> { + ps.setDate(1, Date.valueOf(availableDate.getDate())); + ps.setLong(2, availableDate.meetingId()); + ps.setTimestamp(3, timestamp); + ps.setTimestamp(4, timestamp); + }; + } +} diff --git a/backend/src/main/java/kr/momo/domain/schedule/Schedule.java b/backend/src/main/java/kr/momo/domain/schedule/Schedule.java index fa0335e26..4c1d61859 100644 --- a/backend/src/main/java/kr/momo/domain/schedule/Schedule.java +++ b/backend/src/main/java/kr/momo/domain/schedule/Schedule.java @@ -64,7 +64,15 @@ public LocalTime time() { return timeslot.startTime(); } + public Long attendeeId() { + return attendee.getId(); + } + public String attendeeName() { return attendee.name(); } + + public Long availableDateId() { + return availableDate.getId(); + } } diff --git a/backend/src/main/java/kr/momo/domain/schedule/ScheduleBatchRepository.java b/backend/src/main/java/kr/momo/domain/schedule/ScheduleBatchRepository.java new file mode 100644 index 000000000..1f324de15 --- /dev/null +++ b/backend/src/main/java/kr/momo/domain/schedule/ScheduleBatchRepository.java @@ -0,0 +1,47 @@ +package kr.momo.domain.schedule; + +import java.sql.PreparedStatement; +import java.sql.Timestamp; +import java.time.LocalDateTime; +import java.util.Collection; +import lombok.RequiredArgsConstructor; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.ParameterizedPreparedStatementSetter; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; + +@Repository +@RequiredArgsConstructor +public class ScheduleBatchRepository { + + private static final int BATCH_SIZE = 500; + + private final JdbcTemplate jdbcTemplate; + + @Transactional + public void batchInsert(Collection schedules) { + String sql = """ + INSERT INTO schedule (attendee_id, available_date_id, timeslot, created_at, modified_at) + VALUES (?, ?, ?, ?, ?); + """; + + executeBatchUpdate(schedules, sql); + } + + private void executeBatchUpdate(Collection schedules, String sql) { + LocalDateTime now = LocalDateTime.now(); + Timestamp timestamp = Timestamp.valueOf(now); + + jdbcTemplate.batchUpdate(sql, schedules, BATCH_SIZE, createPreparedStatementSetter(timestamp)); + } + + private ParameterizedPreparedStatementSetter createPreparedStatementSetter(Timestamp timestamp) { + return (PreparedStatement ps, Schedule schedule) -> { + ps.setLong(1, schedule.attendeeId()); + ps.setLong(2, schedule.availableDateId()); + ps.setString(3, schedule.getTimeslot().toString()); + ps.setTimestamp(4, timestamp); + ps.setTimestamp(5, timestamp); + }; + } +} diff --git a/backend/src/main/java/kr/momo/domain/schedule/ScheduleRepository.java b/backend/src/main/java/kr/momo/domain/schedule/ScheduleRepository.java index 94e387b06..384f8ade7 100644 --- a/backend/src/main/java/kr/momo/domain/schedule/ScheduleRepository.java +++ b/backend/src/main/java/kr/momo/domain/schedule/ScheduleRepository.java @@ -4,6 +4,9 @@ import kr.momo.domain.attendee.Attendee; import org.springframework.data.jpa.repository.EntityGraph; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.transaction.annotation.Transactional; public interface ScheduleRepository extends JpaRepository { @@ -13,5 +16,8 @@ public interface ScheduleRepository extends JpaRepository { @EntityGraph(attributePaths = {"availableDate"}) List findAllByAttendeeIn(List attendees); - void deleteAllByAttendee(Attendee attendee); + @Modifying + @Transactional + @Query("DELETE FROM Schedule s WHERE s.attendee = :attendee") + void deleteByAttendee(Attendee attendee); } diff --git a/backend/src/main/java/kr/momo/service/meeting/MeetingService.java b/backend/src/main/java/kr/momo/service/meeting/MeetingService.java index 46125979e..192ffa296 100644 --- a/backend/src/main/java/kr/momo/service/meeting/MeetingService.java +++ b/backend/src/main/java/kr/momo/service/meeting/MeetingService.java @@ -8,6 +8,7 @@ import kr.momo.domain.attendee.Attendee; import kr.momo.domain.attendee.AttendeeRepository; import kr.momo.domain.attendee.Role; +import kr.momo.domain.availabledate.AvailableDateBatchRepository; import kr.momo.domain.availabledate.AvailableDateRepository; import kr.momo.domain.availabledate.AvailableDates; import kr.momo.domain.meeting.Meeting; @@ -33,6 +34,7 @@ public class MeetingService { private final MeetingRepository meetingRepository; private final AvailableDateRepository availableDateRepository; private final AttendeeRepository attendeeRepository; + private final AvailableDateBatchRepository availableDateBatchRepository; @Transactional public MeetingCreateResponse create(MeetingCreateRequest request) { @@ -40,7 +42,7 @@ public MeetingCreateResponse create(MeetingCreateRequest request) { AvailableDates meetingDates = new AvailableDates(request.toAvailableMeetingDates(), meeting); validateNotPast(meetingDates); - availableDateRepository.saveAll(meetingDates.getAvailableDates()); + availableDateBatchRepository.batchInsert(meetingDates.getAvailableDates()); Attendee attendee = saveHostAttendee(meeting, request.hostName(), request.hostPassword()); String token = jwtManager.generate(attendee.getId()); diff --git a/backend/src/main/java/kr/momo/service/schedule/ScheduleService.java b/backend/src/main/java/kr/momo/service/schedule/ScheduleService.java index 7dbecb450..e0b0b5430 100644 --- a/backend/src/main/java/kr/momo/service/schedule/ScheduleService.java +++ b/backend/src/main/java/kr/momo/service/schedule/ScheduleService.java @@ -24,6 +24,7 @@ import kr.momo.domain.meeting.Meeting; import kr.momo.domain.meeting.MeetingRepository; import kr.momo.domain.schedule.Schedule; +import kr.momo.domain.schedule.ScheduleBatchRepository; import kr.momo.domain.schedule.ScheduleRepository; import kr.momo.domain.timeslot.Timeslot; import kr.momo.exception.MomoException; @@ -47,6 +48,7 @@ public class ScheduleService { private final AttendeeRepository attendeeRepository; private final ScheduleRepository scheduleRepository; private final AvailableDateRepository availableDateRepository; + private final ScheduleBatchRepository scheduleBatchRepository; @Transactional public void create(String uuid, long attendeeId, ScheduleCreateRequest request) { @@ -57,9 +59,9 @@ public void create(String uuid, long attendeeId, ScheduleCreateRequest request) Attendee attendee = attendeeRepository.findByIdAndMeeting(attendeeId, meeting) .orElseThrow(() -> new MomoException(AttendeeErrorCode.INVALID_ATTENDEE)); - scheduleRepository.deleteAllByAttendee(attendee); + scheduleRepository.deleteByAttendee(attendee); List schedules = createSchedules(request, meeting, attendee); - scheduleRepository.saveAll(schedules); + scheduleBatchRepository.batchInsert(schedules); } private void validateMeetingUnLocked(Meeting meeting) { diff --git a/backend/src/test/java/kr/momo/domain/availabledate/AvailableDateBatchRepositoryTest.java b/backend/src/test/java/kr/momo/domain/availabledate/AvailableDateBatchRepositoryTest.java new file mode 100644 index 000000000..120b328f6 --- /dev/null +++ b/backend/src/test/java/kr/momo/domain/availabledate/AvailableDateBatchRepositoryTest.java @@ -0,0 +1,51 @@ +package kr.momo.domain.availabledate; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; +import kr.momo.domain.meeting.Meeting; +import kr.momo.domain.meeting.MeetingRepository; +import kr.momo.fixture.AvailableDateFixture; +import kr.momo.fixture.MeetingFixture; +import kr.momo.support.IsolateDatabase; +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.jdbc.core.JdbcTemplate; + +@SpringBootTest +@IsolateDatabase +class AvailableDateBatchRepositoryTest { + + @Autowired + private JdbcTemplate jdbcTemplate; + + @Autowired + private AvailableDateBatchRepository availableDateBatchRepository; + + @Autowired + private MeetingRepository meetingRepository; + + private Meeting meeting; + + @BeforeEach + void setUp() { + meeting = meetingRepository.save(MeetingFixture.COFFEE.create()); + } + + @DisplayName("AvailableDate 리스트를 Batch Insert 한다.") + @Test + void batchInsertTest() { + List availableDate = List.of( + AvailableDateFixture.TODAY.create(meeting), + AvailableDateFixture.TOMORROW.create(meeting) + ); + + availableDateBatchRepository.batchInsert(availableDate); + + Integer count = jdbcTemplate.queryForObject("SELECT COUNT(*) FROM available_date", Integer.class); + assertThat(count).isEqualTo(availableDate.size()); + } +} diff --git a/backend/src/test/java/kr/momo/domain/schedule/ScheduleBatchRepositoryTest.java b/backend/src/test/java/kr/momo/domain/schedule/ScheduleBatchRepositoryTest.java new file mode 100644 index 000000000..4111a81dd --- /dev/null +++ b/backend/src/test/java/kr/momo/domain/schedule/ScheduleBatchRepositoryTest.java @@ -0,0 +1,67 @@ +package kr.momo.domain.schedule; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; +import kr.momo.domain.attendee.Attendee; +import kr.momo.domain.attendee.AttendeeRepository; +import kr.momo.domain.availabledate.AvailableDate; +import kr.momo.domain.availabledate.AvailableDateRepository; +import kr.momo.domain.meeting.Meeting; +import kr.momo.domain.meeting.MeetingRepository; +import kr.momo.domain.timeslot.Timeslot; +import kr.momo.fixture.AttendeeFixture; +import kr.momo.fixture.AvailableDateFixture; +import kr.momo.fixture.MeetingFixture; +import kr.momo.support.IsolateDatabase; +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.jdbc.core.JdbcTemplate; + +@SpringBootTest +@IsolateDatabase +class ScheduleBatchRepositoryTest { + + @Autowired + private JdbcTemplate jdbcTemplate; + + @Autowired + private ScheduleBatchRepository scheduleBatchRepository; + + @Autowired + private MeetingRepository meetingRepository; + + @Autowired + private AttendeeRepository attendeeRepository; + + @Autowired + private AvailableDateRepository availableDateRepository; + + private Attendee attendee; + private AvailableDate availableDate; + + @BeforeEach + void setUp() { + Meeting meeting = meetingRepository.save(MeetingFixture.COFFEE.create()); + attendee = attendeeRepository.save(AttendeeFixture.HOST_JAZZ.create(meeting)); + availableDate = availableDateRepository.save(AvailableDateFixture.TODAY.create(meeting)); + } + + @DisplayName("Schedule 리스트를 Batch Insert 한다.") + @Test + void batchInsertTest() { + List schedules = List.of( + new Schedule(attendee, availableDate, Timeslot.TIME_0000), + new Schedule(attendee, availableDate, Timeslot.TIME_0130), + new Schedule(attendee, availableDate, Timeslot.TIME_0230) + ); + + scheduleBatchRepository.batchInsert(schedules); + + Integer count = jdbcTemplate.queryForObject("SELECT COUNT(*) FROM schedule", Integer.class); + assertThat(count).isEqualTo(schedules.size()); + } +} diff --git a/backend/src/test/java/kr/momo/domain/schedule/ScheduleRepositoryTest.java b/backend/src/test/java/kr/momo/domain/schedule/ScheduleRepositoryTest.java new file mode 100644 index 000000000..e723ee6ae --- /dev/null +++ b/backend/src/test/java/kr/momo/domain/schedule/ScheduleRepositoryTest.java @@ -0,0 +1,68 @@ +package kr.momo.domain.schedule; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; +import kr.momo.domain.attendee.Attendee; +import kr.momo.domain.attendee.AttendeeRepository; +import kr.momo.domain.availabledate.AvailableDate; +import kr.momo.domain.availabledate.AvailableDateRepository; +import kr.momo.domain.meeting.Meeting; +import kr.momo.domain.meeting.MeetingRepository; +import kr.momo.domain.timeslot.Timeslot; +import kr.momo.fixture.AttendeeFixture; +import kr.momo.fixture.AvailableDateFixture; +import kr.momo.fixture.MeetingFixture; +import kr.momo.support.IsolateDatabase; +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.jdbc.core.JdbcTemplate; + +@SpringBootTest +@IsolateDatabase +class ScheduleRepositoryTest { + + @Autowired + private JdbcTemplate jdbcTemplate; + + @Autowired + private ScheduleRepository scheduleRepository; + + @Autowired + private MeetingRepository meetingRepository; + + @Autowired + private AttendeeRepository attendeeRepository; + + @Autowired + private AvailableDateRepository availableDateRepository; + + private Attendee attendee; + private AvailableDate availableDate; + + @BeforeEach + void setUp() { + Meeting meeting = meetingRepository.save(MeetingFixture.COFFEE.create()); + attendee = attendeeRepository.save(AttendeeFixture.HOST_JAZZ.create(meeting)); + availableDate = availableDateRepository.save(AvailableDateFixture.TODAY.create(meeting)); + } + + @DisplayName("참가자의 스케쥴을 한 번에 삭제한다.") + @Test + void batchInsertTest() { + List schedules = List.of( + new Schedule(attendee, availableDate, Timeslot.TIME_0000), + new Schedule(attendee, availableDate, Timeslot.TIME_0130), + new Schedule(attendee, availableDate, Timeslot.TIME_0230) + ); + scheduleRepository.saveAll(schedules); + + scheduleRepository.deleteByAttendee(attendee); + + Integer count = jdbcTemplate.queryForObject("SELECT COUNT(*) FROM schedule", Integer.class); + assertThat(count).isEqualTo(0); + } +}