Files
spring-boot-demo/spring-batch/docs/06-jdbc-writer-and-records.md
T
Claude b81af72bc3 Add spring-batch: jobs, steps, chunk processing and restartability on Boot 4.1
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
2026-09-13 06:37:46 +00:00

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 want beanMapped() with records anyway. Newer versions of Spring's BeanWrapperImpl (the machinery behind BeanPropertySqlParameterSource) 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 silent NULLs, 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

  • JdbcBatchItemWriter and 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.Introspector has always worked, and it bites anywhere a library assumes getX()/setX() — Jackson, JPA, validation frameworks — each with its own answer for how much (if any) record support it has added.

Next: Restartability →