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

60 lines
3.6 KiB
Markdown

# 6. The JDBC writer, and why it is not `beanMapped()`
[&larr; Previous](05-launching-and-jobparameters.md) | [README](../README.md) | [Next: Restartability &rarr;](07-restartability.md)
`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:
```java
public record Product(String sku, String name, long priceCents) {}
```
Its accessors are `sku()`, `name()`, `priceCents()` &mdash; 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 &mdash; not a mistake you want to discover from a table full of NULLs
in production.
[`BatchConfig.productWriter`](../src/main/java/com/ankurm/batch/config/BatchConfig.java) sidesteps
the question entirely with `itemPreparedStatementSetter`, setting each column explicitly:
```java
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.
<blockquote style="background:#f4f5f7;border:1px solid #e2e5ea;border-left:4px solid #b7bec9;border-radius:6px;padding:16px 20px;"><strong>If you want <code>beanMapped()</code> with records anyway.</strong> Newer versions of Spring's <code>BeanWrapperImpl</code> (the machinery behind <code>BeanPropertySqlParameterSource</code>) 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 &mdash; the failure mode when it does not work is silent <code>NULL</code>s, not an exception, which is the worst kind of wrong.</blockquote>
`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 &mdash; item writers](https://docs.spring.io/spring-batch/reference/readers-and-writers/item-writer.html) (`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()` &mdash; Jackson, JPA, validation frameworks &mdash; each with its own answer
for how much (if any) record support it has added.
[Next: Restartability &rarr;](07-restartability.md)