Skip to content

Commit d36e113

Browse files
authored
Merge pull request #65 from DMU-NextLevel/feat/batch
Feat/batch
2 parents 6454b19 + 5e82f2c commit d36e113

File tree

9 files changed

+323
-0
lines changed

9 files changed

+323
-0
lines changed

build.gradle

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,9 @@ dependencies {
6363
testImplementation 'org.springframework.boot:spring-boot-starter-test'
6464
testImplementation 'com.h2database:h2'
6565

66+
// batch
67+
implementation 'org.springframework.boot:spring-boot-starter-batch'
68+
6669
}
6770

6871
tasks.named('test') {
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package NextLevel.demo.config;
2+
3+
import jakarta.persistence.EntityManagerFactory;
4+
import org.springframework.context.annotation.Bean;
5+
import org.springframework.context.annotation.Configuration;
6+
import org.springframework.orm.jpa.JpaTransactionManager;
7+
import org.springframework.transaction.PlatformTransactionManager;
8+
9+
@Configuration
10+
public class BatchConfig {
11+
12+
@Bean
13+
public PlatformTransactionManager transactionManager(EntityManagerFactory emf) {
14+
return new JpaTransactionManager(emf);
15+
}
16+
17+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package NextLevel.demo.project.batch;
2+
3+
import lombok.RequiredArgsConstructor;
4+
import org.springframework.http.ResponseEntity;
5+
import org.springframework.stereotype.Controller;
6+
import org.springframework.web.bind.annotation.GetMapping;
7+
8+
@Controller
9+
@RequiredArgsConstructor
10+
public class BatchController {
11+
12+
private final ProjectBatchService projectBatchService;
13+
14+
@GetMapping("/admin/batch")
15+
public ResponseEntity doBatch() {
16+
projectBatchService.runProjectStatusJob();
17+
return ResponseEntity.ok().build();
18+
}
19+
20+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package NextLevel.demo.project.batch;
2+
3+
import NextLevel.demo.project.ProjectStatus;
4+
import NextLevel.demo.project.project.entity.ProjectEntity;
5+
import java.io.Serializable;
6+
import lombok.Getter;
7+
import lombok.NoArgsConstructor;
8+
import lombok.Setter;
9+
10+
@NoArgsConstructor
11+
@Getter
12+
@Setter
13+
public class ProjectAndFundingPriceDto implements Serializable {
14+
15+
private ProjectSerializableDto projectSerializableDto;
16+
private Integer fundingPrice;
17+
private ProjectStatus projectStatus;
18+
19+
public ProjectAndFundingPriceDto(ProjectSerializableDto projectSerializableDto, Integer fundingPrice) {
20+
this.projectSerializableDto = projectSerializableDto;
21+
this.fundingPrice = fundingPrice;
22+
}
23+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package NextLevel.demo.project.batch;
2+
3+
import NextLevel.demo.exception.CustomException;
4+
import NextLevel.demo.exception.ErrorCode;
5+
import lombok.RequiredArgsConstructor;
6+
import lombok.extern.slf4j.Slf4j;
7+
import org.springframework.batch.core.Job;
8+
import org.springframework.batch.core.JobParameters;
9+
import org.springframework.batch.core.JobParametersBuilder;
10+
import org.springframework.batch.core.launch.JobLauncher;
11+
import org.springframework.scheduling.annotation.Scheduled;
12+
import org.springframework.stereotype.Service;
13+
14+
@Service
15+
@Slf4j
16+
@RequiredArgsConstructor
17+
public class ProjectBatchService {
18+
19+
private final JobLauncher jobLauncher;
20+
private final Job projectStatusJob;
21+
22+
@Scheduled(cron = "${scheduler.day}")
23+
public void runProjectStatusJob() {
24+
try{
25+
JobParameters jobParameters = new JobParametersBuilder()
26+
.addLong("time", 1L)
27+
.toJobParameters();
28+
29+
jobLauncher.run(projectStatusJob, jobParameters);
30+
log.info("Project status job finished");
31+
} catch (Exception e){
32+
e.printStackTrace();
33+
throw new CustomException(ErrorCode.SIBAL_WHAT_IS_IT, e.getMessage());
34+
}
35+
}
36+
37+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package NextLevel.demo.project.batch;
2+
3+
import NextLevel.demo.project.project.entity.ProjectEntity;
4+
import java.io.Serializable;
5+
import lombok.Getter;
6+
import lombok.NoArgsConstructor;
7+
import lombok.Setter;
8+
9+
@NoArgsConstructor
10+
@Getter
11+
@Setter
12+
public class ProjectSerializableDto implements Serializable {
13+
14+
private Long projectId;
15+
private Long projectGoal;
16+
17+
public static ProjectSerializableDto of(ProjectEntity projectEntity) {
18+
ProjectSerializableDto projectSerializableDto = new ProjectSerializableDto();
19+
projectSerializableDto.setProjectId(projectEntity.getId());
20+
projectSerializableDto.setProjectGoal(projectEntity.getGoal());
21+
return projectSerializableDto;
22+
}
23+
24+
}
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
package NextLevel.demo.project.batch;
2+
3+
import NextLevel.demo.project.ProjectStatus;
4+
import NextLevel.demo.project.project.entity.ProjectEntity;
5+
import jakarta.persistence.EntityManagerFactory;
6+
import java.sql.PreparedStatement;
7+
import java.sql.ResultSet;
8+
import java.sql.SQLException;
9+
import java.time.LocalDate;
10+
import java.util.ArrayList;
11+
import java.util.List;
12+
import java.util.Map;
13+
import java.util.stream.Collectors;
14+
import javax.sql.DataSource;
15+
import lombok.RequiredArgsConstructor;
16+
import org.springframework.batch.core.Job;
17+
import org.springframework.batch.core.Step;
18+
import org.springframework.batch.core.StepExecution;
19+
import org.springframework.batch.core.configuration.annotation.JobScope;
20+
import org.springframework.batch.core.configuration.annotation.StepScope;
21+
import org.springframework.batch.core.job.builder.JobBuilder;
22+
import org.springframework.batch.core.repository.JobRepository;
23+
import org.springframework.batch.core.scope.context.StepSynchronizationManager;
24+
import org.springframework.batch.core.step.builder.StepBuilder;
25+
import org.springframework.batch.item.Chunk;
26+
import org.springframework.batch.item.ItemProcessor;
27+
import org.springframework.batch.item.ItemWriter;
28+
import org.springframework.batch.item.database.ItemPreparedStatementSetter;
29+
import org.springframework.batch.item.database.JdbcBatchItemWriter;
30+
import org.springframework.batch.item.database.JdbcCursorItemReader;
31+
import org.springframework.batch.item.database.JpaPagingItemReader;
32+
import org.springframework.batch.item.database.builder.JdbcBatchItemWriterBuilder;
33+
import org.springframework.batch.item.database.builder.JdbcCursorItemReaderBuilder;
34+
import org.springframework.batch.item.database.builder.JpaPagingItemReaderBuilder;
35+
import org.springframework.beans.factory.annotation.Value;
36+
import org.springframework.context.annotation.Bean;
37+
import org.springframework.context.annotation.Configuration;
38+
import org.springframework.jdbc.core.RowMapper;
39+
import org.springframework.transaction.PlatformTransactionManager;
40+
41+
@Configuration
42+
@RequiredArgsConstructor
43+
public class ProjectStatusBatchService {
44+
45+
private final DataSource dataSource;
46+
private final JobRepository jobRepository;
47+
private final EntityManagerFactory entityManagerFactory;
48+
49+
@Bean
50+
public JpaPagingItemReader<ProjectEntity> projectReader() {
51+
return new JpaPagingItemReaderBuilder<ProjectEntity>()
52+
.name("expiredBoardReader")
53+
.queryString(
54+
"""
55+
select p
56+
from ProjectEntity p
57+
where p.projectStatus = :projectStatus and p.expiredAt <= :date
58+
""")
59+
.parameterValues(Map.of(
60+
"projectStatus", ProjectStatus.PROGRESS,
61+
"date", LocalDate.now()
62+
))
63+
.pageSize(10)
64+
.entityManagerFactory(entityManagerFactory)
65+
.build();
66+
}
67+
68+
@Bean
69+
@JobScope
70+
public Step projectSelectStep(
71+
PlatformTransactionManager transactionManager
72+
) {
73+
return new StepBuilder("checkProjectStatus", jobRepository)
74+
.<ProjectEntity, ProjectEntity> chunk(10, transactionManager)
75+
.reader(projectReader())
76+
.processor((p)->p) // nothing to do
77+
.writer(projectChunk->{
78+
StepExecution stepExecution = StepSynchronizationManager.getContext().getStepExecution();
79+
List<ProjectSerializableDto> dtoList = projectChunk.getItems().stream().map(ProjectSerializableDto::of).toList();
80+
stepExecution.getJobExecution().getExecutionContext().put("projectDtoList", dtoList);
81+
})
82+
.allowStartIfComplete(true)
83+
.build();
84+
}
85+
86+
@Bean
87+
@JobScope
88+
public JdbcCursorItemReader<ProjectAndFundingPriceDto> projectFundingPriceReader(
89+
@Value("#{jobExecutionContext['projectDtoList']}") List<ProjectSerializableDto> projectChunk
90+
) {
91+
// StepExecution stepExecution = StepSynchronizationManager.getContext().getStepExecution();
92+
// Chunk<ProjectEntity> projectChunk = (Chunk<ProjectEntity>)stepExecution.getJobExecution().getExecutionContext().get("projectChunk");
93+
Map<Long, ProjectSerializableDto> projectMap = projectChunk.stream().collect(Collectors.toMap(ProjectSerializableDto::getProjectId, p -> p));
94+
return new JdbcCursorItemReaderBuilder<ProjectAndFundingPriceDto>()
95+
.name("projectFundingPriceReader")
96+
.sql("""
97+
SELECT
98+
p.id AS projectId,
99+
100+
COALESCE((
101+
SELECT SUM(ff.price)
102+
FROM free_funding ff
103+
WHERE ff.project_id = p.id
104+
), 0)
105+
+
106+
COALESCE((
107+
SELECT SUM(ofd.count * o.price)
108+
FROM option_funding ofd
109+
JOIN `option` o ON ofd.option_id = o.id
110+
WHERE o.project_id = p.id
111+
), 0) AS fundingPrice
112+
FROM project p
113+
WHERE p.id IN (?)
114+
""")
115+
.queryArguments(projectChunk.stream().map(ProjectSerializableDto::getProjectId).toList())
116+
.rowMapper(new RowMapper() {
117+
@Override
118+
public Object mapRow(ResultSet rs, int rowNum) throws SQLException {
119+
long projectId = rs.getLong("projectId");
120+
int fundingPrice = rs.getInt("fundingPrice");
121+
return new ProjectAndFundingPriceDto(projectMap.get(projectId), fundingPrice);
122+
}
123+
})
124+
.dataSource(dataSource)
125+
.build();
126+
}
127+
128+
@Bean
129+
public ItemProcessor<ProjectAndFundingPriceDto, ProjectAndFundingPriceDto> projectProcessor() {
130+
return (dto)-> {
131+
ProjectStatus status = dto.getFundingPrice() >= dto.getProjectSerializableDto().getProjectGoal() ? ProjectStatus.SUCCESS : ProjectStatus.FAIL;
132+
dto.setProjectStatus(status);
133+
return dto;
134+
};
135+
}
136+
137+
@Bean
138+
public JdbcBatchItemWriter<ProjectAndFundingPriceDto> projectWriter() {
139+
return new JdbcBatchItemWriterBuilder<ProjectAndFundingPriceDto>()
140+
.sql("""
141+
update project
142+
set project_status = ?
143+
where project.id = ?
144+
""")
145+
.dataSource(dataSource)
146+
.itemPreparedStatementSetter(new ItemPreparedStatementSetter<ProjectAndFundingPriceDto>() {
147+
@Override
148+
public void setValues(ProjectAndFundingPriceDto projectDto, PreparedStatement ps) throws SQLException {
149+
ps.setObject(1, projectDto.getProjectStatus().name());
150+
ps.setLong(2, projectDto.getProjectSerializableDto().getProjectId());
151+
}
152+
})
153+
.beanMapped()
154+
.build();
155+
}
156+
157+
@Bean
158+
public Step expiredProjectStep(
159+
PlatformTransactionManager transactionManager,
160+
JdbcCursorItemReader<ProjectAndFundingPriceDto> projectFundingPriceReader
161+
) {
162+
return new StepBuilder("expiredProjectStep", jobRepository)
163+
.<ProjectAndFundingPriceDto, ProjectAndFundingPriceDto> chunk(10, transactionManager)
164+
.reader(projectFundingPriceReader)
165+
.processor(projectProcessor())
166+
.writer(projectWriter())
167+
.allowStartIfComplete(true)
168+
.build();
169+
}
170+
171+
@Bean
172+
public Job projectStatusJob(
173+
JobRepository jobRepository,
174+
Step projectSelectStep,
175+
Step expiredProjectStep
176+
) {
177+
return new JobBuilder("projectStatusJob", jobRepository)
178+
.start(projectSelectStep)
179+
.next(expiredProjectStep)
180+
.build();
181+
}
182+
}

src/main/java/NextLevel/demo/project/project/service/ProjectStatusService.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,19 @@
55
import NextLevel.demo.project.project.repository.ProjectRepository;
66
import lombok.RequiredArgsConstructor;
77
import lombok.extern.slf4j.Slf4j;
8+
import org.springframework.batch.core.Job;
9+
import org.springframework.batch.core.JobParameters;
10+
import org.springframework.batch.core.JobParametersBuilder;
11+
import org.springframework.batch.core.Step;
12+
import org.springframework.batch.core.launch.JobLauncher;
13+
import org.springframework.batch.core.repository.JobRepository;
14+
import org.springframework.beans.factory.annotation.Autowired;
815
import org.springframework.context.annotation.Bean;
16+
import org.springframework.http.ResponseEntity;
17+
import org.springframework.scheduling.annotation.Scheduled;
918
import org.springframework.stereotype.Service;
1019
import org.springframework.transaction.annotation.Transactional;
20+
import org.springframework.web.bind.annotation.GetMapping;
1121

1222
@Service
1323
@Slf4j

src/main/resources/application.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,10 @@ spring:
7979
user-info-uri: https://www.googleapis.com/oauth2/v2/userinfo
8080
user-name-attribute: id # Google의 사용자 식별자 (고유 ID)
8181

82+
batch:
83+
jdbc:
84+
initialize-schema: always
85+
8286

8387
jwt:
8488
secret: ${JWT_SECRET}
@@ -108,6 +112,9 @@ email:
108112
EMAIL: ${EMAIL}
109113
EMAIL_PASSWORD: "${EMAIL_PASSWORD}"
110114

115+
scheduler:
116+
day: 0 0 3 * * * # 메일 세벽 3시 작동
117+
111118
---
112119
spring:
113120
config:

0 commit comments

Comments
 (0)