diff --git a/get-p-batch/build.gradle b/get-p-batch/build.gradle new file mode 100644 index 00000000..f011e57c --- /dev/null +++ b/get-p-batch/build.gradle @@ -0,0 +1,24 @@ +dependencies { + implementation(project(":get-p-domain")) + implementation(project(":get-p-persistence")) + implementation(testFixtures(project(":get-p-domain"))) + testImplementation(testFixtures(project(':get-p-domain'))) + + // Spring Data + implementation 'org.springframework.boot:spring-boot-starter-data-jpa:3.3.5' + + // Flyway + implementation 'org.flywaydb:flyway-core:9.16.3' + implementation 'org.flywaydb:flyway-mysql:9.16.3' + + // JDBC MySQL 드라이버 + runtimeOnly 'com.mysql:mysql-connector-j:9.0.0' +} + +bootJar { + enabled = true +} + +jar { + enabled = false +} \ No newline at end of file diff --git a/get-p-batch/src/main/java/es/princip/getp/batch/BatchInsertionException.java b/get-p-batch/src/main/java/es/princip/getp/batch/BatchInsertionException.java new file mode 100644 index 00000000..41d0e2c3 --- /dev/null +++ b/get-p-batch/src/main/java/es/princip/getp/batch/BatchInsertionException.java @@ -0,0 +1,12 @@ +package es.princip.getp.batch; + +public class BatchInsertionException extends RuntimeException { + + public BatchInsertionException(final String message) { + super(message); + } + + public BatchInsertionException(final String message, final Throwable cause) { + super(message, cause); + } +} diff --git a/get-p-batch/src/main/java/es/princip/getp/batch/GetpBatchApplication.java b/get-p-batch/src/main/java/es/princip/getp/batch/GetpBatchApplication.java new file mode 100644 index 00000000..0f55cdb2 --- /dev/null +++ b/get-p-batch/src/main/java/es/princip/getp/batch/GetpBatchApplication.java @@ -0,0 +1,34 @@ +package es.princip.getp.batch; + +import es.princip.getp.batch.project.commission.BatchDeleteProjectService; +import es.princip.getp.batch.project.commission.ParallelBatchInsertProjectService; +import es.princip.getp.batch.project.apply.BatchDeleteProjectApplicationService; +import es.princip.getp.batch.project.apply.BatchInsertProjectApplicationService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.CommandLineRunner; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class GetpBatchApplication implements CommandLineRunner { + + @Autowired private BatchDeleteProjectApplicationService batchDeleteProjectApplicationService; + @Autowired private BatchDeleteProjectService batchDeleteProjectService; + + @Autowired private BatchInsertProjectApplicationService batchInsertProjectApplicationService; + @Autowired private ParallelBatchInsertProjectService batchInsertProjectService; + + public static void main(String[] args) { + SpringApplication.run(GetpBatchApplication.class, args); + } + + private static final int PROJECT_SIZE = 100_000; + @Override + public void run(final String... args) { + batchDeleteProjectApplicationService.delete(); + batchDeleteProjectService.delete(); + + batchInsertProjectService.insert(PROJECT_SIZE); + batchInsertProjectApplicationService.insert(PROJECT_SIZE); + } +} diff --git a/get-p-batch/src/main/java/es/princip/getp/batch/UniqueLongGenerator.java b/get-p-batch/src/main/java/es/princip/getp/batch/UniqueLongGenerator.java new file mode 100644 index 00000000..86cfdcc2 --- /dev/null +++ b/get-p-batch/src/main/java/es/princip/getp/batch/UniqueLongGenerator.java @@ -0,0 +1,11 @@ +package es.princip.getp.batch; + +import java.util.concurrent.atomic.AtomicLong; + +public class UniqueLongGenerator { + private static final AtomicLong counter = new AtomicLong(); + + public static long generateUniqueLong() { + return counter.incrementAndGet(); + } +} \ No newline at end of file diff --git a/get-p-batch/src/main/java/es/princip/getp/batch/config/ExecutionTimer.java b/get-p-batch/src/main/java/es/princip/getp/batch/config/ExecutionTimer.java new file mode 100644 index 00000000..c6962c33 --- /dev/null +++ b/get-p-batch/src/main/java/es/princip/getp/batch/config/ExecutionTimer.java @@ -0,0 +1,30 @@ +package es.princip.getp.batch.config; + +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.springframework.stereotype.Component; + +@Slf4j +@Aspect +@Component +public class ExecutionTimer { + + @Pointcut("@annotation(es.princip.getp.batch.config.ExtendsWithExecutionTimer)") + private void timer() {} + + @Around("timer()") + public Object AssumeExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable { + final long start = System.currentTimeMillis(); + try { + return joinPoint.proceed(); + } finally { + final long finish = System.currentTimeMillis(); + final long executionTime = finish - start; + final String signature = joinPoint.getSignature().toShortString(); + log.info("execution time of {}: {}ms", signature, executionTime); + } + } +} \ No newline at end of file diff --git a/get-p-batch/src/main/java/es/princip/getp/batch/config/ExtendsWithExecutionTimer.java b/get-p-batch/src/main/java/es/princip/getp/batch/config/ExtendsWithExecutionTimer.java new file mode 100644 index 00000000..bf601c80 --- /dev/null +++ b/get-p-batch/src/main/java/es/princip/getp/batch/config/ExtendsWithExecutionTimer.java @@ -0,0 +1,11 @@ +package es.princip.getp.batch.config; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE}) +@Retention(RetentionPolicy.RUNTIME) +public @interface ExtendsWithExecutionTimer { +} diff --git a/get-p-batch/src/main/java/es/princip/getp/batch/parallel/ParallelBatchInsertService.java b/get-p-batch/src/main/java/es/princip/getp/batch/parallel/ParallelBatchInsertService.java new file mode 100644 index 00000000..e4bb801a --- /dev/null +++ b/get-p-batch/src/main/java/es/princip/getp/batch/parallel/ParallelBatchInsertService.java @@ -0,0 +1,51 @@ +package es.princip.getp.batch.parallel; + +import es.princip.getp.batch.BatchInsertionException; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +@Slf4j +@Service +public class ParallelBatchInsertService { + + @Getter + private final int numThreads = Runtime.getRuntime().availableProcessors(); + + public void insert(final int size, final ParallelBatchInserter batchInserter) { + final ExecutorService executorService = Executors.newFixedThreadPool(numThreads); + final List> futures = new ArrayList<>(); + final int batchSize = size / numThreads; + for (int i = 0; i < numThreads; i++) { + final int start = i * batchSize + 1; + final int end = (i == numThreads - 1) ? size : start + batchSize - 1; + final CompletableFuture future = CompletableFuture.runAsync(() -> { + final String threadName = Thread.currentThread().getName(); + log.info("Thread {} is inserting projects from {} to {}", threadName, start, end); + try { + batchInserter.insert(start, end); + } catch (final Exception exception) { + throw new BatchInsertionException( + String.format( + "Thread %s encountered an error during batch insert for range %d to %d: ", + threadName, + start, + end + ), + exception + ); + } + }, executorService); + futures.add(future); + } + CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join(); + executorService.shutdown(); + log.info("All threads completed. Executor service shutdown."); + } +} diff --git a/get-p-batch/src/main/java/es/princip/getp/batch/parallel/ParallelBatchInserter.java b/get-p-batch/src/main/java/es/princip/getp/batch/parallel/ParallelBatchInserter.java new file mode 100644 index 00000000..3f0fcf16 --- /dev/null +++ b/get-p-batch/src/main/java/es/princip/getp/batch/parallel/ParallelBatchInserter.java @@ -0,0 +1,6 @@ +package es.princip.getp.batch.parallel; + +@FunctionalInterface +public interface ParallelBatchInserter { + void insert(int start, int end); +} diff --git a/get-p-batch/src/main/java/es/princip/getp/batch/project/apply/BatchDeleteProjectApplicationService.java b/get-p-batch/src/main/java/es/princip/getp/batch/project/apply/BatchDeleteProjectApplicationService.java new file mode 100644 index 00000000..91feb494 --- /dev/null +++ b/get-p-batch/src/main/java/es/princip/getp/batch/project/apply/BatchDeleteProjectApplicationService.java @@ -0,0 +1,27 @@ +package es.princip.getp.batch.project.apply; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Service; + +@Slf4j +@Service +@RequiredArgsConstructor +public class BatchDeleteProjectApplicationService { + + private final JdbcTemplate jdbcTemplate; + + public void delete() { + jdbcTemplate.execute("delete from team_project_application_teammate"); + jdbcTemplate.execute("delete from team_project_application"); + jdbcTemplate.execute("delete from individual_project_application"); + jdbcTemplate.execute("delete from project_application_attachment_file"); + jdbcTemplate.execute("delete from project_application"); + log.info("Table \"team_project_application_teammate\" is dropped"); + log.info("Table \"team_project_application\" is dropped"); + log.info("Table \"individual_project_application\" is dropped"); + log.info("Table \"project_application_attachment_file\" is dropped"); + log.info("Table \"project_application\" is dropped"); + } +} diff --git a/get-p-batch/src/main/java/es/princip/getp/batch/project/apply/BatchInsertIndividualProjectApplicationJdbcService.java b/get-p-batch/src/main/java/es/princip/getp/batch/project/apply/BatchInsertIndividualProjectApplicationJdbcService.java new file mode 100644 index 00000000..55f5e111 --- /dev/null +++ b/get-p-batch/src/main/java/es/princip/getp/batch/project/apply/BatchInsertIndividualProjectApplicationJdbcService.java @@ -0,0 +1,45 @@ +package es.princip.getp.batch.project.apply; + +import es.princip.getp.domain.project.apply.model.IndividualProjectApplication; +import es.princip.getp.domain.project.apply.model.ProjectApplication; +import lombok.RequiredArgsConstructor; +import org.springframework.jdbc.core.BatchPreparedStatementSetter; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Service; + +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.List; + +@Service +@RequiredArgsConstructor +class BatchInsertIndividualProjectApplicationJdbcService { + + private final JdbcTemplate jdbcTemplate; + private static final String sql = + """ + insert into individual_project_application ( + project_application_id + ) values (?); + """; + + public void batchUpdate(final List applications) { + final List individuals = applications.stream() + .filter(IndividualProjectApplication.class::isInstance) + .map(IndividualProjectApplication.class::cast) + .toList(); + + jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() { + @Override + public void setValues(final PreparedStatement ps, final int i) throws SQLException { + final IndividualProjectApplication application = individuals.get(i); + ps.setLong(1, application.getId().getValue()); + } + + @Override + public int getBatchSize() { + return individuals.size(); + } + }); + } +} diff --git a/get-p-batch/src/main/java/es/princip/getp/batch/project/apply/BatchInsertProjectApplicationAttachmentFileJdbcService.java b/get-p-batch/src/main/java/es/princip/getp/batch/project/apply/BatchInsertProjectApplicationAttachmentFileJdbcService.java new file mode 100644 index 00000000..0c7dadd4 --- /dev/null +++ b/get-p-batch/src/main/java/es/princip/getp/batch/project/apply/BatchInsertProjectApplicationAttachmentFileJdbcService.java @@ -0,0 +1,58 @@ +package es.princip.getp.batch.project.apply; + +import es.princip.getp.domain.common.model.AttachmentFile; +import es.princip.getp.domain.project.apply.model.ProjectApplication; +import es.princip.getp.domain.project.apply.model.ProjectApplicationId; +import lombok.RequiredArgsConstructor; +import org.springframework.jdbc.core.BatchPreparedStatementSetter; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Service; + +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.List; + +@Service +@RequiredArgsConstructor +class BatchInsertProjectApplicationAttachmentFileJdbcService { + + private record ProjectApplicationIdAttachmentFile( + ProjectApplicationId applicationId, + AttachmentFile attachmentFile + ) { + } + + private final JdbcTemplate jdbcTemplate; + private static final String sql = + """ + insert into project_application_attachment_file( + project_application_id, + attachment_files + ) values (?, ?); + """; + + public void batchUpdate(final List applications) { + final List attachmentFiles = applications.stream() + .flatMap(application -> application.getAttachmentFiles().stream() + .map(attachmentFile -> new ProjectApplicationIdAttachmentFile( + application.getId(), + attachmentFile + ))) + .toList(); + + jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() { + @Override + public void setValues(final PreparedStatement ps, final int i) throws SQLException { + final ProjectApplicationId applicationId = attachmentFiles.get(i).applicationId(); + final AttachmentFile attachmentFile = attachmentFiles.get(i).attachmentFile(); + ps.setLong(1, applicationId.getValue()); + ps.setString(2, attachmentFile.getUrl().getValue()); + } + + @Override + public int getBatchSize() { + return attachmentFiles.size(); + } + }); + } +} diff --git a/get-p-batch/src/main/java/es/princip/getp/batch/project/apply/BatchInsertProjectApplicationJdbcService.java b/get-p-batch/src/main/java/es/princip/getp/batch/project/apply/BatchInsertProjectApplicationJdbcService.java new file mode 100644 index 00000000..73ca614d --- /dev/null +++ b/get-p-batch/src/main/java/es/princip/getp/batch/project/apply/BatchInsertProjectApplicationJdbcService.java @@ -0,0 +1,65 @@ +package es.princip.getp.batch.project.apply; + +import es.princip.getp.domain.project.apply.model.IndividualProjectApplication; +import es.princip.getp.domain.project.apply.model.ProjectApplication; +import es.princip.getp.domain.project.apply.model.TeamProjectApplication; +import lombok.RequiredArgsConstructor; +import org.springframework.jdbc.core.BatchPreparedStatementSetter; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Service; + +import java.sql.Date; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.List; + +@Service +@RequiredArgsConstructor +class BatchInsertProjectApplicationJdbcService { + + private final BatchInsertProjectApplicationAttachmentFileJdbcService attachmentFileJdbcService; + private final BatchInsertTeamProjectApplicationJdbcService teamJdbcService; + private final BatchInsertIndividualProjectApplicationJdbcService individualJdbcService; + private final JdbcTemplate jdbcTemplate; + private static final String sql = + """ + insert into project_application ( + project_application_id, + expected_end_date, + expected_start_date, + description, + status, + people_id, + project_id, + dtype + ) values (?, ?, ?, ?, ?, ?, ?, ?); + """; + + public void batchUpdate(final List applications) { + jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() { + @Override + public void setValues(final PreparedStatement ps, final int i) throws SQLException { + final ProjectApplication application = applications.get(i); + final String dtype = (application instanceof TeamProjectApplication) ? + TeamProjectApplication.TYPE : IndividualProjectApplication.TYPE; + ps.setLong(1, application.getId().getValue()); + ps.setDate(2, Date.valueOf(application.getExpectedDuration().getEndDate())); + ps.setDate(3, Date.valueOf(application.getExpectedDuration().getStartDate())); + ps.setString(4, application.getDescription()); + ps.setString(5, application.getStatus().toString()); + ps.setLong(6, application.getApplicantId().getValue()); + ps.setLong(7, application.getProjectId().getValue()); + ps.setString(8,dtype); + } + + @Override + public int getBatchSize() { + return applications.size(); + } + }); + + attachmentFileJdbcService.batchUpdate(applications); + individualJdbcService.batchUpdate(applications); + teamJdbcService.batchUpdate(applications); + } +} diff --git a/get-p-batch/src/main/java/es/princip/getp/batch/project/apply/BatchInsertProjectApplicationService.java b/get-p-batch/src/main/java/es/princip/getp/batch/project/apply/BatchInsertProjectApplicationService.java new file mode 100644 index 00000000..b392207e --- /dev/null +++ b/get-p-batch/src/main/java/es/princip/getp/batch/project/apply/BatchInsertProjectApplicationService.java @@ -0,0 +1,65 @@ +package es.princip.getp.batch.project.apply; + +import es.princip.getp.batch.UniqueLongGenerator; +import es.princip.getp.batch.config.ExtendsWithExecutionTimer; +import es.princip.getp.batch.parallel.ParallelBatchInsertService; +import es.princip.getp.domain.people.model.PeopleId; +import es.princip.getp.domain.project.apply.model.*; +import es.princip.getp.domain.project.commission.model.ProjectId; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +import static es.princip.getp.fixture.project.ProjectApplicationFixture.*; + +@Slf4j +@Service +@RequiredArgsConstructor +public class BatchInsertProjectApplicationService { + + private final BatchInsertProjectApplicationJdbcService jdbcService; + private final ParallelBatchInsertService batchInsertService; + + @ExtendsWithExecutionTimer + public void insert(final int projectSize) { + batchInsertService.insert(projectSize, (start, end) -> { + final List applications = new ArrayList<>(); + for (int i = start; i <= end; i++) { + final ProjectId projectId = new ProjectId((long) i); + applications.add(individualProjectApplication( + new ProjectApplicationId(UniqueLongGenerator.generateUniqueLong()), + new PeopleId(1L), + projectId + )); + applications.add(teamProjectApplication( + new ProjectApplicationId(UniqueLongGenerator.generateUniqueLong()), + new PeopleId(2L), + projectId, + ProjectApplicationStatus.COMPLETED, + Set.of( + teammate( + new TeammateId(UniqueLongGenerator.generateUniqueLong()), + new PeopleId(3L), + TeammateStatus.APPROVED + ), + teammate( + new TeammateId(UniqueLongGenerator.generateUniqueLong()), + new PeopleId(4L), + TeammateStatus.APPROVED + ), + teammate( + new TeammateId(UniqueLongGenerator.generateUniqueLong()), + new PeopleId(5L), + TeammateStatus.APPROVED + ) + ) + )); + } + jdbcService.batchUpdate(applications); + }); + } +} diff --git a/get-p-batch/src/main/java/es/princip/getp/batch/project/apply/BatchInsertTeamProjectApplicationJdbcService.java b/get-p-batch/src/main/java/es/princip/getp/batch/project/apply/BatchInsertTeamProjectApplicationJdbcService.java new file mode 100644 index 00000000..8893e091 --- /dev/null +++ b/get-p-batch/src/main/java/es/princip/getp/batch/project/apply/BatchInsertTeamProjectApplicationJdbcService.java @@ -0,0 +1,48 @@ +package es.princip.getp.batch.project.apply; + +import es.princip.getp.domain.project.apply.model.ProjectApplication; +import es.princip.getp.domain.project.apply.model.TeamProjectApplication; +import lombok.RequiredArgsConstructor; +import org.springframework.jdbc.core.BatchPreparedStatementSetter; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Service; + +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.List; + +@Service +@RequiredArgsConstructor +class BatchInsertTeamProjectApplicationJdbcService { + + private final BatchInsertTeamProjectApplicationTeammateJdbcService teammateJdbcService; + private final JdbcTemplate jdbcTemplate; + private static final String sql = + """ + insert into team_project_application ( + project_application_id + ) values (?); + """; + + public void batchUpdate(final List applications) { + final List teams = applications.stream() + .filter(TeamProjectApplication.class::isInstance) + .map(TeamProjectApplication.class::cast) + .toList(); + + jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() { + @Override + public void setValues(final PreparedStatement ps, final int i) throws SQLException { + final TeamProjectApplication application = teams.get(i); + ps.setLong(1, application.getId().getValue()); + } + + @Override + public int getBatchSize() { + return teams.size(); + } + }); + + teammateJdbcService.batchUpdate(teams); + } +} diff --git a/get-p-batch/src/main/java/es/princip/getp/batch/project/apply/BatchInsertTeamProjectApplicationTeammateJdbcService.java b/get-p-batch/src/main/java/es/princip/getp/batch/project/apply/BatchInsertTeamProjectApplicationTeammateJdbcService.java new file mode 100644 index 00000000..4e0c140c --- /dev/null +++ b/get-p-batch/src/main/java/es/princip/getp/batch/project/apply/BatchInsertTeamProjectApplicationTeammateJdbcService.java @@ -0,0 +1,59 @@ +package es.princip.getp.batch.project.apply; + +import es.princip.getp.domain.project.apply.model.ProjectApplicationId; +import es.princip.getp.domain.project.apply.model.TeamProjectApplication; +import es.princip.getp.domain.project.apply.model.Teammate; +import lombok.RequiredArgsConstructor; +import org.springframework.jdbc.core.BatchPreparedStatementSetter; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Service; + +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.List; + +@Service +@RequiredArgsConstructor +class BatchInsertTeamProjectApplicationTeammateJdbcService { + + private record ProjectApplicationIdTeammate( + ProjectApplicationId applicationId, + Teammate teammate + ) { + } + + private final JdbcTemplate jdbcTemplate; + private static final String sql = + """ + insert into team_project_application_teammate ( + teammate_id, + people_id, + status, + project_application_id + ) values (?, ?, ?, ?) + """; + + public void batchUpdate(final List applications) { + final List teammates = applications.stream() + .flatMap(application -> application.getTeammates().stream() + .map(teammate -> new ProjectApplicationIdTeammate(application.getId(), teammate))) + .toList(); + + jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() { + @Override + public void setValues(final PreparedStatement ps, final int i) throws SQLException { + final ProjectApplicationId applicationId = teammates.get(i).applicationId(); + final Teammate teammate = teammates.get(i).teammate(); + ps.setLong(1, teammate.getId().getValue()); + ps.setLong(2, teammate.getPeopleId().getValue()); + ps.setString(3, teammate.getStatus().toString()); + ps.setLong(4, applicationId.getValue()); + } + + @Override + public int getBatchSize() { + return teammates.size(); + } + }); + } +} diff --git a/get-p-batch/src/main/java/es/princip/getp/batch/project/commission/BatchDeleteProjectService.java b/get-p-batch/src/main/java/es/princip/getp/batch/project/commission/BatchDeleteProjectService.java new file mode 100644 index 00000000..60a0a8c2 --- /dev/null +++ b/get-p-batch/src/main/java/es/princip/getp/batch/project/commission/BatchDeleteProjectService.java @@ -0,0 +1,23 @@ +package es.princip.getp.batch.project.commission; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Service; + +@Slf4j +@Service +@RequiredArgsConstructor +public class BatchDeleteProjectService { + + private final JdbcTemplate jdbcTemplate; + + public void delete() { + jdbcTemplate.execute("delete from project_hashtag"); + jdbcTemplate.execute("delete from project_attachment_file"); + jdbcTemplate.execute("delete from project"); + log.info("Table \"project_hashtag\" is dropped"); + log.info("Table \"project_attachment_file\" is dropped"); + log.info("Table \"project\" is dropped"); + } +} diff --git a/get-p-batch/src/main/java/es/princip/getp/batch/project/commission/BatchInsertProjectAttachmentFileJdbcService.java b/get-p-batch/src/main/java/es/princip/getp/batch/project/commission/BatchInsertProjectAttachmentFileJdbcService.java new file mode 100644 index 00000000..d4c24504 --- /dev/null +++ b/get-p-batch/src/main/java/es/princip/getp/batch/project/commission/BatchInsertProjectAttachmentFileJdbcService.java @@ -0,0 +1,53 @@ +package es.princip.getp.batch.project.commission; + +import es.princip.getp.domain.common.model.AttachmentFile; +import es.princip.getp.domain.project.commission.model.Project; +import es.princip.getp.domain.project.commission.model.ProjectId; +import lombok.RequiredArgsConstructor; +import org.springframework.jdbc.core.BatchPreparedStatementSetter; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Service; + +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.List; + +@Service +@RequiredArgsConstructor +class BatchInsertProjectAttachmentFileJdbcService { + + private record ProjectIdAttachmentFile( + ProjectId projectId, + AttachmentFile attachmentFile + ) { + } + + private final JdbcTemplate jdbcTemplate; + private static final String sql = + """ + insert into project_attachment_file (project_id, attachment_files) + values (?, ?); + """; + + public void batchUpdate(final List projects) { + final List attachmentFiles = projects.stream() + .flatMap(project -> project.getAttachmentFiles().stream() + .map(file -> new ProjectIdAttachmentFile(project.getId(), file))) + .toList(); + + jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() { + @Override + public void setValues(final PreparedStatement ps, final int i) throws SQLException { + final Long projectId = attachmentFiles.get(i).projectId().getValue(); + final String attachmentFileUrl = attachmentFiles.get(i).attachmentFile().getUrl().getValue(); + ps.setLong(1, projectId); + ps.setString(2, attachmentFileUrl); + } + + @Override + public int getBatchSize() { + return attachmentFiles.size(); + } + }); + } +} diff --git a/get-p-batch/src/main/java/es/princip/getp/batch/project/commission/BatchInsertProjectHashtagJdbcService.java b/get-p-batch/src/main/java/es/princip/getp/batch/project/commission/BatchInsertProjectHashtagJdbcService.java new file mode 100644 index 00000000..54aac7c9 --- /dev/null +++ b/get-p-batch/src/main/java/es/princip/getp/batch/project/commission/BatchInsertProjectHashtagJdbcService.java @@ -0,0 +1,53 @@ +package es.princip.getp.batch.project.commission; + +import es.princip.getp.domain.common.model.Hashtag; +import es.princip.getp.domain.project.commission.model.Project; +import es.princip.getp.domain.project.commission.model.ProjectId; +import lombok.RequiredArgsConstructor; +import org.springframework.jdbc.core.BatchPreparedStatementSetter; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Service; + +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.List; + +@Service +@RequiredArgsConstructor +class BatchInsertProjectHashtagJdbcService { + + private record ProjectIdHashtag( + ProjectId projectId, + Hashtag hashtag + ) { + } + + private final JdbcTemplate jdbcTemplate; + private static final String sql = + """ + insert into project_hashtag (project_id, hashtags) + values (?, ?) + """; + + public void batchUpdate(final List projects) { + final List hashtags = projects.stream() + .flatMap(project -> project.getHashtags().stream() + .map(hashtag -> new ProjectIdHashtag(project.getId(), hashtag))) + .toList(); + + jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() { + @Override + public void setValues(final PreparedStatement ps, final int i) throws SQLException { + final Long projectId = hashtags.get(i).projectId().getValue(); + final String hashtag = hashtags.get(i).hashtag().getValue(); + ps.setLong(1, projectId); + ps.setString(2, hashtag); + } + + @Override + public int getBatchSize() { + return hashtags.size(); + } + }); + } +} diff --git a/get-p-batch/src/main/java/es/princip/getp/batch/project/commission/BatchInsertProjectJdbcService.java b/get-p-batch/src/main/java/es/princip/getp/batch/project/commission/BatchInsertProjectJdbcService.java new file mode 100644 index 00000000..618b562d --- /dev/null +++ b/get-p-batch/src/main/java/es/princip/getp/batch/project/commission/BatchInsertProjectJdbcService.java @@ -0,0 +1,74 @@ +package es.princip.getp.batch.project.commission; + +import es.princip.getp.domain.project.commission.model.Project; +import lombok.RequiredArgsConstructor; +import org.springframework.jdbc.core.BatchPreparedStatementSetter; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Service; + +import java.sql.Date; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.time.LocalDateTime; +import java.util.List; + +@Service +@RequiredArgsConstructor +class BatchInsertProjectJdbcService { + + private final BatchInsertProjectHashtagJdbcService hashtagJdbcService; + private final BatchInsertProjectAttachmentFileJdbcService attachmentFileJdbcService; + private final JdbcTemplate jdbcTemplate; + private static final String sql = + """ + insert into project ( + project_id, + application_end_date, + application_start_date, + estimated_end_date, + estimated_start_date, + payment, + category, + description, + meeting_type, + status, + title, + client_id, + recruitment_count, + created_at, + updated_at + ) + values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """; + + public void batchUpdate(final List projects) { + jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() { + @Override + public void setValues(final PreparedStatement ps, final int i) throws SQLException { + final Project project = projects.get(i); + ps.setLong(1, project.getId().getValue()); + ps.setDate(2, Date.valueOf(project.getApplicationDuration().getEndDate())); + ps.setDate(3, Date.valueOf(project.getApplicationDuration().getStartDate())); + ps.setDate(4, Date.valueOf(project.getEstimatedDuration().getEndDate())); + ps.setDate(5, Date.valueOf(project.getEstimatedDuration().getStartDate())); + ps.setLong(6, project.getPayment()); + ps.setString(7, project.getCategory().toString()); + ps.setString(8, project.getDescription()); + ps.setString(9, project.getMeetingType().toString()); + ps.setString(10, project.getStatus().toString()); + ps.setString(11, project.getTitle()); + ps.setLong(12, project.getClientId().getValue()); + ps.setLong(13, project.getRecruitmentCount()); + ps.setString(14, String.valueOf(LocalDateTime.now())); + ps.setString(15, String.valueOf(LocalDateTime.now())); + } + + @Override + public int getBatchSize() { + return projects.size(); + } + }); + hashtagJdbcService.batchUpdate(projects); + attachmentFileJdbcService.batchUpdate(projects); + } +} diff --git a/get-p-batch/src/main/java/es/princip/getp/batch/project/commission/BatchInsertProjectService.java b/get-p-batch/src/main/java/es/princip/getp/batch/project/commission/BatchInsertProjectService.java new file mode 100644 index 00000000..24ee5230 --- /dev/null +++ b/get-p-batch/src/main/java/es/princip/getp/batch/project/commission/BatchInsertProjectService.java @@ -0,0 +1,6 @@ +package es.princip.getp.batch.project.commission; + +public interface BatchInsertProjectService { + + void insert(int size); +} diff --git a/get-p-batch/src/main/java/es/princip/getp/batch/project/commission/ParallelBatchInsertProjectService.java b/get-p-batch/src/main/java/es/princip/getp/batch/project/commission/ParallelBatchInsertProjectService.java new file mode 100644 index 00000000..40ad36eb --- /dev/null +++ b/get-p-batch/src/main/java/es/princip/getp/batch/project/commission/ParallelBatchInsertProjectService.java @@ -0,0 +1,40 @@ +package es.princip.getp.batch.project.commission; + +import es.princip.getp.batch.config.ExtendsWithExecutionTimer; +import es.princip.getp.batch.parallel.ParallelBatchInsertService; +import es.princip.getp.domain.client.model.ClientId; +import es.princip.getp.domain.project.commission.model.Project; +import es.princip.getp.domain.project.commission.model.ProjectId; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.stream.LongStream; + +import static es.princip.getp.domain.project.commission.model.ProjectStatus.APPLICATION_OPENED; +import static es.princip.getp.fixture.project.ProjectFixture.project; + +@Slf4j +@Service +@RequiredArgsConstructor +public class ParallelBatchInsertProjectService implements BatchInsertProjectService { + + private final BatchInsertProjectJdbcService jdbcService; + private final ParallelBatchInsertService batchInsertService; + + @ExtendsWithExecutionTimer + public void insert(final int size) { + batchInsertService.insert(size, (start, end) -> { + final List projects = LongStream.rangeClosed(start, end) + .boxed() + .map(id -> project( + new ProjectId(id), + new ClientId(id), + APPLICATION_OPENED + )) + .toList(); + jdbcService.batchUpdate(projects); + }); + } +} diff --git a/get-p-batch/src/main/java/es/princip/getp/batch/project/commission/SequentialBatchInsertProjectService.java b/get-p-batch/src/main/java/es/princip/getp/batch/project/commission/SequentialBatchInsertProjectService.java new file mode 100644 index 00000000..892ebb8e --- /dev/null +++ b/get-p-batch/src/main/java/es/princip/getp/batch/project/commission/SequentialBatchInsertProjectService.java @@ -0,0 +1,36 @@ +package es.princip.getp.batch.project.commission; + +import es.princip.getp.batch.config.ExtendsWithExecutionTimer; +import es.princip.getp.domain.client.model.ClientId; +import es.princip.getp.domain.project.commission.model.Project; +import es.princip.getp.domain.project.commission.model.ProjectId; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.stream.LongStream; + +import static es.princip.getp.domain.project.commission.model.ProjectStatus.APPLICATION_OPENED; +import static es.princip.getp.fixture.project.ProjectFixture.project; + +@Slf4j +@Service +@RequiredArgsConstructor +public class SequentialBatchInsertProjectService implements BatchInsertProjectService { + + private final BatchInsertProjectJdbcService jdbcService; + + @ExtendsWithExecutionTimer + public void insert(final int size) { + final List projects = LongStream.rangeClosed(1, size) + .boxed() + .map(id -> project( + new ProjectId(id), + new ClientId(id), + APPLICATION_OPENED + )) + .toList(); + jdbcService.batchUpdate(projects); + } +} diff --git a/get-p-batch/src/main/resources/application.yml b/get-p-batch/src/main/resources/application.yml new file mode 100644 index 00000000..055dc92f --- /dev/null +++ b/get-p-batch/src/main/resources/application.yml @@ -0,0 +1,31 @@ +spring: + datasource: + url: ${DB_URL}?&rewriteBatchedStatements=true&profileSQL=true&logger=Slf4JLogger + username: ${DB_USERNAME} + password: ${DB_PASSWORD} + + flyway: + enabled: true + baseline-on-migrate: true + + jpa: + hibernate: + ddl-auto: validate + properties: + hibernate: + show_sql: true + format_sql: true + highlight_sql: true + use_sql_comments: true + jdbc: + time_zone: Asia/Seoul + default_batch_fetch_size: 20 + dialect: org.hibernate.dialect.MySQLDialect + +logging: + level: + org: + type: + descriptor: + sql: + BasicBinder: TRACE \ No newline at end of file diff --git a/get-p-domain/src/testFixtures/java/es/princip/getp/fixture/project/ProjectApplicationFixture.java b/get-p-domain/src/testFixtures/java/es/princip/getp/fixture/project/ProjectApplicationFixture.java index b9cd21d4..93d2a779 100644 --- a/get-p-domain/src/testFixtures/java/es/princip/getp/fixture/project/ProjectApplicationFixture.java +++ b/get-p-domain/src/testFixtures/java/es/princip/getp/fixture/project/ProjectApplicationFixture.java @@ -22,17 +22,40 @@ public static Duration expectedDuration() { ); } + private static IndividualProjectApplication.IndividualProjectApplicationBuilder individualBuilder() { + return IndividualProjectApplication.builder() + .expectedDuration(expectedDuration()) + .status(ACCEPTED) + .description(DESCRIPTION) + .attachmentFiles(attachmentFiles()); + } + + private static TeamProjectApplication.TeamProjectApplicationBuilder teamBuilder() { + return TeamProjectApplication.builder() + .expectedDuration(expectedDuration()) + .description(DESCRIPTION) + .attachmentFiles(attachmentFiles()); + } + public static ProjectApplication individualProjectApplication( final PeopleId peopleId, final ProjectId projectId ) { - return IndividualProjectApplication.builder() + return individualBuilder() + .applicantId(peopleId) + .projectId(projectId) + .build(); + } + + public static ProjectApplication individualProjectApplication( + final ProjectApplicationId applicationId, + final PeopleId peopleId, + final ProjectId projectId + ) { + return individualBuilder() + .id(applicationId) .applicantId(peopleId) .projectId(projectId) - .expectedDuration(expectedDuration()) - .status(ACCEPTED) - .description(DESCRIPTION) - .attachmentFiles(attachmentFiles()) .build(); } @@ -41,20 +64,48 @@ public static TeamProjectApplication teamProjectApplication( final ProjectId projectId, final ProjectApplicationStatus status, final Set teammates - ) { - return TeamProjectApplication.builder() + ) { + return teamBuilder() .applicantId(peopleId) .projectId(projectId) - .expectedDuration(expectedDuration()) .status(status) - .description(DESCRIPTION) - .attachmentFiles(attachmentFiles()) .teammates(teammates) .build(); } - public static Teammate teammate(final PeopleId peopleId, final TeammateStatus status) { + public static TeamProjectApplication teamProjectApplication( + final ProjectApplicationId applicationId, + final PeopleId peopleId, + final ProjectId projectId, + final ProjectApplicationStatus status, + final Set teammates + ) { + return teamBuilder() + .id(applicationId) + .applicantId(peopleId) + .projectId(projectId) + .status(status) + .teammates(teammates) + .build(); + } + + public static Teammate teammate( + final PeopleId peopleId, + final TeammateStatus status + ) { + return Teammate.builder() + .peopleId(peopleId) + .status(status) + .build(); + } + + public static Teammate teammate( + final TeammateId teammateId, + final PeopleId peopleId, + final TeammateStatus status + ) { return Teammate.builder() + .id(teammateId) .peopleId(peopleId) .status(status) .build(); diff --git a/get-p-domain/src/testFixtures/java/es/princip/getp/fixture/project/ProjectFixture.java b/get-p-domain/src/testFixtures/java/es/princip/getp/fixture/project/ProjectFixture.java index 780f3812..257b81f8 100644 --- a/get-p-domain/src/testFixtures/java/es/princip/getp/fixture/project/ProjectFixture.java +++ b/get-p-domain/src/testFixtures/java/es/princip/getp/fixture/project/ProjectFixture.java @@ -2,10 +2,7 @@ import es.princip.getp.domain.client.model.ClientId; import es.princip.getp.domain.common.model.Duration; -import es.princip.getp.domain.project.commission.model.MeetingType; -import es.princip.getp.domain.project.commission.model.Project; -import es.princip.getp.domain.project.commission.model.ProjectCategory; -import es.princip.getp.domain.project.commission.model.ProjectStatus; +import es.princip.getp.domain.project.commission.model.*; import java.time.LocalDate; @@ -23,22 +20,41 @@ public class ProjectFixture { public static final LocalDate ESTIMATED_START_DATE = LocalDate.of(2024, 8, 1); public static final LocalDate ESTIMATED_END_DATE = LocalDate.of(2024, 8, 31); - private static final Project.ProjectBuilder builder = Project.builder() - .category(ProjectCategory.BACKEND) - .attachmentFiles(attachmentFiles()) - .payment(PAYMENT) - .recruitmentCount(RECRUITMENT_COUNT) - .title(TITLE) - .description(DESCRIPTION) - .meetingType(MeetingType.IN_PERSON) - .estimatedDuration(Duration.of( - ESTIMATED_START_DATE, - ESTIMATED_END_DATE - )) - .hashtags(hashtags()); + public static Project.ProjectBuilder builder() { + return Project.builder() + .category(ProjectCategory.BACKEND) + .attachmentFiles(attachmentFiles()) + .payment(PAYMENT) + .recruitmentCount(RECRUITMENT_COUNT) + .title(TITLE) + .description(DESCRIPTION) + .meetingType(MeetingType.IN_PERSON) + .estimatedDuration(Duration.of( + ESTIMATED_START_DATE, + ESTIMATED_END_DATE + )) + .hashtags(hashtags()); + } + + public static Project project( + final ProjectId projectId, + final ClientId clientId, + final ProjectStatus status + ) { + return builder() + .id(projectId) + .clientId(clientId) + .status(status) + .applicationDuration(Duration.of( + APPLICATION_START_DATE, + APPLICATION_END_DATE + )) + .build(); + } public static Project project(final ClientId clientId, final ProjectStatus status) { - return builder.clientId(clientId) + return builder() + .clientId(clientId) .status(status) .applicationDuration(Duration.of( APPLICATION_START_DATE, @@ -52,7 +68,8 @@ public static Project project( final ProjectStatus status, final Duration applicationDuration ) { - return builder.clientId(clientId) + return builder() + .clientId(clientId) .status(status) .applicationDuration(applicationDuration) .build(); diff --git a/get-p-persistence/src/main/resources/db/migration/V8__drop_team_project_application_team.sql b/get-p-persistence/src/main/resources/db/migration/V8__drop_team_project_application_team.sql new file mode 100644 index 00000000..b78f62c0 --- /dev/null +++ b/get-p-persistence/src/main/resources/db/migration/V8__drop_team_project_application_team.sql @@ -0,0 +1 @@ +drop table team_project_application_team; \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index 172df1db..5dbd9429 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,9 +1,8 @@ rootProject.name = 'get-p-server' -include( - "get-p-api", - "get-p-domain", - "get-p-application", - "get-p-persistence", - "get-p-infrastructure" -) \ No newline at end of file +include 'get-p-api' +include 'get-p-domain' +include 'get-p-application' +include 'get-p-persistence' +include 'get-p-infrastructure' +include 'get-p-batch' \ No newline at end of file