Skip to content

Commit

Permalink
[BE] JPA의 saveAll, deleteAll를 bulk query로 개선 (#273)
Browse files Browse the repository at this point in the history
* 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으로 조정
  • Loading branch information
seokmyungham authored Aug 22, 2024
1 parent fc19455 commit 8431474
Show file tree
Hide file tree
Showing 10 changed files with 306 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,8 @@ public boolean isBefore(LocalDate other) {
public boolean isEqual(LocalDate other) {
return date.isEqual(other);
}

public Long meetingId() {
return meeting.getId();
}
}
Original file line number Diff line number Diff line change
@@ -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<AvailableDate> availableDates) {
String sql = """
INSERT INTO available_date (date, meeting_id, created_at, modified_at)
VALUES (?, ?, ?, ?);
""";

executeBatchUpdate(availableDates, sql);
}

private void executeBatchUpdate(Collection<AvailableDate> availableDates, String sql) {
LocalDateTime now = LocalDateTime.now();
Timestamp timestamp = Timestamp.valueOf(now);

jdbcTemplate.batchUpdate(sql, availableDates, BATCH_SIZE, createPreparedStatementSetter(timestamp));
}

private ParameterizedPreparedStatementSetter<AvailableDate> 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);
};
}
}
8 changes: 8 additions & 0 deletions backend/src/main/java/kr/momo/domain/schedule/Schedule.java
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
}
Original file line number Diff line number Diff line change
@@ -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<Schedule> schedules) {
String sql = """
INSERT INTO schedule (attendee_id, available_date_id, timeslot, created_at, modified_at)
VALUES (?, ?, ?, ?, ?);
""";

executeBatchUpdate(schedules, sql);
}

private void executeBatchUpdate(Collection<Schedule> schedules, String sql) {
LocalDateTime now = LocalDateTime.now();
Timestamp timestamp = Timestamp.valueOf(now);

jdbcTemplate.batchUpdate(sql, schedules, BATCH_SIZE, createPreparedStatementSetter(timestamp));
}

private ParameterizedPreparedStatementSetter<Schedule> 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);
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<Schedule, Long> {

Expand All @@ -13,5 +16,8 @@ public interface ScheduleRepository extends JpaRepository<Schedule, Long> {
@EntityGraph(attributePaths = {"availableDate"})
List<Schedule> findAllByAttendeeIn(List<Attendee> attendees);

void deleteAllByAttendee(Attendee attendee);
@Modifying
@Transactional
@Query("DELETE FROM Schedule s WHERE s.attendee = :attendee")
void deleteByAttendee(Attendee attendee);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -33,14 +34,15 @@ 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) {
Meeting meeting = saveMeeting(request.meetingName(), request.toMeetingStartTime(), request.toMeetingEndTime());
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());

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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) {
Expand All @@ -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<Schedule> schedules = createSchedules(request, meeting, attendee);
scheduleRepository.saveAll(schedules);
scheduleBatchRepository.batchInsert(schedules);
}

private void validateMeetingUnLocked(Meeting meeting) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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> 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());
}
}
Original file line number Diff line number Diff line change
@@ -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<Schedule> 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());
}
}
Original file line number Diff line number Diff line change
@@ -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<Schedule> 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);
}
}

0 comments on commit 8431474

Please sign in to comment.