# Mariadb Connector R2dbc Usage

> Explains MariaDB Connector/R2DBC specifics for writing reactive Java code: URL scheme, placeholder binding, lazy execution, consume-once results, transactions, pooling, and error handling.

- Skill: `mariadb-corporation/mariadb-connector-r2dbc-usage` (Agent Skill)
- Install (CLI): `npx skillmds add mariadb-corporation/mariadb-connector-r2dbc-usage`
- Raw SKILL.md: https://api.skillmd.com/api/skills/mariadb-corporation/mariadb-connector-r2dbc-usage/raw
- Safety review: PASS (external: skill-scanner PASS, skillspector PASS)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools, API Design
- Tags: Database, Java, Mariadb, R2dbc, Reactive, Reactor, Sql
- Author: mariadb-corporation (https://skillmd.com/u/mariadb-corporation)
- Updated: 2026-08-22
- Page: https://skillmd.com/skills/mariadb-corporation/mariadb-connector-r2dbc-usage

---


# MariaDB Connector/R2DBC

*Last updated: 2026-08-10*

MariaDB Connector/R2DBC is the official reactive, non-blocking driver for MariaDB implementing the [R2DBC](https://r2dbc.io) SPI — Maven coordinates `org.mariadb:r2dbc-mariadb`. It is **100% pure Java** (Java 8+), built on Reactor and Netty, and does **not** depend on Connector/C. This skill covers the connector-specific behavior and the traps that bite generated application code. For adding the driver to a build and configuring the factory, pooling, and TLS, see **`mariadb-connector-r2dbc-install`**. It is a separate, independently-versioned sibling of **MariaDB Connector/J** (the blocking JDBC driver, also pure Java) — same vendor, different API paradigm; do not assume JDBC idioms (blocking calls, `try`-with-resources returning a live `ResultSet`) carry over.

> **Default context:** Assume the **1.4** line (`r2dbc-mariadb` 1.4.1) unless the user states otherwise; 1.0 and 1.1 have reached end of life. The connector follows the [R2DBC 1.0.0 spec](https://r2dbc.io/spec/1.0.0.RELEASE/spec/html/) (a legacy `r2dbc-mariadb-0.9.1-spec` artifact exists for the older spec). Connector versions are independent of the MariaDB server version.

## What LLMs Often Miss

| If the agent writes / assumes… | …prefer the MariaDB form |
|---|---|
| A JDBC-style URL (`jdbc:mariadb://...`) or a bare host string | The R2DBC URL scheme is **`r2dbc:mariadb://[user:pw@]host[:port][,host2...]/db[?opt=val]`** — the registered driver id is `mariadb`. Get a factory with `ConnectionFactories.get(url)`, or build one explicitly: `MariadbConnectionConfiguration.builder()...build()` + `new MariadbConnectionFactory(config)` |
| `%s`/f-string/concat SQL, or assumes only `?` works | Two placeholder styles, never string-built SQL: positional **`?`** bound by **0-based** index (`.bind(0, val)`), and named **`:name`** bound by name (`.bind("name", val)`). Both are parsed from the same SQL text; pick one style per statement |
| Expects `execute()` to run the query immediately | `Statement.execute()` returns a **`Flux<MariadbResult>`** (a cold `Publisher`) — **nothing hits the wire until it is subscribed** (`.subscribe()`, `Mono.from(...).block()`, a Reactor operator chain, etc.). Building the statement and calling `execute()` alone does nothing |
| Reads a `Result` twice, or ignores it after `execute()` | A `Result` is **consume-once, forward-only**: call exactly one of `.map(BiFunction<Row,RowMetadata,T>)` or `.getRowsUpdated()`. Not fully consuming it can leave the corresponding statement incomplete |
| Binds `null` as a plain value | `.bind(index, null)` is invalid — use **`bindNull(index, Class<?> type)`** (or the named overload) so the driver knows the SQL type to send |
| Assumes DML needs an explicit commit, or that autocommit is off | **`autocommit` defaults to `true`** — new connections auto-commit each statement. To use transactions, call `setAutoCommit(false)` or explicitly `beginTransaction()` |
| Expects server-side prepared statements (binary protocol) by default | **`useServerPrepStmts` defaults to `false`** — the text protocol is used (parameters substituted client-side) unless set to `true`, which switches to server-side prepare + a prepare-result LRU cache (`prepareCacheSize`, default 256) |
| Calls `conn.commit()` / `conn.rollback()` (JDBC-style) | Transactions are reactive: **`beginTransaction()`**, **`commitTransaction()`**, **`rollbackTransaction()`** each return `Mono<Void>` — they are no-ops until subscribed, e.g. `Mono.from(connection.beginTransaction()).block()` or chained into the pipeline |
| Calls `.block()` inside a reactive chain to "simplify" the code | Blocking a Netty event-loop thread inside the pipeline is the classic reactive anti-pattern — it defeats non-blocking I/O and can deadlock the loop. Compose with `Mono`/`Flux` operators end-to-end; only `.block()` at the outermost boundary (e.g. `main`, a test) |
| Wraps calls in `try/catch (SQLException)` expecting a thrown exception | Errors are delivered as an **`onError` signal**, not a synchronous throw — subscribe with an error handler (`.doOnError()`, `.onErrorResume()`, etc.). Errors surface as typed `R2dbcException` subclasses (e.g. `R2dbcBadGrammarException`, `R2dbcDataIntegrityViolationException`, `R2dbcTransientResourceException`) derived from the server's SQLSTATE class |
| Hand-rolls a pool or opens a new connection per request | No pooling is built in — add **`r2dbc-pool`** and wrap the `MariadbConnectionFactory` in `ConnectionPoolConfiguration.builder(factory).maxSize(...).build()` → `new ConnectionPool(poolConfig)`; borrow with `pool.create()`, return by `.close()` |
| Loops single-row `execute()` calls for a bulk insert | Call **`Statement.add()`** between `.bind()` calls on the *same* parameterized statement to queue another parameter set, then `execute()` once — one round trip covers all bound rows |
| Uses `io.r2dbc.spi.Batch` expecting parameter binding | `Batch` (`conn.createBatch().add(sql1).add(sql2)...`) groups **distinct, already-literal SQL strings with no parameter binding** — for parameterized bulk operations use `Statement.add()` instead (see above) |
| Assumes TLS is on by default | `sslMode` defaults to **`DISABLE`** (no TLS) — set it explicitly (`TRUST`/`VERIFY_CA`/`VERIFY_FULL`) for anything beyond local development. Certificate configuration lives in **`mariadb-connector-r2dbc-install`** |
| Runs a second `SELECT` to read back an auto-generated id | **`Statement.returnGeneratedValues(columns…)`** makes the insert itself yield them. Note the server-version constraint: **more than one column requires MariaDB 10.5.1 or later**, and asking for several against an older server throws `IllegalArgumentException` |
| Pulls a large result set through the default flow control | **`Statement.fetchSize(rows)`** switches the statement to cursor-based fetching, so rows arrive in bounded chunks instead of one large burst |
| Rolls back the whole transaction to undo one step | Savepoints are reactive too: **`createSavepoint(name)`**, **`rollbackTransactionToSavepoint(name)`**, **`releaseSavepoint(name)`**, each returning `Mono<Void>` that must be subscribed |
| Sets an isolation level or a lock timeout with a raw `SET` statement | The connection exposes them directly: **`setTransactionIsolationLevel()`**, **`setLockWaitTimeout(Duration)`**, **`setStatementTimeout(Duration)`** |

## Connection, query, and result mapping

```java
import io.r2dbc.spi.ConnectionFactories;
import io.r2dbc.spi.ConnectionFactory;
import io.r2dbc.spi.Connection;
import io.r2dbc.spi.Result;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

ConnectionFactory factory =
    ConnectionFactories.get("r2dbc:mariadb://app:secret@localhost:3306/appdb");

Flux<String> names = Mono.from(factory.create())
    .flatMapMany(conn ->
        Flux.from(conn.createStatement("SELECT name FROM t WHERE qty > ?")
                .bind(0, 0)          // positional, 0-based
                .execute())
            .flatMap(result -> result.map((row, meta) -> row.get("name", String.class)))
            .concatWith(Mono.from(conn.close()).then(Mono.empty())));

names.subscribe(System.out::println);   // nothing runs before this subscribe
```

Transactions (manual, reactive — no implicit commit/rollback):

```java
Mono<Void> work = Mono.from(factory.create())
    .flatMap(conn -> Mono.from(conn.beginTransaction())
        .then(Mono.from(conn.createStatement(
                "INSERT INTO t (name) VALUES (:name)")
            .bind("name", "widget")
            .execute()))
        .flatMap(res -> Mono.from(res.getRowsUpdated()))
        .then(Mono.from(conn.commitTransaction()))
        .onErrorResume(e -> Mono.from(conn.rollbackTransaction()).then(Mono.error(e)))
        .then(Mono.from(conn.close())));

work.block();   // only block at the outermost boundary
```

## Bulk insert and generated values

```java
Mono.from(factory.create())
    .flatMapMany(conn -> {
      Statement stmt = conn.createStatement(
          "INSERT INTO t (name, qty) VALUES (?, ?)")
          .returnGeneratedValues("id");

      stmt.bind(0, "widget").bind(1, 5).add();      // queue this parameter set
      stmt.bind(0, "gadget").bind(1, 3);            // no trailing add() on the last one

      return Flux.from(stmt.execute())
          .flatMap(result -> result.map((row, meta) -> row.get("id", Long.class)))
          .concatWith(Mono.from(conn.close()).then(Mono.empty()));
    })
    .subscribe(System.out::println);
```

`add()` separates parameter sets on one statement; a trailing `add()` after the final bind queues an empty set and is a common source of a spurious extra execution.

## See Also

- **`mariadb-connector-r2dbc-install`** — adding the driver to a build, the `ConnectionFactory`, `r2dbc-pool`, and TLS configuration
- **`mariadb-connector-j-usage`** — the blocking JDBC sibling (also pure Java, no Connector/C); use it when the application isn't reactive
- **`mariadb-transactions`** / **`mariadb-set-transaction`** — server-side semantics behind `beginTransaction()`/`commitTransaction()`/isolation levels
- **`mariadb-prepare`** — server-side prepared statements, what `useServerPrepStmts=true` uses under the hood
- Canonical reference on `mariadb.com/docs`, consult for edge cases not covered here: <https://mariadb.com/docs/connectors/mariadb-connector-r2dbc>

<sub>_This page is: Copyright © 2026 MariaDB. All rights reserved._</sub>

