--- name: mariadb-connector-j-usage description: "MariaDB-specific behavior of MariaDB Connector/J (the `org.mariadb.jdbc:mariadb-java-client` JDBC driver) for application code — that JDBC 4 auto-registers the driver so `Class.forName` is unneeded; that the URL scheme is `jdbc:mariadb://host:port/db?opts` (`jdbc:mysql:` needs `permitMysqlScheme`); that `useServerPrepStmts` defaults to false, so `PreparedStatement` substitutes parameters client-side; that batched INSERTs use the `COM_STMT_BULK` protocol by default and `getGeneratedKeys()` then returns all generated ids, one per row, not a single id; that `autocommit` defaults to true so transactions need `setAutoCommit(false)`; that `setFetchSize(Integer.MIN_VALUE)` throws (fetch size must be >= 0); that pooling should use `MariaDbPoolDataSource` or an external pool; and that failover uses multi-host URLs, not driver code. Use when writing or reviewing Java code that talks to MariaDB with the `mariadb-java-client` JDBC driver." --- # MariaDB Connector/J *Last updated: 2026-08-10* MariaDB Connector/J is the official JDBC driver for MariaDB and MySQL — Maven coordinates `org.mariadb.jdbc:mariadb-java-client`, LGPL-licensed. 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 URL, TLS, and pooling, see **`mariadb-connector-j-install`**. It is **pure Java** — it implements the client/server protocol itself and does **not** depend on MariaDB Connector/C (unlike Connector/Python or Connector/ODBC). > **Default context:** Assume the **3.5.x** line (current release `3.5.10`) unless the user states otherwise. Connector versions are independent of the MariaDB server version — a 3.x connector routinely talks to a 10.6/10.11/11.x server. ## What LLMs Often Miss | If the agent writes / assumes… | …prefer the MariaDB form | |---|---| | `Class.forName("org.mariadb.jdbc.Driver")` before connecting | **Not needed.** The driver self-registers via the JDBC 4 `META-INF/services/java.sql.Driver` mechanism — `DriverManager.getConnection(url)` alone finds it. The legacy call still works, but only add it if targeting JDBC 3-era containers | | A `jdbc:mysql://host:3306/db` URL | Use **`jdbc:mariadb://host:port/db?opts`**. The `jdbc:mysql:` prefix is only accepted when the `permitMysqlScheme` option is present in the URL — otherwise the driver won't claim it | | Builds SQL with string concat / `String.format()` / `+` | Never. Use **`PreparedStatement`** with `?` placeholders and `setX(index, value)` — the driver escapes/binds values, closing off SQL injection | | Expects server-side prepared statements (binary protocol) by default | **`useServerPrepStmts` defaults to `false`.** `PreparedStatement` parameter substitution happens **client-side** (text protocol) unless you set `useServerPrepStmts=true` on the URL. Enable it when the application reuses a bounded set of statements that stays within the prepare cache (`prepStmtCacheSize`, default **250**) — repeated executions then reuse the server-side prepare, a substantial win. Leave it at the default when queries are mostly distinct: each fills a cache slot and consumes a server-side prepare (watch `max_prepared_stmt_count`) for no reuse benefit | | Loops single `executeUpdate()` calls for bulk insert/update | Use **`addBatch()` / `executeBatch()`**. For `INSERT` batches, 3.x already sends them via the `COM_STMT_BULK` protocol by default (`useBulkStmtsForInserts=true`, `useBulkStmts` itself stays `false` for non-insert statements) — no extra option needed for the common case | | Reads `getGeneratedKeys()` after a batch insert expecting a single id | `getGeneratedKeys()` returns **all** generated ids — one per inserted row — and the result is the **same regardless of server**. The driver only varies the mechanism: a fast bulk unit-results path on a **MariaDB 11.5.1+** server (capability, [MDEV-30366](https://jira.mariadb.org/browse/MDEV-30366)) and a **pipelined** fallback on older MariaDB or MySQL servers. Don't code for a single id | | Assumes autocommit is off; wraps every statement in `setAutoCommit(false)` needlessly, or forgets to call it | **`autocommit` defaults to `true`** (standard JDBC, opposite of Connector/Python and Connector/C). For a transaction, call `conn.setAutoCommit(false)` once, then `conn.commit()` / `conn.rollback()` explicitly — don't rely on statement-level commit | | Streams a huge `ResultSet` with `setFetchSize(Integer.MIN_VALUE)` | **Throws `SQLException`** — MariaDB Connector/J requires `rows >= 0`. Use a **positive** value, e.g. `stmt.setFetchSize(1000)`, on a `TYPE_FORWARD_ONLY` statement to stream row-by-row. Even then, the fetch size is capped internally (16384) and **the server still sends all rows** to the client socket — running another statement on the same connection before the stream is fully read pulls the remainder into memory (OOM risk); use a separate connection for concurrent work | | Hand-rolls a pool, or opens a new `Connection` per request | Use **`MariaDbPoolDataSource`** (internal pool: `maxPoolSize` default **8**, `minPoolSize` defaults to `maxPoolSize`, `maxIdleTime` default **600s**) or wrap the plain `MariaDbDataSource`/driver URL with an external pool (HikariCP). `connection.close()` on a pooled connection returns it to the pool — it doesn't disconnect — and resets autocommit, isolation level, and any active transaction (rolled back) | | Builds a custom failover/round-robin loop over multiple hosts | Multi-host URLs are native: **`jdbc:mariadb:sequential://host1,host2,host3/db`** (also `replication:`, `loadbalance:`, `load-balance-read:` prefixes) — comma-separate hosts in one URL instead of driver-side retry code | | Enables TLS with `useSSL=true&verifyServerCertificate=true` (other-driver naming) | Since 3.0 the option is **`sslMode`**: `disable` (the default) / `trust` / `verify-ca` / `verify-full`. Certificate and key material is configuration — see **`mariadb-connector-j-install`** | | Calls a stored procedure with `prepareStatement("CALL …")` and reads `OUT` parameters from a result set | Use **`CallableStatement`** from `conn.prepareCall("{call proc(?, ?)}")`, register `OUT` parameters with `registerOutParameter()`, and read them from the statement after `execute()` | | Assumes `executeUpdate()` returns a `long` for very large row counts | It returns `int`. For counts beyond `Integer.MAX_VALUE` use **`executeLargeUpdate()`** / **`executeLargeBatch()`** | | Leaves an isolation level to a `SET TRANSACTION` statement | `conn.setTransactionIsolation(Connection.TRANSACTION_REPEATABLE_READ)` applies it through the driver, and a pooled connection resets it on release | | Catches a generic `Exception` or expects a MariaDB-specific exception class | The driver throws the **standard `java.sql.SQLException` hierarchy** mapped from the server's SQLSTATE: `SQLIntegrityConstraintViolationException`, `SQLSyntaxErrorException`, `SQLTransactionRollbackException`, `SQLTransientConnectionException`, `SQLNonTransientConnectionException`. Read `.getErrorCode()` / `.getSQLState()` for the MariaDB-specific code, don't string-match `.getMessage()` | | Sets `Statement.executeUpdate(sql, Statement.RETURN_GENERATED_KEYS)` and assumes it "just works" for any statement | `getGeneratedKeys()` only returns rows when `RETURN_GENERATED_KEYS` was explicitly requested on that statement/`prepareStatement()` call — it's not implicit for a plain `INSERT` | ## Connection, prepared statements, and a transaction ```java import java.sql.*; // JDBC 4 auto-registers the driver — no Class.forName() needed try (Connection conn = DriverManager.getConnection( "jdbc:mariadb://localhost:3306/appdb", "app", "secret")) { conn.setAutoCommit(false); // autocommit defaults to true — turn it off for a transaction try (PreparedStatement ps = conn.prepareStatement( "INSERT INTO t (name, qty) VALUES (?, ?)")) { ps.setString(1, "widget"); ps.setInt(2, 5); ps.executeUpdate(); conn.commit(); } catch (SQLException e) { conn.rollback(); throw e; } try (PreparedStatement ps = conn.prepareStatement( "SELECT id, name FROM t WHERE qty > ?")) { ps.setInt(1, 0); try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { System.out.println(rs.getInt("id") + " " + rs.getString("name")); } } } } ``` ## Pooling with `MariaDbPoolDataSource` ```java import org.mariadb.jdbc.MariaDbPoolDataSource; import java.sql.*; try (MariaDbPoolDataSource pool = new MariaDbPoolDataSource( "jdbc:mariadb://localhost:3306/appdb?user=app&password=secret&maxPoolSize=10")) { try (Connection conn = pool.getConnection(); Statement stmt = conn.createStatement(); ResultSet rs = stmt.executeQuery("SELECT 1")) { rs.next(); } // conn.close() (implicit) returns the connection to the pool, it doesn't disconnect } ``` ## Batch insert and generated keys ```java try (PreparedStatement ps = conn.prepareStatement( "INSERT INTO t (name, qty) VALUES (?, ?)", Statement.RETURN_GENERATED_KEYS)) { for (Row row : rows) { ps.setString(1, row.name()); ps.setInt(2, row.qty()); ps.addBatch(); } ps.executeBatch(); try (ResultSet keys = ps.getGeneratedKeys()) { while (keys.next()) { System.out.println(keys.getLong(1)); // one id per inserted row } } } ``` `RETURN_GENERATED_KEYS` has to be requested when the statement is prepared; adding it later has no effect. ## See Also - **`mariadb-connector-j-install`** — adding the driver to a Maven or Gradle build, the URL and datasource wiring, and TLS configuration - **`mariadb-connector-r2dbc-usage`** — the reactive, non-blocking sibling driver for Java (also pure Java, no Connector/C dependency) - **`mariadb-transactions`** / **`mariadb-set-transaction`** — the server-side semantics behind `conn.commit()` / `conn.rollback()` and 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: _This page is: Copyright © 2026 MariaDB. All rights reserved._