Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ dependencies {
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'com.h2database:h2'

// batch
implementation 'org.springframework.boot:spring-boot-starter-batch'

}

tasks.named('test') {
Expand Down
17 changes: 17 additions & 0 deletions src/main/java/NextLevel/demo/config/BatchConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package NextLevel.demo.config;

import jakarta.persistence.EntityManagerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;

@Configuration
public class BatchConfig {

@Bean
public PlatformTransactionManager transactionManager(EntityManagerFactory emf) {
return new JpaTransactionManager(emf);
}

}
20 changes: 20 additions & 0 deletions src/main/java/NextLevel/demo/project/batch/BatchController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package NextLevel.demo.project.batch;

import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
@RequiredArgsConstructor
public class BatchController {

private final ProjectBatchService projectBatchService;

@GetMapping("/admin/batch")
public ResponseEntity doBatch() {
projectBatchService.runProjectStatusJob();
return ResponseEntity.ok().build();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package NextLevel.demo.project.batch;

import NextLevel.demo.project.ProjectStatus;
import NextLevel.demo.project.project.entity.ProjectEntity;
import java.io.Serializable;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@NoArgsConstructor
@Getter
@Setter
public class ProjectAndFundingPriceDto implements Serializable {

private ProjectSerializableDto projectSerializableDto;
private Integer fundingPrice;
private ProjectStatus projectStatus;

public ProjectAndFundingPriceDto(ProjectSerializableDto projectSerializableDto, Integer fundingPrice) {
this.projectSerializableDto = projectSerializableDto;
this.fundingPrice = fundingPrice;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package NextLevel.demo.project.batch;

import NextLevel.demo.exception.CustomException;
import NextLevel.demo.exception.ErrorCode;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.JobParameters;
import org.springframework.batch.core.JobParametersBuilder;
import org.springframework.batch.core.launch.JobLauncher;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;

@Service
@Slf4j
@RequiredArgsConstructor
public class ProjectBatchService {

private final JobLauncher jobLauncher;
private final Job projectStatusJob;

@Scheduled(cron = "${scheduler.day}")
public void runProjectStatusJob() {
try{
JobParameters jobParameters = new JobParametersBuilder()
.addLong("time", 1L)
.toJobParameters();

jobLauncher.run(projectStatusJob, jobParameters);
log.info("Project status job finished");
} catch (Exception e){
e.printStackTrace();
throw new CustomException(ErrorCode.SIBAL_WHAT_IS_IT, e.getMessage());
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package NextLevel.demo.project.batch;

import NextLevel.demo.project.project.entity.ProjectEntity;
import java.io.Serializable;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@NoArgsConstructor
@Getter
@Setter
public class ProjectSerializableDto implements Serializable {

private Long projectId;
private Long projectGoal;

public static ProjectSerializableDto of(ProjectEntity projectEntity) {
ProjectSerializableDto projectSerializableDto = new ProjectSerializableDto();
projectSerializableDto.setProjectId(projectEntity.getId());
projectSerializableDto.setProjectGoal(projectEntity.getGoal());
return projectSerializableDto;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
package NextLevel.demo.project.batch;

import NextLevel.demo.project.ProjectStatus;
import NextLevel.demo.project.project.entity.ProjectEntity;
import jakarta.persistence.EntityManagerFactory;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import javax.sql.DataSource;
import lombok.RequiredArgsConstructor;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.StepExecution;
import org.springframework.batch.core.configuration.annotation.JobScope;
import org.springframework.batch.core.configuration.annotation.StepScope;
import org.springframework.batch.core.job.builder.JobBuilder;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.batch.core.scope.context.StepSynchronizationManager;
import org.springframework.batch.core.step.builder.StepBuilder;
import org.springframework.batch.item.Chunk;
import org.springframework.batch.item.ItemProcessor;
import org.springframework.batch.item.ItemWriter;
import org.springframework.batch.item.database.ItemPreparedStatementSetter;
import org.springframework.batch.item.database.JdbcBatchItemWriter;
import org.springframework.batch.item.database.JdbcCursorItemReader;
import org.springframework.batch.item.database.JpaPagingItemReader;
import org.springframework.batch.item.database.builder.JdbcBatchItemWriterBuilder;
import org.springframework.batch.item.database.builder.JdbcCursorItemReaderBuilder;
import org.springframework.batch.item.database.builder.JpaPagingItemReaderBuilder;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.transaction.PlatformTransactionManager;

@Configuration
@RequiredArgsConstructor
public class ProjectStatusBatchService {

private final DataSource dataSource;
private final JobRepository jobRepository;
private final EntityManagerFactory entityManagerFactory;

@Bean
public JpaPagingItemReader<ProjectEntity> projectReader() {
return new JpaPagingItemReaderBuilder<ProjectEntity>()
.name("expiredBoardReader")
.queryString(
"""
select p
from ProjectEntity p
where p.projectStatus = :projectStatus and p.expiredAt <= :date
""")
.parameterValues(Map.of(
"projectStatus", ProjectStatus.PROGRESS,
"date", LocalDate.now()
))
.pageSize(10)
.entityManagerFactory(entityManagerFactory)
.build();
}

@Bean
@JobScope
public Step projectSelectStep(
PlatformTransactionManager transactionManager
) {
return new StepBuilder("checkProjectStatus", jobRepository)
.<ProjectEntity, ProjectEntity> chunk(10, transactionManager)
.reader(projectReader())
.processor((p)->p) // nothing to do
.writer(projectChunk->{
StepExecution stepExecution = StepSynchronizationManager.getContext().getStepExecution();
List<ProjectSerializableDto> dtoList = projectChunk.getItems().stream().map(ProjectSerializableDto::of).toList();
stepExecution.getJobExecution().getExecutionContext().put("projectDtoList", dtoList);
})
.allowStartIfComplete(true)
.build();
}

@Bean
@JobScope
public JdbcCursorItemReader<ProjectAndFundingPriceDto> projectFundingPriceReader(
@Value("#{jobExecutionContext['projectDtoList']}") List<ProjectSerializableDto> projectChunk
) {
// StepExecution stepExecution = StepSynchronizationManager.getContext().getStepExecution();
// Chunk<ProjectEntity> projectChunk = (Chunk<ProjectEntity>)stepExecution.getJobExecution().getExecutionContext().get("projectChunk");
Map<Long, ProjectSerializableDto> projectMap = projectChunk.stream().collect(Collectors.toMap(ProjectSerializableDto::getProjectId, p -> p));
return new JdbcCursorItemReaderBuilder<ProjectAndFundingPriceDto>()
.name("projectFundingPriceReader")
.sql("""
SELECT
p.id AS projectId,

COALESCE((
SELECT SUM(ff.price)
FROM free_funding ff
WHERE ff.project_id = p.id
), 0)
+
COALESCE((
SELECT SUM(ofd.count * o.price)
FROM option_funding ofd
JOIN `option` o ON ofd.option_id = o.id
WHERE o.project_id = p.id
), 0) AS fundingPrice
FROM project p
WHERE p.id IN (?)
""")
.queryArguments(projectChunk.stream().map(ProjectSerializableDto::getProjectId).toList())
.rowMapper(new RowMapper() {
@Override
public Object mapRow(ResultSet rs, int rowNum) throws SQLException {
long projectId = rs.getLong("projectId");
int fundingPrice = rs.getInt("fundingPrice");
return new ProjectAndFundingPriceDto(projectMap.get(projectId), fundingPrice);
}
})
.dataSource(dataSource)
.build();
}

@Bean
public ItemProcessor<ProjectAndFundingPriceDto, ProjectAndFundingPriceDto> projectProcessor() {
return (dto)-> {
ProjectStatus status = dto.getFundingPrice() >= dto.getProjectSerializableDto().getProjectGoal() ? ProjectStatus.SUCCESS : ProjectStatus.FAIL;
dto.setProjectStatus(status);
return dto;
};
}

@Bean
public JdbcBatchItemWriter<ProjectAndFundingPriceDto> projectWriter() {
return new JdbcBatchItemWriterBuilder<ProjectAndFundingPriceDto>()
.sql("""
update project
set project_status = ?
where project.id = ?
""")
.dataSource(dataSource)
.itemPreparedStatementSetter(new ItemPreparedStatementSetter<ProjectAndFundingPriceDto>() {
@Override
public void setValues(ProjectAndFundingPriceDto projectDto, PreparedStatement ps) throws SQLException {
ps.setObject(1, projectDto.getProjectStatus().name());
ps.setLong(2, projectDto.getProjectSerializableDto().getProjectId());
}
})
.beanMapped()
.build();
}

@Bean
public Step expiredProjectStep(
PlatformTransactionManager transactionManager,
JdbcCursorItemReader<ProjectAndFundingPriceDto> projectFundingPriceReader
) {
return new StepBuilder("expiredProjectStep", jobRepository)
.<ProjectAndFundingPriceDto, ProjectAndFundingPriceDto> chunk(10, transactionManager)
.reader(projectFundingPriceReader)
.processor(projectProcessor())
.writer(projectWriter())
.allowStartIfComplete(true)
.build();
}

@Bean
public Job projectStatusJob(
JobRepository jobRepository,
Step projectSelectStep,
Step expiredProjectStep
) {
return new JobBuilder("projectStatusJob", jobRepository)
.start(projectSelectStep)
.next(expiredProjectStep)
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,19 @@
import NextLevel.demo.project.project.repository.ProjectRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.JobParameters;
import org.springframework.batch.core.JobParametersBuilder;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.launch.JobLauncher;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.http.ResponseEntity;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.GetMapping;

@Service
@Slf4j
Expand Down
7 changes: 7 additions & 0 deletions src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,10 @@ spring:
user-info-uri: https://www.googleapis.com/oauth2/v2/userinfo
user-name-attribute: id # Google의 사용자 식별자 (고유 ID)

batch:
jdbc:
initialize-schema: always


jwt:
secret: ${JWT_SECRET}
Expand Down Expand Up @@ -108,6 +112,9 @@ email:
EMAIL: ${EMAIL}
EMAIL_PASSWORD: "${EMAIL_PASSWORD}"

scheduler:
day: 0 0 3 * * * # 메일 세벽 3시 작동

---
spring:
config:
Expand Down
Loading