Companion code for "Spring Batch on Boot 4.1: Jobs, Steps, Chunk Processing and Restartability". A productImportJob configured three ways by profile against a poisoned CSV row, run as real java -jar processes (not just JUnit) so the restart story is genuine: a chunk fails and rolls back, the process exits, a brand-new JVM against the same file-based H2 database resumes at the exact next unread row (READ_COUNT 20, not 60) and completes. Findings the build pins: - StepBuilder.chunk(int, PlatformTransactionManager) still compiles in Batch 6.0.5 but returns the legacy SimpleStepBuilder; chunk(int) returns the new ChunkOrientedStepBuilder, and only the latter is used here. - Two different ExecutionContext classes now exist in two different packages (infrastructure.item vs core.repository.persistence) with different shapes. - spring-boot-starter-batch alone gives a resourceless JobRepository that forgets every JobInstance the moment the JVM exits; spring-boot-starter- batch-jdbc is what makes the restart demo possible at all, demonstrated by excluding BatchJdbcAutoConfiguration and watching a "restart" collide with the previous run's own data instead of resuming it. - A migration-guide summary claiming CommandLineJobRunner was removed in 6.0 is wrong -- javap against the real jar shows @Deprecated(forRemoval=true), not removed. - RepeatStatus moved from core.repeat to infrastructure.repeat, caught by the compiler rather than by reading docs. 11 documentation chapters, 10 captured transcripts (unit tests, javap output, and real two-JVM scenario runs), all regenerated by scripts/run-all.sh. Fixed after push: three dead docs.spring.io links in the doc chapters (readersAndWriters/* and chunk-oriented-processing/*.html paths moved when Spring Batch 6 reorganized its reference docs; corrected to the current readers-and-writers/*, processor.html and chunk-oriented-processing.html paths, verified 200 via curl before committing). Co-Authored-By: Claude Sonnet 5 <[email protected]> Claude-Session: https://claude.ai/code/session_019DXsJ1zpikbA1MQJN6RqFA
3.6 KiB
6. The JDBC writer, and why it is not beanMapped()
← Previous | README | Next: Restartability →
JdbcBatchItemWriterBuilder offers two ways to map an item's fields to SQL parameters:
columnMapped() (positional, via Map/SqlParameterSource keyed by column name) and
beanMapped() (reflective, via BeanPropertySqlParameterSource, matching :paramName markers
in the SQL to JavaBean getters). Most Spring Batch examples reach for beanMapped() because it
needs the least code.
Product in this module is a Java record:
public record Product(String sku, String name, long priceCents) {}
Its accessors are sku(), name(), priceCents() — no get prefix. Standard JavaBean
introspection, which BeanPropertySqlParameterSource uses, looks for getSku(), getName(),
getPriceCents(). Those do not exist on a record, so beanMapped() against a plain record either
finds nothing to bind (leaving every parameter NULL) or fails outright, depending on the exact
introspector version in play — not a mistake you want to discover from a table full of NULLs
in production.
BatchConfig.productWriter sidesteps
the question entirely with itemPreparedStatementSetter, setting each column explicitly:
new JdbcBatchItemWriterBuilder<Product>()
.dataSource(jdbcTemplate.getDataSource())
.sql("INSERT INTO PRODUCT (sku, name, price_cents) VALUES (?, ?, ?)")
.itemPreparedStatementSetter((item, ps) -> {
ps.setString(1, item.sku());
ps.setString(2, item.name());
ps.setLong(3, item.priceCents());
})
.assertUpdates(true)
.build();
More typing, zero ambiguity about what gets bound where, and it works identically whether the item type is a record, a plain class, or something with no JavaBean getters at all.
If you wantbeanMapped()with records anyway. Newer versions of Spring'sBeanWrapperImpl(the machinery behindBeanPropertySqlParameterSource) have gained some record support in recent Spring Framework releases, but the safe rule is to check it against the exact Spring Framework version you are on rather than assume — the failure mode when it does not work is silentNULLs, not an exception, which is the worst kind of wrong.
assertUpdates(true) is worth keeping on deliberately: it makes the writer throw if a batch
statement reports zero rows updated for any item, instead of silently accepting a no-op write.
Combined with itemPreparedStatementSetter, a typo in a column name fails loudly at the first
write attempt rather than producing a table that looks plausible but is missing a column's worth
of data.
Going deeper
JdbcBatchItemWriterand its two mapping styles: Spring Batch reference — item writers (rel="nofollow").- Java records and JavaBean introspection generally: this is not a Spring Batch quirk, it is how
java.beans.Introspectorhas always worked, and it bites anywhere a library assumesgetX()/setX()— Jackson, JPA, validation frameworks — each with its own answer for how much (if any) record support it has added.