Files
spring-boot-demo/spring-batch-partitioning/docs/04-the-writer-and-the-beanmapped-trap.md

4.0 KiB

4. The writer, the beanMapped trap, and finding the partition's own name

← Previous | README | Next: The diagnostic endpoint →

Not beanMapped(), again

The earlier spring-batch module already found this once: Order and RiskScoredOrder are Java records, and JdbcBatchItemWriterBuilder.beanMapped() binds SQL parameters through BeanPropertySqlParameterSource, which looks for JavaBean-style getters (getOrderId()). A record's accessors are orderId(), no get prefix — beanMapped() would silently bind every column to NULL against this exact shape, because BeanPropertySqlParameterSource treats a missing getter as an absent property rather than an error. This module's riskWriter uses an explicit itemPreparedStatementSetter instead, precisely to avoid rediscovering that bug a second time.

Finding out which partition wrote which row

ORDER_RISK_SUMMARY.partition_name exists so a reader (or a bug report) can tell which worker wrote a given row. The first attempt to populate it reached for StepSynchronizationManager.getContext().getStepExecution().getStepName() inside the itemPreparedStatementSetter lambda, guessing the class lived at org.springframework.batch.core.step.StepSynchronizationManager by analogy with StepExecution living in org.springframework.batch.core.step. It does not compile:

error: package org.springframework.batch.core.step does not exist
       (well -- StepSynchronizationManager specifically does not exist there)

javap against the real jar answers where it actually is: docs/output/03-package-repackaging-javap.txt shows org.springframework.batch.core.scope.context.StepSynchronizationManager — alongside JobScope and StepScope's own scope-context machinery, which is the more sensible package for it in hindsight; the guess just followed the wrong analogy.

The fix that shipped is simpler than getting the import right would have been: late-bind the step name directly, the same mechanism chapter 2 already uses for the shard file path.

@Bean
@StepScope
public ItemWriter<RiskScoredOrder> riskWriter(JdbcTemplate jdbcTemplate,
                                               @Value("#{stepExecution.stepName}") String partitionName) {
    return new JdbcBatchItemWriterBuilder<RiskScoredOrder>()
            .dataSource(jdbcTemplate.getDataSource())
            .sql("MERGE INTO ORDER_RISK_SUMMARY (order_id, customer_id, amount_cents, region, " +
                    "risk_score, high_risk, partition_name) KEY(order_id) VALUES (?, ?, ?, ?, ?, ?, ?)")
            .itemPreparedStatementSetter((item, ps) -> {
                // ... ps.setString(7, partitionName);
            })
            .assertUpdates(true)
            .build();
}

#{stepExecution.stepName} reaches the current StepExecution's name (ordersWorkerStep:partition2, for instance) through the same @StepScope proxy machinery that resolves #{stepExecutionContext['fileName']} — no lookup class needed, no package to get wrong. MERGE ... KEY(order_id) (H2 upsert syntax) rather than a plain INSERT was a deliberate second choice: it makes re-running a single partition idempotent, which matters once chapter 8 starts re-executing partitions after a failure.

Going deeper

Next: The diagnostic endpoint →