Reactive data with R2DBC and Spring Data
Sections
Reading settings
No voice for this language in this browser.
More voices
Code and English terms
Off: the page’s voice reads everything, with no pause between two voices.
No voice for this language in this browser.
More voices
More voices: Microsoft Edge has natural online voices, and Windows (Settings › Time & language › Speech), macOS and Android install others.
R2DBC is a reactive database contract. Unlike JDBC, it does not dedicate a blocked thread to each query. Spring Data R2DBC maps rows and composes database work as Mono and Flux.
The boundary, not just the return type
Section titled “The boundary, not just the return type”Returning Flux<Row> around a JDBC call is still blocking. A true R2DBC driver produces Reactive Streams signals from connection acquisition through row decoding. The lesson module uses the all-Java H2 R2DBC driver in memory, so the same test runs on Windows, Linux and macOS without Docker.
@Table("voicing")public record Voicing(@Id Long id, String symbol, int fret) { public static Voicing newVoicing(String symbol, int fret) { return new Voicing(null, symbol, fret); }}R2dbcEntityTemplate is the direct counterpart of using an EF Core DbContext without hiding it behind a generic repository:
public Mono<Voicing> save(Voicing voicing) { return template.insert(voicing);}
public Flux<Voicing> findBySymbol(String symbol) { return template.select(Voicing.class) .matching(Query.query(Criteria.where("symbol").is(symbol))) .all();}Nothing executes until subscription. The test chains schema creation, inserts and selection, then verifies the exact signal sequence with StepVerifier. block() appears only in test setup, outside the pipeline being taught.
Run the evidence:
mvn -B -pl l05-r2dbc -am testTransactions are publishers too
Section titled “Transactions are publishers too”R2DBC transactions live on a connection and are asynchronous. The second test begins a transaction, inserts G7, rolls it back and proves that the following query is empty. Application code normally uses R2dbcTransactionManager and TransactionalOperator; the explicit connection makes the lifecycle visible.
Do not share one mutable connection between subscribers, call blocking JDBC code on the event loop, or rely on generated IDs before the insert publisher completes. Backpressure controls row delivery, not database capacity: connection-pool limits still matter.
Exercises
Section titled “Exercises”- Add
findAtOrAboveFret(int fret)and return rows ordered by fret.
Solution
Use Query.query(Criteria.where("fret").greaterThanOrEquals(fret)).sort(Sort.by("fret")), then verify the ordered values with StepVerifier.expectNextMatches.
- Insert two rows inside one transaction, force an error after the second insert, and prove that neither row remains.
Solution
Compose both statements on the same connection, append Mono.error, and use onErrorResume only after rollbackTransaction(). The important invariant is atomicity, not the exception text.
- Explain when JDBC on virtual threads is simpler than R2DBC.
Solution
Choose JDBC when the driver, ORM and team are blocking-first and concurrency is moderate. Choose R2DBC when the whole request path is reactive and high concurrency would otherwise consume many blocked threads. A reactive controller alone is not sufficient evidence.