Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,11 @@ public ApiResponse<GetProductDetailResponse> getProductDetail(
@GetMapping("/rankings")
public ApiResponse<GetProductRankingsResponse> getProductRankings(
@RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate date,
@RequestParam(required = false, defaultValue = "DAILY") String type,
@RequestParam(required = false, defaultValue = "0") int pageNo,
@RequestParam(required = false, defaultValue = "10") int pageSize
) {
ProductRankingList ranking = getProductRankingService.getRanking(new GetProductRankingQuery(date, pageNo, pageSize));
ProductRankingList ranking = getProductRankingService.getRanking(new GetProductRankingQuery(date, type, pageNo, pageSize));

return ApiResponse.success(GetProductRankingsResponse.from(ranking));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,5 +41,5 @@ ApiResponse<GetProductListResponse> getProductList(
summary = "์ƒํ’ˆ ๋žญํ‚น ์กฐํšŒ",
description = "์ƒํ’ˆ ๋žญํ‚น์„ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค."
)
ApiResponse<GetProductRankingsResponse> getProductRankings(LocalDate date, int pageNo, int pageSize);
ApiResponse<GetProductRankingsResponse> getProductRankings(LocalDate date, String type, int pageNo, int pageSize);
}
26 changes: 26 additions & 0 deletions apps/commerce-batch/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
dependencies {
implementation(project(":core:infra:database:mysql:mysql-config"))
implementation(project(":supports:jackson"))
implementation(project(":supports:logging"))
implementation(project(":supports:monitoring"))

//service
implementation(project(":core:service"))

//domain
implementation(project(":core:domain"))

// web
implementation("org.springframework.boot:spring-boot-starter-web")

//batch
implementation("org.springframework.boot:spring-boot-starter-batch")

implementation("org.springframework:spring-tx")
implementation("org.springframework.boot:spring-boot-starter-actuator")

// test-fixtures
testImplementation(project(":core:infra:database:mysql:mysql-config"))
testImplementation(testFixtures(project(":core:domain")))
testImplementation(testFixtures(project(":core:infra:database:mysql")))
}
Comment on lines +1 to +26
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

โš ๏ธ Potential issue | ๐ŸŸ  Major

๐Ÿงฉ Analysis chain

๐Ÿ Script executed:

# Check for web component usage in commerce-batch module
fd -e java -e kt --path-separator / | grep -E "apps/commerce-batch" | head -20

Repository: Loopers-dev-lab/loopers-spring-java-template

Length of output: 1574


๐Ÿ Script executed:

# Search for web annotations in commerce-batch source files
rg -i '(@(RestController|Controller|RequestMapping|GetMapping|PostMapping|Service|Component)|WebController|RestEndpoint)' apps/commerce-batch --type java --type kotlin -n

Repository: Loopers-dev-lab/loopers-spring-java-template

Length of output: 828


๐Ÿ Script executed:

# Check the actual directory structure of commerce-batch
find apps/commerce-batch -type f -name "*.kt" -o -name "*.java" -o -name "*.kts" | grep -v ".gradle" | head -30

Repository: Loopers-dev-lab/loopers-spring-java-template

Length of output: 1574


๐Ÿ Script executed:

# Check CommerceBatchApplication main class
cat -n apps/commerce-batch/src/main/java/com/loopers/CommerceBatchApplication.java

Repository: Loopers-dev-lab/loopers-spring-java-template

Length of output: 599


๐Ÿ Script executed:

# Check one of the scheduler files to see if web exposure is used
cat -n apps/commerce-batch/src/main/java/com/loopers/application/batch/product/MonthlyProductMetricScheduler.java

Repository: Loopers-dev-lab/loopers-spring-java-template

Length of output: 2033


๐Ÿ Script executed:

# Search for any Actuator endpoint configuration or web exposure in the batch module
rg -i '(actuator|endpoint|web|servlet)' apps/commerce-batch --type java --type kotlin -A 2 -B 2

Repository: Loopers-dev-lab/loopers-spring-java-template

Length of output: 788


spring-boot-starter-web ์ œ๊ฑฐ ๋ฐ ์ค‘๋ณต๋œ mysql-config ์˜์กด์„ฑ ์ •๋ฆฌ

  1. spring-boot-starter-web ์ œ๊ฑฐ (Line 14): ๋ฐฐ์น˜ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์—์„œ ์›น ์Šคํƒ€ํ„ฐ๊ฐ€ ์‚ฌ์šฉ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๋ชจ๋“ˆ ์ „์ฒด์—์„œ HTTP ์—”๋“œํฌ์ธํŠธ๋‚˜ ์›น ์ปจํŠธ๋กค๋Ÿฌ๊ฐ€ ์—†์œผ๋ฉฐ, ๋ชจ๋“  ๋ฐฐ์น˜ ์ž‘์—…์€ @Scheduled ์–ด๋…ธํ…Œ์ด์…˜์„ ํ†ตํ•ด ์Šค์ผ€์ค„๋ง์œผ๋กœ ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค. ์ด ์˜์กด์„ฑ์„ ์ œ๊ฑฐํ•˜์„ธ์š”.

  2. ์ค‘๋ณต๋œ mysql-config ์ œ๊ฑฐ (Line 23): mysql-config๋Š” ์ด๋ฏธ implementation ์Šค์ฝ”ํ”„(Line 2)์— ์„ ์–ธ๋˜์–ด ์žˆ์œผ๋ฏ€๋กœ, testImplementation์—์„œ์˜ ์„ ์–ธ์€ ์ œ๊ฑฐํ•˜์„ธ์š”. Gradle์—์„œ ํ…Œ์ŠคํŠธ ์˜์กด์„ฑ์€ ์ž๋™์œผ๋กœ ๊ตฌํ˜„ ์˜์กด์„ฑ์„ ์ƒ์†๋ฐ›์Šต๋‹ˆ๋‹ค.

๐Ÿค– Prompt for AI Agents
In apps/commerce-batch/build.gradle.kts around lines 1 to 26, remove the
unnecessary "org.springframework.boot:spring-boot-starter-web" dependency (line
~14) because this is a batch-only module with no web controllers, and delete the
duplicate testImplementation(project(":core:infra:database:mysql:mysql-config"))
(line ~23) since mysql-config is already declared as an implementation
dependency and test scope inherits implementation dependencies; update the file
to keep only the batch- and required support dependencies.

Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.loopers;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;

@EnableScheduling
@SpringBootApplication
public class CommerceBatchApplication {

public static void main(String[] args) {
SpringApplication.run(CommerceBatchApplication.class, args);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package com.loopers.application.batch.product;

import com.loopers.core.domain.product.repository.DailyProductMetricRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.batch.core.configuration.annotation.StepScope;
import org.springframework.batch.core.partition.support.Partitioner;
import org.springframework.batch.item.ExecutionContext;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.time.LocalDate;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

@Component
@StepScope
@RequiredArgsConstructor
public class MonthlyProductMetricBatchPartitioner implements Partitioner {

private final DailyProductMetricRepository dailyProductMetricRepository;

@Value("#{jobParameters['startDate']}")
private String startDateParam;

@Value("#{jobParameters['endDate']}")
private String endDateParam;

@Override
public Map<String, ExecutionContext> partition(int gridSize) {
LocalDate startDate = LocalDate.parse(startDateParam);
LocalDate endDate = LocalDate.parse(endDateParam);
Long totalCount = dailyProductMetricRepository.countDistinctProductIdsBy(startDate, endDate);

if (totalCount == 0) {
return Collections.emptyMap();
}

long targetSize = (totalCount / gridSize) + 1;
Map<String, ExecutionContext> partitions = new HashMap<>();

for (int i = 0; i < gridSize; i++) {
ExecutionContext context = new ExecutionContext();

context.putLong("partitionOffset", i * targetSize);
context.putLong("partitionLimit", targetSize);
context.putString("startDate", startDateParam);
context.putString("endDate", endDateParam);

partitions.put("partition" + i, context);
}

return partitions;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.loopers.application.batch.product;

import com.loopers.core.domain.product.repository.DailyProductMetricRepository;
import com.loopers.core.domain.product.vo.ProductMetricAggregation;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import org.springframework.batch.item.ExecutionContext;
import org.springframework.batch.item.ItemStreamReader;

import java.time.LocalDate;
import java.util.Iterator;
import java.util.Objects;

@RequiredArgsConstructor
public class MonthlyProductMetricBatchReader implements ItemStreamReader<ProductMetricAggregation> {

private final DailyProductMetricRepository dailyProductMetricRepository;
private Iterator<ProductMetricAggregation> iterator;

@Override
public void open(@NonNull ExecutionContext executionContext) {
LocalDate startDate = LocalDate.parse(executionContext.getString("startDate"));
LocalDate endDate = LocalDate.parse(executionContext.getString("endDate"));
long partitionOffset = executionContext.getLong("partitionOffset");
long partitionLimit = executionContext.getLong("partitionLimit");

this.iterator = dailyProductMetricRepository.findAggregatedBy(startDate, endDate, partitionOffset, partitionLimit)
.iterator();
}

@Override
public ProductMetricAggregation read() {
if (Objects.isNull(iterator) || !iterator.hasNext()) {
return null;
}

return iterator.next();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.loopers.application.batch.product;

import com.loopers.core.domain.product.MonthlyProductMetric;
import com.loopers.core.domain.product.repository.MonthlyProductMetricRepository;
import com.loopers.core.domain.product.vo.ProductMetricAggregation;
import com.loopers.core.domain.product.vo.YearMonth;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import org.springframework.batch.core.configuration.annotation.StepScope;
import org.springframework.batch.item.Chunk;
import org.springframework.batch.item.ExecutionContext;
import org.springframework.batch.item.ItemStreamWriter;
import org.springframework.stereotype.Component;

import java.time.LocalDate;
import java.util.List;

@StepScope
@Component
@RequiredArgsConstructor
public class MonthlyProductMetricBatchWriter implements ItemStreamWriter<ProductMetricAggregation> {

private final MonthlyProductMetricRepository repository;
private YearMonth yearMonth;

@Override
public void open(@NonNull ExecutionContext executionContext) {
LocalDate startDate = LocalDate.parse(executionContext.getString("startDate"));
this.yearMonth = YearMonth.from(startDate);
}

@Override
public void write(@NonNull Chunk<? extends ProductMetricAggregation> chunk) {
List<MonthlyProductMetric> monthlyMetrics = chunk.getItems().stream()
.map(aggregation -> aggregation.to(yearMonth))
.toList();

repository.bulkUpsert(monthlyMetrics);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.loopers.application.batch.product;

import lombok.RequiredArgsConstructor;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.JobParameters;
import org.springframework.batch.core.JobParametersBuilder;
import org.springframework.batch.core.JobParametersInvalidException;
import org.springframework.batch.core.launch.JobLauncher;
import org.springframework.batch.core.repository.JobExecutionAlreadyRunningException;
import org.springframework.batch.core.repository.JobInstanceAlreadyCompleteException;
import org.springframework.batch.core.repository.JobRestartException;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import java.time.LocalDate;
import java.time.YearMonth;

@Component
@RequiredArgsConstructor
public class MonthlyProductMetricScheduler {

private final JobLauncher jobLauncher;
private final Job monthlyProductMetricJob;

@Scheduled(cron = "0 0 2 1 * ?")
public void run() throws JobInstanceAlreadyCompleteException, JobExecutionAlreadyRunningException, JobParametersInvalidException, JobRestartException {
Comment on lines +25 to +26
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

โš ๏ธ Potential issue | ๐ŸŸ  Major

๋ฐฐ์น˜ ์ž‘์—… ์‹คํ–‰ ์‹คํŒจ ์‹œ ๊ด€์ฐฐ์„ฑ๊ณผ ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ๋ฅผ ๊ฐœ์„ ํ•˜์„ธ์š”.

์Šค์ผ€์ค„๋Ÿฌ ๋ฉ”์„œ๋“œ๊ฐ€ 4๊ฐœ์˜ ์ฒดํฌ ์˜ˆ์™ธ๋ฅผ ๋˜์ง€์ง€๋งŒ ๋กœ๊น…์ด๋‚˜ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ๊ฐ€ ์—†์–ด, ๋ฐฐ์น˜ ์ž‘์—… ์‹คํŒจ๊ฐ€ ์กฐ์šฉํžˆ ๋ฌด์‹œ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์šด์˜ ํ™˜๊ฒฝ์—์„œ ๋ฌธ์ œ ์ง„๋‹จ์„ ์–ด๋ ต๊ฒŒ ๋งŒ๋“ญ๋‹ˆ๋‹ค.

๐Ÿ”Ž ๋กœ๊น…๊ณผ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ ์ถ”๊ฐ€ ์ œ์•ˆ
     @Scheduled(cron = "0 0 2 1 * ?")
-    public void run() throws JobInstanceAlreadyCompleteException, JobExecutionAlreadyRunningException, JobParametersInvalidException, JobRestartException {
+    public void run() {
+        try {
+            log.info("Starting monthly product metric aggregation batch job");
             YearMonth lastMonth = YearMonth.now().minusMonths(1);
             LocalDate startDate = lastMonth.atDay(1);
             LocalDate endDate = lastMonth.atEndOfMonth();
     
             JobParameters jobParameters = new JobParametersBuilder()
                     .addString("startDate", startDate.toString())
                     .addString("endDate", endDate.toString())
                     .addLong("timestamp", System.currentTimeMillis())
                     .toJobParameters();
     
             jobLauncher.run(monthlyProductMetricJob, jobParameters);
+            log.info("Monthly product metric aggregation batch job completed successfully");
+        } catch (JobExecutionAlreadyRunningException e) {
+            log.warn("Monthly product metric batch job is already running", e);
+        } catch (JobInstanceAlreadyCompleteException | JobRestartException | JobParametersInvalidException e) {
+            log.error("Failed to execute monthly product metric batch job", e);
+        }
     }

Lombok์˜ @Slf4j ์–ด๋…ธํ…Œ์ด์…˜์„ ํด๋ž˜์Šค์— ์ถ”๊ฐ€ํ•˜์„ธ์š”.

Committable suggestion skipped: line range outside the PR's diff.

๐Ÿค– Prompt for AI Agents
In
apps/commerce-batch/src/main/java/com/loopers/application/batch/product/MonthlyProductMetricScheduler.java
around lines 25-26, the scheduled run() method currently declares four checked
exceptions but has no logging or handling; add Lombok's @Slf4j to the class,
wrap the job execution in a try/catch that catches those specific exceptions (or
Exception) inside the method instead of letting them propagate silently, call
log.error(...) with a clear contextual message and the exception instance for
each failure, and optionally emit a metric or rethrow a runtime exception if you
need the scheduler to mark the job as failed.

YearMonth lastMonth = YearMonth.now().minusMonths(1);
LocalDate startDate = lastMonth.atDay(1);
LocalDate endDate = lastMonth.atEndOfMonth();

JobParameters jobParameters = new JobParametersBuilder()
.addString("startDate", startDate.toString())
.addString("endDate", endDate.toString())
.addLong("timestamp", System.currentTimeMillis())
.toJobParameters();

jobLauncher.run(monthlyProductMetricJob, jobParameters);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package com.loopers.application.batch.product;

import com.loopers.core.domain.product.repository.DailyProductMetricRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.batch.core.configuration.annotation.StepScope;
import org.springframework.batch.core.partition.support.Partitioner;
import org.springframework.batch.item.ExecutionContext;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.time.LocalDate;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

@Component
@StepScope
@RequiredArgsConstructor
public class WeeklyProductMetricBatchPartitioner implements Partitioner {

private final DailyProductMetricRepository dailyProductMetricRepository;

@Value("#{jobParameters['startDate']}")
private String startDateParam;

@Value("#{jobParameters['endDate']}")
private String endDateParam;

@Override
public Map<String, ExecutionContext> partition(int gridSize) {
LocalDate startDate = LocalDate.parse(startDateParam);
LocalDate endDate = LocalDate.parse(endDateParam);
Long totalCount = dailyProductMetricRepository.countDistinctProductIdsBy(startDate, endDate);

if (totalCount == 0) {
return Collections.emptyMap();
}

long targetSize = (totalCount / gridSize) + 1;
Map<String, ExecutionContext> partitions = new HashMap<>();

for (int i = 0; i < gridSize; i++) {
ExecutionContext context = new ExecutionContext();

context.putLong("partitionOffset", i * targetSize);
context.putLong("partitionLimit", targetSize);
context.putString("startDate", startDateParam);
context.putString("endDate", endDateParam);

partitions.put("partition" + i, context);
}

return partitions;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package com.loopers.application.batch.product;

import com.loopers.core.domain.product.repository.DailyProductMetricRepository;
import com.loopers.core.domain.product.vo.ProductMetricAggregation;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import org.springframework.batch.item.ExecutionContext;
import org.springframework.batch.item.ItemStreamReader;
import org.springframework.lang.Nullable;

import java.time.LocalDate;
import java.util.Iterator;
import java.util.Objects;

@RequiredArgsConstructor
public class WeeklyProductMetricBatchReader implements ItemStreamReader<ProductMetricAggregation> {

private final DailyProductMetricRepository dailyProductMetricRepository;
private Iterator<ProductMetricAggregation> iterator;

@Override
public void open(@NonNull ExecutionContext executionContext) {
LocalDate startDate = LocalDate.parse(executionContext.getString("startDate"));
LocalDate endDate = LocalDate.parse(executionContext.getString("endDate"));
long partitionOffset = executionContext.getLong("partitionOffset");
long partitionLimit = executionContext.getLong("partitionLimit");

this.iterator = dailyProductMetricRepository.findAggregatedBy(startDate, endDate, partitionOffset, partitionLimit)
.iterator();
}

@Nullable
@Override
public ProductMetricAggregation read() {
if (Objects.isNull(iterator) || !iterator.hasNext()) {
return null;
}

return iterator.next();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.loopers.application.batch.product;

import com.loopers.core.domain.common.vo.YearMonthWeek;
import com.loopers.core.domain.product.WeeklyProductMetric;
import com.loopers.core.domain.product.repository.WeeklyProductMetricRepository;
import com.loopers.core.domain.product.vo.ProductMetricAggregation;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import org.springframework.batch.core.configuration.annotation.StepScope;
import org.springframework.batch.item.Chunk;
import org.springframework.batch.item.ExecutionContext;
import org.springframework.batch.item.ItemStreamWriter;
import org.springframework.stereotype.Component;

import java.time.LocalDate;
import java.util.List;

@StepScope
@Component
@RequiredArgsConstructor
public class WeeklyProductMetricBatchWriter implements ItemStreamWriter<ProductMetricAggregation> {

private final WeeklyProductMetricRepository repository;
private YearMonthWeek yearMonthWeek;

@Override
public void open(@NonNull ExecutionContext executionContext) {
LocalDate startDate = LocalDate.parse(executionContext.getString("startDate"));
this.yearMonthWeek = YearMonthWeek.from(startDate);
}
Comment on lines +19 to +30
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

โš ๏ธ Potential issue | ๐Ÿ”ด Critical

์‹ฑ๊ธ€ํ†ค Bean์—์„œ ๊ฐ€๋ณ€ ์ƒํƒœ๋กœ ์ธํ•œ ๋™์‹œ์„ฑ ๋ฌธ์ œ

@Component๋Š” ์‹ฑ๊ธ€ํ†ค ์Šค์ฝ”ํ”„์ด์ง€๋งŒ, yearMonthWeek ํ•„๋“œ๋Š” ๊ฐ€๋ณ€ ์ƒํƒœ์ž…๋‹ˆ๋‹ค. ํŒŒํ‹ฐ์…˜์ด ๋ณ‘๋ ฌ๋กœ ์‹คํ–‰๋  ๊ฒฝ์šฐ, ์—ฌ๋Ÿฌ ์Šค๋ ˆ๋“œ๊ฐ€ ๋™์ผํ•œ ์ธ์Šคํ„ด์Šค๋ฅผ ๊ณต์œ ํ•˜๋ฉด์„œ open() ๋ฉ”์„œ๋“œ์—์„œ yearMonthWeek๋ฅผ ๋ฎ์–ด์“ธ ์ˆ˜ ์žˆ์–ด ๋ฐ์ดํ„ฐ ์ •ํ•ฉ์„ฑ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

@StepScope๋ฅผ ์ถ”๊ฐ€ํ•˜์—ฌ ๊ฐ ํŒŒํ‹ฐ์…˜์ด ๋…๋ฆฝ์ ์ธ Writer ์ธ์Šคํ„ด์Šค๋ฅผ ๊ฐ–๋„๋ก ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ œ์•ˆ๋œ ์ˆ˜์ •
 import org.springframework.batch.item.Chunk;
 import org.springframework.batch.item.ExecutionContext;
 import org.springframework.batch.item.ItemStreamWriter;
+import org.springframework.batch.core.configuration.annotation.StepScope;
 import org.springframework.stereotype.Component;

 @Component
+@StepScope
 @RequiredArgsConstructor
 public class WeeklyProductMetricBatchWriter implements ItemStreamWriter<ProductMetricAggregation> {
๐Ÿค– Prompt for AI Agents
In
apps/commerce-batch/src/main/java/com/loopers/application/batch/product/WeeklyProductMetricBatchWriter.java
around lines 17 to 28, the class is a singleton component but holds mutable
state in the yearMonthWeek field which can be overwritten concurrently by
parallel partitions; make the writer step-scoped so each partition gets its own
instance: annotate the class with Spring Batch's @StepScope (and add the
necessary import) so dependency injection still works but a new writer instance
is created per step execution, keep yearMonthWeek as an instance field
(non-static) and leave open() logic unchanged; verify no other shared mutable
fields exist.


@Override
public void write(@NonNull Chunk<? extends ProductMetricAggregation> chunk) {
List<WeeklyProductMetric> weeklyMetrics = chunk.getItems().stream()
.map(aggregation -> aggregation.to(yearMonthWeek))
.toList();

repository.bulkUpsert(weeklyMetrics);
}
}
Loading