part1에서 진행했던 내용을 기반으로 이어서 진행한다.
● User의 totalAmount를 Orders Entity로 변경
- 하나의 User는 N개의 Orders를 포함
● 주문 총 금액은 Orders Entity를 기준으로 합산
● `-date=2021-08` JobParameters 사용
- 주문 금액 집계는 orderStatisticsStep으로 생성
■ `2021년_08월_주문_금액.csv` 파일은 2021년 8월1일~8월31일 주문 통계 내역
`date` 파라미터가 없는 경우, orderStatisticsStep은 실행하지 않는다.
user 한명이 orders의 여러개를 만들 수 있도록 일대다 구조로 만들 수 있게 엔티티를 수정한다.
1. Orders 추가
@Entity
@Getter
@NoArgsConstructor
public class Orders {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String itemName;
private int amount;
private LocalDate createdDate;
@Builder
public Orders(String itemName, int amount, LocalDate createdDate) {
this.itemName = itemName;
this.amount = amount;
this.createdDate = createdDate;
}
}
2. User 일대다 구조로 수정
@Getter
@Entity
@NoArgsConstructor
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
@Enumerated(EnumType.STRING)
private Level level = Level.NORMAL;
// private int totalAmount;
/**
* 1:N 관계
* user는 n개의 Orders를 가질 수 있다.
* user가 저장되면서 Orders를 같이 저장할 수 있도록 영속성 전이(PERSIST)를 적용한다.
**/
@OneToMany(cascade = CascadeType.PERSIST)
@JoinColumn(name = "user_id")
private List<Orders> orders;
private LocalDate updatedDate;
@Builder
private User(String username, List<Orders> orders) {
this.username = username;
this.orders = orders;
}
public boolean availableLeveUp() {
return Level.availableLevelUp(this.getLevel(), this.getTotalAmount());
}
private int getTotalAmount() {
return this.orders.stream()
.mapToInt(Orders::getAmount)
.sum();
}
public Level levelUp() {
Level nextLevel = Level.getNextLevel(this.getTotalAmount());
this.level = nextLevel;
this.updatedDate = LocalDate.now();
return nextLevel;
}
}
3. OrderStatistics (주문금액) 객체를 따로 분리한다.
@Getter
public class OrderStatistics {
private String amount;
private LocalDate date;
@Builder
public OrderStatistics(String amount, LocalDate date) {
this.amount = amount;
this.date = date;
}
}
4. UserConfiguration에 orderStatisticsStep(주문금액집계)를 추가하여 월 단위 주문 통계 내역을 조회하고 csv파일을 생성한다.
@Configuration
@Slf4j
public class UserConfiguration {
public static final int CHUNK_SIZE = 100;
private final JobBuilderFactory jobBuilderFactory;
private final StepBuilderFactory stepBuilderFactory;
private final UserRepository userRepository;
private final EntityManagerFactory entityManagerFactory;
private final DataSource dataSource;
public UserConfiguration(JobBuilderFactory jobBuilderFactory, StepBuilderFactory stepBuilderFactory, UserRepository userRepository, EntityManagerFactory entityManagerFactory, DataSource dataSource) {
this.jobBuilderFactory = jobBuilderFactory;
this.stepBuilderFactory = stepBuilderFactory;
this.userRepository = userRepository;
this.entityManagerFactory = entityManagerFactory;
this.dataSource = dataSource;
}
@Bean
public Job userJob() throws Exception {
return this.jobBuilderFactory.get("userJob")
.incrementer(new RunIdIncrementer())
.start(this.saveUserStep())
.next(this.userLevelUpStep())
.next(this.orderStatisticsStep(null))
.listener(new LevelUpJobExecutionListener(userRepository))
.build();
}
@Bean
@JobScope
public Step orderStatisticsStep(@Value("#{jobParameters[date]}") String date) throws Exception {
return this.stepBuilderFactory.get("orderStatisticsStep")
.<OrderStatistics, OrderStatistics>chunk(CHUNK_SIZE)
.reader(orderStatisticsItemReader(date))
.writer(orderStatisticsItemWriter(date))
.build();
}
private ItemReader<? extends OrderStatistics> orderStatisticsItemReader(String date) throws Exception {
YearMonth yearMonth = YearMonth.parse(date);
Map<String, Object> parameters = new HashMap<>();
parameters.put("startDate", yearMonth.atDay(1)); // 2021년 8월 1일
parameters.put("endDate", yearMonth.atEndOfMonth()); // 8월의 마지막 날
Map<String, Order> sortKey = new HashMap<>();
sortKey.put("created_date", Order.ASCENDING);
JdbcPagingItemReader<OrderStatistics> itemReader = new JdbcPagingItemReaderBuilder<OrderStatistics>()
.dataSource(this.dataSource)
.rowMapper((resultSet, i) -> OrderStatistics.builder()
.amount(resultSet.getString(1))
.date(LocalDate.parse(resultSet.getString(2), DateTimeFormatter.ISO_DATE))
.build())
.pageSize(CHUNK_SIZE) // 페이징 설정
.name("orderStatisticsItemReader")
.selectClause("sum(amount), created_date")
.fromClause("orders")
.whereClause("created_date >= :startDate and created_date <= :endDate")
.groupClause("created_date")
.parameterValues(parameters)
.sortKeys(sortKey)
.build();
itemReader.afterPropertiesSet();
return itemReader;
}
private ItemWriter<? super OrderStatistics> orderStatisticsItemWriter(String date) throws Exception {
YearMonth yearMonth = YearMonth.parse(date);
String fileName = yearMonth.getYear() + "년_" + yearMonth.getMonthValue() + "월_일별_주문_금액.csv";
BeanWrapperFieldExtractor<OrderStatistics> fieldExtractor = new BeanWrapperFieldExtractor<>();
fieldExtractor.setNames(new String[]{"amount", "date"});
DelimitedLineAggregator<OrderStatistics> lineAggregator = new DelimitedLineAggregator<>();
lineAggregator.setDelimiter(",");
lineAggregator.setFieldExtractor(fieldExtractor);
FlatFileItemWriter<OrderStatistics> itemWriter = new FlatFileItemWriterBuilder<OrderStatistics>()
.resource(new FileSystemResource("output/" + fileName))
.lineAggregator(lineAggregator)
.name("orderStatisticsItemWriter")
.encoding("UTF-8")
.headerCallback(writer -> writer.write("total_amount,date"))
.build();
itemWriter.afterPropertiesSet();
return itemWriter;
}
@Bean
public Step saveUserStep() {
return this.stepBuilderFactory.get("saveUserStep")
.tasklet(new SaveUserTasklet(userRepository))
.build();
}
@Bean
public Step userLevelUpStep() throws Exception {
return this.stepBuilderFactory.get("userLevelUpStep")
.<User, User>chunk(CHUNK_SIZE)
.reader(itemReader())
.processor(itemProcessor())
.writer(itemWriter())
.build();
}
private ItemWriter<? super User> itemWriter() {
return users -> users.forEach(x -> {
x.levelUp();
userRepository.save(x);
});
}
private ItemProcessor<? super User, ? extends User> itemProcessor() {
return user -> {
if (user.availableLeveUp()) {
return user;
}
return null;
};
}
private ItemReader<? extends User> itemReader() throws Exception {
JpaPagingItemReader<User> itemReader = new JpaPagingItemReaderBuilder<User>()
.queryString("select u from User u")
.entityManagerFactory(entityManagerFactory)
.pageSize(CHUNK_SIZE)
.name("userItemReader")
.build();
itemReader.afterPropertiesSet();
return itemReader;
}
}
5. JobExecutionDecider에서 제공하는 주문 금액 집계 step 실행 여부 벨리데이션을 구현한다.
public class JobParameterDecide implements JobExecutionDecider {
public static final FlowExecutionStatus CONTINUE = new FlowExecutionStatus("CONTINUE");
private final String key;
public JobParameterDecide(String key) {
this.key = key;
}
@Override
public FlowExecutionStatus decide(JobExecution jobExecution, StepExecution stepExecution) {
String value = jobExecution.getJobParameters().getString(key);
// key가 없으면
if(StringUtils.isEmpty(value)) {
return FlowExecutionStatus.COMPLETED;
}
return CONTINUE;
}
}
* JobExecutionDecider의 인터페이스는 flow결정과_배치실행여부를 결정한다.
6. 5번에서 만든 JobExecutionDecide를 기반으로 step 기능을 추가한다.
* UserConfiguration.java 의 userJob() 영역 step 추가
@Bean
public Job userJob() throws Exception {
return this.jobBuilderFactory.get("userJob")
.incrementer(new RunIdIncrementer())
.start(this.saveUserStep())
.next(this.userLevelUpStep())
.listener(new LevelUpJobExecutionListener(userRepository))
.next(new JobParameterDecide("date")) // 가져온값이 아래의 CONTINUE인지 체크
.on(JobParameterDecide.CONTINUE.getName()) // CONTINUE이면 아래의 to메서드 실행
.to(this.orderStatisticsStep(null))
.build()
.build();
}
* 결과 값
느낀점 :
1. user가 저장되면서 Orders를 같이 저장할 수 있도록 영속성 전이(PERSIST)를 적용 함으로 써 JPA의 이해를 한번 더 상기 시켰다.
(잊을만 하면 기존에 기록했던 영속성 전이를 다시 보면서 리마인딩 하자)
2. JobExecutionDecider를 통해 배치실행여부를 좀 더 쉽게 확인이 가능한점을 배움.
3. FlatFileItemWriter로 데이터 CSV 파일 쓰기를 통해 보다 편하게 생성할 수 있었다.
학습사이트 : 패스트 캠퍼스 (스프링 배치편)
'SPRING > 개발 TIP' 카테고리의 다른 글
[SPRING] 스프링 배치에서 사용되는 Quartz 배치 스케줄링 (1) | 2022.02.06 |
---|---|
[SPRING] 주문금액 집계 프로젝트 실습 part3 (성능개선과 성능비교) (0) | 2021.08.07 |
[SPRING] 주문금액 집계 프로젝트 실습 part1 (0) | 2021.08.04 |
[SPRING] 애노테이션 직접 만들기 (0) | 2021.04.27 |
[SPRING] SpringBootServletInitializer 란 무엇이고 왜 상속받고 있는가? (0) | 2021.04.19 |