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
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
# 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)
|
||||
Reference in New Issue
Block a user