Jdbi (Java / JVM)
A convenience layer over JDBC that runs the raw SQL you write and maps result rows to Java objects, offered through two APIs over one core — a fluent, imperative Handle/statement builder and a declarative SQL Object layer of annotated interfaces Jdbi implements at runtime — with injection-safe parameter binding and a pluggable RowMapper/ColumnMapper/BeanMapper system, and deliberately no ORM machinery above that rung.
| Field | Value |
|---|---|
| Language | Java (usable from Kotlin, Clojure, Scala, and other JVM languages; requires Java 17+) |
| License | Apache-2.0 (LICENSE) |
| Repository | jdbi/jdbi |
| Documentation | jdbi.org Developer Guide · Javadoc · docs/src/adoc/index.adoc |
| Category | Safe-SQL / micro-mapper — raw SQL + injection-safe binding + row→object mapping; two API styles, no query DSL, no ORM |
| Abstraction level | Safe-SQL / micro-mapper rung — a Handle wraps a JDBC Connection; parameters bind out-of-band, rows hydrate into types |
| Query model | Raw SQL string you write — fluent (createQuery(...).bind(...)) or annotated interfaces (@SqlQuery("select ...")) |
| Effect/async model | Blocking JDBC; exception-based errors (unchecked JdbiException); optional executor-backed CompletionStage async facade |
| Backends | Any database with a JDBC driver — Postgres, MySQL/MariaDB, SQLite, Oracle, H2, and others; vendor-type plugins for Postgres/MySQL/SQLite/Oracle |
| First release | Jdbi 3 ≈2017 (web-attested; the Jdbi/DBI lineage dates to ≈2011) |
| Latest version | 3.54.0 @ 2026-07-01 (index.adoc); pinned tree at 3.54.1-SNAPSHOT |
NOTE
Jdbi sits on the safe-SQL / micro-mapper rung of the abstraction ladder — one step above a bare driver (JDBC itself) and far below a full ORM. You write every SQL string; Jdbi binds your parameters out-of-band and maps the returned columns to Java types. It is this survey's JVM thin-mapper baseline — the Java analogue of Dapper (.NET), hasql (Haskell), and Go database/sql+sqlx, and the deliberate antithesis of the heavyweight Hibernate/JPA it refuses to become. Its distinguishing feature within that niche is a two-API duality: the same core is driven either fluently or through a declarative annotated-interface DAO layer. See concepts for shared vocabulary.
Overview
What it solves
Raw JDBC is verbose and error-prone: you acquire a Connection, build a PreparedStatement, setXxx each parameter by index, executeQuery, loop a ResultSet, and pull each column out by ordinal into a hand-written object — all while managing close() on every resource in the right order. Jdbi collapses that ceremony without moving up to an ORM. It keeps the SQL in your hands and takes over exactly two mechanical jobs — turning values into bound parameters and turning result columns into typed objects — plus the resource and transaction plumbing around them. The project's one-line pitch (README.md):
"The Jdbi library provides convenient, idiomatic access to relational databases in Java and other JVM technologies such as Kotlin, Clojure or Scala."
Jdbi does not replace JDBC; it refines it (README.md):
"Jdbi is built on top of JDBC. If your database has a JDBC driver, you can use Jdbi with it."
That is the whole portability story: Jdbi speaks whatever JDBC speaks, so any database with a driver works, and the SQL dialect stays your responsibility because Jdbi generates none.
Design philosophy
Four stated principles define Jdbi, each a verbatim claim from its own developer guide.
It is emphatically not an ORM — and the absence is the feature. The guide states it twice, first as a headline (index.adoc):
"Jdbi is not an ORM. It is a convenience library to make Java database operations simpler and more pleasant to program than raw JDBC. While there is some ORM-like functionality, Jdbi goes to great length to ensure that there is no hidden magic that makes it hard to understand what is going on."
and then concretely, enumerating exactly which ORM machinery is missing (index.adoc):
"Jdbi is not an ORM. There is no session cache, "open session in view", change-tracking, or cajoling the library to understand your schema."
SQL is a first-class language, not something to hide. (index.adoc):
"Jdbi does not hide SQL away. One of the design principles of Jdbi is that SQL is the native language of the database, and it is unnecessary to wrap it into code, deconstruct it, or hide it away."
The corollary is the division of labour (index.adoc): "Jdbi provides straightforward mapping between SQL and data accessible through a JDBC driver. You bring your own SQL, and Jdbi executes it." There is no query builder and no compile-time SQL check — the raw-string query model, checked never at compile time, the opposite pole from jOOQ/Diesel (typed builders) or sqlx/sqlc (build-time-verified SQL).
It provides primitives, not a framework. (index.adoc): "Jdbi does not aim to provide a complete database management framework. It provides the building blocks that allow constructing the mapping between data and objects as appropriate for your application and the necessary primitives to execute SQL code against your database."
Two APIs, one core. Jdbi's headline structural choice is that it exposes the same engine through two surfaces (index.adoc): "Jdbi's API comes in two flavors" — the Fluent API (a builder-style, imperative core) and the declarative SQL Object extension. Crucially they are not rivals (index.adoc): "The declarative API uses the [...] fluent API "under the hood" and the two styles can be mixed." This duality is the through-line of the two sections below.
Connection, pooling & resource lifetime
The Jdbi class is the entry point and the long-lived object. Its own Javadoc calls it the "Main entry point; configurable wrapper around a JDBC DataSource. Use it to obtain Handle instances and provide configuration for all handles obtained from it." (Jdbi.java). You construct one per data source, from a JDBC URL, a DataSource, a ConnectionFactory, or a single Connection, and share it (index.adoc): "Jdbi instances are thread-safe and do not own any database resources."
Jdbi does not pool. This is an explicit non-goal (index.adoc): "Jdbi does not provide connection pooling or other [...] features, but it can be combined with other software that does." In practice you hand it a pooling DataSource (HikariCP, etc.), exactly as Dapper leaves pooling to the ADO.NET provider.
The unit of work is a Handle — a wrapped, live connection. Its Javadoc (Handle.java):
"This represents a connection to the database system. It is a wrapper around a JDBC Connection object. Handle provides essential methods for transaction management, statement creation, and other operations tied to the database session."
Handles are short-lived and must be closed to release the connection (index.adoc), and they are not thread-safe. The idiomatic path never closes one by hand: the withHandle / useHandle callbacks on Jdbi fully manage the lifecycle. withHandle is "A convenience function which manages the lifecycle of a handle and yields it to a callback for use by clients." (Jdbi.java) — withHandle returns a value, useHandle does not. The naming convention is systematic: with- methods return and pair with -Callback objects, use- methods are void and pair with -Consumer objects. A raw Jdbi#open() returns an unmanaged handle for the rare case (streaming a ResultSet past the callback boundary) that a try-with-resources block must own it directly — with the guide's caution that "Failing to release the handle will leak connections." (index.adoc).
SQL Object types add a third lifetime, Jdbi#onDemand(...), whose instances "obtain and release connections for each method call [...] are thread-safe, and may be reused across an application" (sqlobject/package-info.java) — a connection-per-call convenience that trades a small acquisition cost for statelessness. There is no scoped acquire/release Resource value of the kind the effect systems model; resource safety is the managed-callback discipline plus JDBC close().
Query construction & injection safety
This is Jdbi's centre of gravity, and its defining trait is that the same raw-SQL, out-of-band-binding model is offered through two API styles.
The safety mechanism: bound arguments, never interpolation
Arguments are Jdbi's wrapper over JDBC statement parameters (index.adoc): "Arguments are Jdbi's representation of JDBC statement parameters (the ? in SELECT * FROM Foo WHERE bar = ?)." When you bind a value, Jdbi finds an ArgumentFactory that converts it to an Argument, whose job is to call setString/setInt/… on the PreparedStatement exactly as hand-written JDBC would. The value therefore travels the JDBC parameter channel — it is never spliced into SQL text — so SQL injection is structurally impossible for a bound value. The guide is explicit that binding is the safe path (index.adoc): "Binding ensures that the parameterized query string (... where foo = ?) is transmitted to the database without allowing hostile parameter values to inject SQL."
Two placeholder syntaxes are supported: positional ? tokens bound by 0-based index, and named :name tokens bound by name (parsed by the default ColonPrefixSqlParser; a HashPrefixSqlParser swaps in #name when a colon is awkward in the dialect). Mixing the two in one statement is disallowed. A key scoping note (index.adoc): "arguments usually cannot be used to change the structure of a query (for example the table or column name, SELECT or INSERT, etc.) nor may they be interpolated into string literals" — structural changes are the templating engine's job (below), not binding's.
API style 1: the Fluent API
You obtain a statement builder from the Handle — createQuery for result-bearing statements, createUpdate for INSERT/UPDATE/DELETE/DDL, plus createBatch, createCall (stored procedures), and createScript. createQuery returns "a Query instance that executes a statement with bound parameters and maps the result set into Java types." (Handle.java). Binding is a fluent chain; a Query is a ResultBearing whose mapTo/ mapToBean selects a mapper and whose terminal method (one/findOne/first/findFirst/ list/stream) collects. The guide's introductory example shows every binding form in one block (IntroductionTest.java):
List<User> users = jdbi.withHandle(handle -> {
handle.execute("CREATE TABLE \"user\" (id INTEGER PRIMARY KEY, \"name\" VARCHAR)");
// Inline positional parameters
handle.execute("INSERT INTO \"user\" (id, \"name\") VALUES (?, ?)", 0, "Alice");
// Positional parameters
handle.createUpdate("INSERT INTO \"user\" (id, \"name\") VALUES (?, ?)")
.bind(0, 1) // 0-based parameter indexes
.bind(1, "Bob")
.execute();
// Named parameters
handle.createUpdate("INSERT INTO \"user\" (id, \"name\") VALUES (:id, :name)")
.bind("id", 2)
.bind("name", "Clarice")
.execute();
// Named parameters from bean properties
handle.createUpdate("INSERT INTO \"user\" (id, \"name\") VALUES (:id, :name)")
.bindBean(new User(3, "David"))
.execute();
// Easy mapping to any type
return handle.createQuery("SELECT * FROM \"user\" ORDER BY \"name\"")
.mapToBean(User.class)
.list();
});Binding scales from single values up to whole objects: bind(name, value), bindMap(map), bindBean(bean) (JavaBean getters), bindFields(obj) (public fields), bindMethods(obj) (parameterless methods), each optionally prefixed (bindBean("f", folder) → :f.id), and each resolving nested properties (:user.address.street). bindList("kinds", …) expands an Iterable into a comma-separated placeholder list for IN clauses — but, notably, it requires a template attribute (<kinds>), not a bind token, because the count of placeholders is a structural change rendered before binding.
API style 2: SQL Objects (the declarative DAO layer)
The SQL Object extension is "a declarative-style extension to the fluent-style, programmatic Core APIs" (index.adoc). You declare a public Java interface, annotate each method with the SQL to run, and Jdbi synthesizes the implementation at runtime. The package Javadoc frames it (sqlobject/package-info.java):
"The SQLObject API allows for declarative definition of interfaces which will handle the generation of statements and queries on your behalf when needed."
The method annotations are @SqlQuery ("Used to indicate that a method should execute a query." — SqlQuery.java), @SqlUpdate, @SqlBatch, @SqlCall, and @SqlScript. Method parameters become statement arguments; the return type drives the mapping. The same introductory UserDao in declarative form (IntroductionTest.java):
public interface UserDao {
@SqlUpdate("CREATE TABLE \"user\" (id INTEGER PRIMARY KEY, \"name\" VARCHAR)")
void createTable();
@SqlUpdate("INSERT INTO \"user\" (id, \"name\") VALUES (?, ?)")
void insertPositional(int id, String name);
@SqlUpdate("INSERT INTO \"user\" (id, \"name\") VALUES (:id, :name)")
void insertNamed(@Bind("id") int id, @Bind("name") String name);
@SqlUpdate("INSERT INTO \"user\" (id, \"name\") VALUES (:id, :name)")
void insertBean(@BindBean User user);
@SqlQuery("SELECT * FROM \"user\" ORDER BY \"name\"")
@RegisterBeanMapper(User.class)
List<User> listUsers();
}Binding is by annotation, mirroring the fluent binders one-for-one. @Bind "Binds the annotated argument as a named parameter, and as a positional parameter." (Bind.java); @BindBean "Binds the properties of a JavaBean to a SQL statement." (BindBean.java); and @BindFields, @BindMethods, @BindMap, @BindList complete the set. Compile with -parameters and even @Bind is optional — unannotated parameters bind to their own names. To use SQL Objects you install the plugin and attach the interface:
Jdbi jdbi = Jdbi.create("jdbc:h2:mem:test");
jdbi.installPlugin(new SqlObjectPlugin());
List<User> users = jdbi.withExtension(UserDao.class, dao -> {
dao.createTable();
dao.insertPositional(0, "Alice");
dao.insertBean(new User(3, "David"));
return dao.listUsers();
});Because SQL Objects run on the fluent core, any interface implicitly gets a getHandle() (via the SqlObject mixin) to "drop down" to the fluent API for anything the annotations don't cover (sqlobject/package-info.java) — the concrete realization of "the two styles can be mixed."
The one value-into-text path: template attributes
For the structural changes binding cannot express (table names, optional clauses, IN-list expansion), Jdbi has a separate template layer with <name> attributes set by define(...) (or @Define in SQL Objects). The default DefinedAttributeTemplateEngine replaces angle-bracket placeholders; StringTemplate 4, Commons Text, and Freemarker engines plug in for richer templating. This runs before the SQL parser, and the guide is unambiguous that it is the dangerous path (index.adoc):
"Unlike _argument binding, the rendering of attributes performed by TemplateEngines is not SQL-aware."_
with a pointed caution (index.adoc): "Query templating is a common attack vector! Always prefer binding parameters to static SQL over dynamic SQL when possible." The two layers compose left to right — templating renders attributes, then the parser binds arguments:
handle.createQuery("SELECT * FROM <TABLE> WHERE name = :n")
.define("TABLE", "Person") // structural: substituted into text
.bind("n", "MyName"); // value: bound out-of-bandThe design keeps values on the safe channel even inside dynamic SQL: @BindList/bindList render only the placeholder count into text and still bind each element as a parameter, and defineNamedBindings() exposes per-binding booleans so <if(a)>a = :a,<endif> conditional clauses are driven by template logic while the value stays bound.
Schema, migrations & code generation
Jdbi owns no schema, and this is a deliberate absence. There is no entity model that is the schema (no code-first), no schema file it treats as truth (no schema-first), and no introspection→codegen step (the jOOQ/sqlc move). You write CREATE TABLE/ALTER as ordinary SQL and run it through execute or a @SqlUpdate method like any other statement. There is no migration runner: the guide explicitly delegates it (index.adoc): "we recommend using a schema migration tool such as Flyway or Liquibase to maintain your database schemas." The column↔member mapping is by name, by convention, at runtime (below), so there is no declared schema to check a query against — a renamed column surfaces only when the statement runs. This is the same "you own the schema" stance as Dapper and Go database/sql.
Type mapping & result decoding
Result decoding is Jdbi's second core job, and it is a registry of composable mappers split along the same grain as JDBC's ResultSet access.
Two mapper kinds. A RowMapper<T> "Maps result set rows to objects." (RowMapper.java) — invoked once per row, given the whole ResultSet positioned at the current row. A ColumnMapper<T> "Maps result set columns to objects." (ColumnMapper.java) — maps a single cell, and composes inside row mappers (a reflection row mapper looks up a column mapper per field). Both are @FunctionalInterfaces, so simple cases are lambdas:
List<User> users = handle
.createQuery("SELECT id, name FROM user ORDER BY id ASC")
.map((rs, ctx) -> new User(rs.getInt("id"), rs.getString("name")))
.list();A registry keyed by type. Register a mapper for a type and thereafter mapTo(User.class) finds it: jdbi.registerRowMapper(User.class, …), or jdbi.registerColumnMapper(Money.class, …). A mapper class with an explicit mapped type can be registered without repeating the type (Jdbi reflects the generic signature). Column mappers ship out of the box for primitives and their boxes, String, Enum, BigDecimal, byte[], the java.net/java.sql/java.time families, UUID, and arrays/collections; last-registered wins, so any built-in is overridable. ColumnMapperFactory/RowMapperFactory produce mappers for generic or open-ended types (e.g. an Optional<T> mapper composed from the T mapper).
Reflection mappers for POJOs. Three no-boilerplate mappers derive the column↔member mapping from a class's shape:
ConstructorMapper— "assigns columns to constructor parameters by name" (index.adoc), using-parametersnames,@ColumnName, or@ConstructorProperties; the idiomatic choice for immutable types and records.BeanMapper— maps to JavaBean setters. Its Javadoc notes the constraints (BeanMapper.java): "This uses the JDK's built in bean mapping facilities, so it does not support nested properties. The mapped class must have a default constructor."FieldMapper— maps directly to fields.
In SQL Objects the same mappers attach by annotation on the method or interface — @RegisterRowMapper "Register a row mapper in the context of a SQL Object type or method." (RegisterRowMapper.java), plus @RegisterColumnMapper, @RegisterBeanMapper, @RegisterConstructorMapper, @RegisterFieldMapper and their factory variants.
Nullability is JDBC/Java nullability: a SQL NULL becomes a null reference or an empty Optional (bound and mapped around any supported type). It is not lifted into the type system the way sqlx/Kysely do — there is no described schema to derive it from — so a NULL in a non-nullable primitive slot is a runtime concern (with a coalesceNullPrimitivesToDefaults config that substitutes the JDBC default). Custom types plug in through ArgumentFactory (bind side) and ColumnMapper/RowMapper (read side); a Codec abstraction pairs the two, but there is no single composable codec algebra threading encode+decode the way skunk/hasql expose.
Effect model, transactions & error handling
This is the dimension the survey weights most heavily, and Jdbi sits at the blocking, exception-based end — the JVM mainstream, no effect value, no typed error channel.
Blocking JDBC. Every operation occupies the calling thread until the database answers: a Query runs on the terminal list()/one(), an Update on execute(). There is no effect value to compose and interpret at the edge — the contrast with doobie/skunk/Quill's ConnectionIO/IO/ZIO, and even with hasql's eager-but-monadic Session. A @Beta async facade exists — JdbiExecutor "wraps a Jdbi instance and an Executor to run callbacks asynchronously, returning CompletionStage results" (async/package-info.java) — but it is thread-pool offloading of the same blocking calls (the Javadoc suggests sizing the pool to the connection pool), not non-blocking I/O. Jdbi supports virtual threads, which is the ecosystem's actual answer to blocking-at-scale.
Transactions are a managed combinator over the Handle. The inTransaction/useTransaction methods (on Jdbi or Handle) open a transaction, pass the handle to a callback, and settle it automatically (Jdbi.java):
"The handle will be in a transaction when the callback is invoked, and that transaction will be committed if the callback finishes normally, or rolled back if the callback raises an exception."
Nesting is by transaction reuse, not true nesting (index.adoc): "if these methods are called while the handle is already in an open transaction, the existing transaction is reused (no nested transaction is created)" — an inner inTransaction joins the outer scope and defers commit to it. Savepoints are available on the unmanaged path: savepoint(name), releaseSavepoint(name), and rollbackToSavepoint(name), "not supported by all TransactionHandlers and requires support from the JDBC driver" (index.adoc). Isolation levels are a parameter to the transaction methods (an overload takes a TransactionIsolationLevel), and a SerializableTransactionRunner retries transactions that "abort due to serialization failures" transparently (index.adoc) — the closest Jdbi comes to Effect's isRetryable reasons, but wired as a swappable TransactionHandler rather than surfaced as typed error metadata. In SQL Objects, a default method annotated @Transaction runs within a transaction, and a Transactional mixin exposes inTransaction/useTransaction on the DAO itself.
Errors are unchecked exceptions. Jdbi does not model failure as a value. Every Jdbi exception descends from JdbiException, which extends RuntimeException (JdbiException.java) — so nothing is a checked SQLException and there is no typed error channel, no Either/Result, no reason union. A failed statement surfaces as UnableToExecuteStatementException (a StatementException), wrapping the driver's SQLException whose SQLState you inspect. To turn a specific SQLState into a domain exception, Jdbi offers a SqlExceptionHandler extension point — imperatively via addExceptionHandler, or declaratively via @RegisterSqlExceptionHandler — but the result is still a thrown exception, the mainstream JVM idiom, contrasted here with the effect systems' value-typed errors.
Ecosystem & maturity
Jdbi is a mature, widely-deployed JVM data-access library, founded by Brian McCallister and maintained by a small team (README.md); Jdbi 3 is the current line (the DBI/Jdbi lineage predates it by years). It is released under the permissive Apache-2.0 license (LICENSE) and keeps a deliberately small footprint — the core module "uses only slf4j and geantyref as hard dependencies" (index.adoc) — while a large ring of optional modules folds in library and vendor support. Distribution is via Maven Central, with a BOM for version alignment; the pinned tree tests against Java 17/21/25.
Backends: any JDBC database. Because Jdbi builds on JDBC, it works with Postgres, MySQL/ MariaDB, SQLite, Oracle, H2, and anything exposing a driver; there is no dialect layer because there is no SQL generation — dialect differences live in the SQL you write. Vendor plugins (jdbi3-postgres, jdbi3-mysql, jdbi3-sqlite, jdbi3-oracle12, jdbi3-postgis) add support for non-standard column types beyond the JDBC baseline.
A plugin/extension architecture carries the rest: SqlObjectPlugin (the declarative layer), Kotlin (jdbi3-kotlin, jdbi3-kotlin-sqlobject) with data-class mapping, Guava, JodaTime, Vavr, Guice, Spring, JPA-annotation, and JSON plugins (Jackson 2/3, Gson, Moshi), pluggable SQL-template engines (StringTemplate 4, Commons Text, Freemarker) and caches (Caffeine, Guava). Jdbi is a recipient of the Spotify FOSS 2023 Fund (README.md).
Strengths
- Minimal, honest abstraction. A
Handleis a JDBC connection; a statement is your SQL; a mapper is a function. No hidden queries, no flush, no lazy proxies — "no hidden magic." - Injection-safe by default. Values become real JDBC
PreparedStatementparameters on the out-of-band channel; the only value-into-text path is the explicitly-flagged template layer. - Two ergonomic API styles over one core. Fluent for ad-hoc/imperative code, declarative SQL Objects for a typed DAO layer — and they interoperate (
getHandle()), so you can escalate or drop down without leaving Jdbi. - Rich, composable mapping. Row/column mappers, a type registry, reflection mappers for constructors/beans/fields/records, factories for generics, and per-method annotation wiring.
- You keep full SQL control. CTEs, window functions, vendor extensions,
RETURNING— any SQL the database supports runs verbatim; nothing is hidden behind a builder. - Provider-agnostic + broad plugin ecosystem. Any JDBC backend; Kotlin/Guava/Jackson/Spring and vendor-type plugins extend it without bloating the core.
- Managed transactions with savepoints and serializable-retry. A real
inTransaction/useTransactioncombinator with commit/rollback semantics, plus savepoint and auto-retry handlers.
Weaknesses
- No compile-time SQL checking. SQL is opaque text (doubly so in SQL Object annotation strings); a bad column, type mismatch, or renamed property is a runtime failure — the price of the raw-string model.
- You own the schema and migrations. No code generation, no migration runner — delegated to Flyway/Liquibase.
- No change tracking, identity map, or unit of work. Updates mean writing the
UPDATEyourself; there is noSaveChanges/dirty-state detection (by design — it is not an ORM). - Blocking-only core. No effect value and no genuine non-blocking I/O; the async facade is
@Betaexecutor offloading, and scale-out leans on virtual threads. - Exception-based, unchecked errors. No typed/value error channel and no first-class retryability metadata; failure handling is
try/catchonSQLState(or aSqlExceptionHandlerthat still throws). - Mapping is by-name convention + reflection. Column↔member matching is stringly-typed and reflective; no declared, statically-checked mapping, and reflection has AOT/GraalVM friction.
- SQL-in-annotation strings. The declarative style embeds SQL in Java annotation literals — no IDE SQL support, awkward multi-line concatenation, and the SQL is checked only at runtime.
Key design decisions and trade-offs
| Decision | Rationale | Trade-off |
|---|---|---|
| Not an ORM — no session cache, change-tracking, or schema modelling | No hidden magic; predictable, understandable SQL; "you bring your own SQL, Jdbi executes it" | You hand-write all DML/DDL and updates; no dirty tracking, SaveChanges, identity map, or lazy loading |
| Raw SQL you write, no query builder or generation | Full SQL power; provider-agnostic (no dialect layer); SQL is a first-class language | No compile-time column/type checking; SQL portability and correctness are on you |
| Two APIs over one core (Fluent + SQL Objects), interoperable | Imperative ergonomics and a declarative DAO layer; drop-down via getHandle(), mixable | Two surfaces to learn; SQL Objects hide SQL in annotation strings with only runtime checking |
Out-of-band binding (:name/? → JDBC parameters) | Injection-safe by default; ergonomic bean/field/method/map binders | By-name, reflective binding; structural changes need the separate (SQL-unaware) template layer |
Template attributes (<name>/@Define) for structural SQL | Enables table names, IN-lists, conditional clauses that binding can't express | Rendering is not SQL-aware — "a common attack vector"; the escape hatch that re-exposes injection |
Mapper registry (RowMapper/ColumnMapper + reflection mappers) | Composable, type-keyed, overridable; no-boilerplate mapping for POJOs/records | Stringly-typed by-name matching + reflection (AOT friction); no statically-checked mapping |
Handle = wrapped JDBC connection, managed via callbacks | Explicit, short-lived resource; withHandle/inTransaction manage lifecycle & commit | Not thread-safe; leaks if opened unmanaged; no scoped-Resource value like the effect systems |
No connection pooling (delegated to the DataSource) | Stays a thin layer; compose with HikariCP/etc. | Pool sizing/lifecycle is the caller's problem |
| Blocking JDBC, exception-based unchecked errors | Familiar JVM model; virtual-thread friendly; managed transactions with savepoint & retry | No effect value, no non-blocking I/O, no typed/value error channel or isRetryable |
Sources
- jdbi/jdbi — GitHub repository · Developer Guide · Javadoc
README.md— "convenient, idiomatic access to relational databases in Java"; "built on top of JDBC"; Apache-2.0; founders; Spotify FOSS funddocs/src/adoc/index.adoc— "not an ORM" / "no session cache … change-tracking"; "does not hide SQL away"; two-API duality ("under the hood", "can be mixed"); binding vs templating & injection cautions; transactions (reuse, savepoints, serializable retry); Flyway/Liquibase delegation; slf4j+geantyref dependencies; module/plugin overview; 3.54.0 @ 2026-07-01docs/src/test/java/jdbi/doc/IntroductionTest.java— the fluent-vs-SQL-ObjectUserDaoexamplecore/.../Jdbi.java— entry-point Javadoc;withHandle/inTransactionsemanticscore/.../Handle.java— "wrapper around a JDBC Connection object";createQuerycore/.../statement/SqlStatement.java—bind/bindBeanbinding APIcore/.../mapper/RowMapper.java— "Maps result set rows to objects" ·ColumnMapper.java— "Maps result set columns to objects" ·mapper/reflect/BeanMapper.java— bean-mapping constraintscore/.../JdbiException.java—extends RuntimeException(unchecked errors) ·core/.../async/package-info.java—JdbiExecutorCompletionStagefacadesqlobject/.../package-info.java— "declarative definition of interfaces"; attach/onDemand lifecycles;@Transactionmixin ·sqlobject/.../statement/SqlQuery.java·sqlobject/.../customizer/Bind.java·BindBean.java·config/RegisterRowMapper.java- Shared vocabulary: concepts & vocabulary · the abstraction ladder · query construction models · statements, parameters & injection · connections, pools & sessions · schema, migrations & code generation · type mapping & result decoding · effects, transactions & error handling