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
60 lines
3.6 KiB
Markdown
60 lines
3.6 KiB
Markdown
# 6. The JDBC writer, and why it is not `beanMapped()`
|
|
|
|
[← Previous](05-launching-and-jobparameters.md) | [README](../README.md) | [Next: Restartability →](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()` — 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`](../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 — 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 — 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()` — Jackson, JPA, validation frameworks — each with its own answer
|
|
for how much (if any) record support it has added.
|
|
|
|
[Next: Restartability →](07-restartability.md)
|