Skip to content

✅ Mapping Java parameters to JDBC

SQL parameters are Java method parameters that appear as named markers in an SQL statement. Most parameters use one of these forms:

  • :name binds one Java parameter as one JDBC value
  • :customer.{names} expands record component names
  • :customer.{values} or :customer.* expands record, array, or list values
  • @JdbcConverterName("...") selects one named converter for one whole parameter

Use :name when one Java parameter should become one JDBC value. This is the normal choice for String, numbers, dates, enums, and small value objects.

@JdbcSelect("SELECT :name || ':' || :budget")
String bindOneValue(String name, int budget);

The marker name must match the Java method parameter name. ? placeholders are not supported.

For an enum, the default converter binds the enum name as a String.

enum PricingPlan {
FREE, BASIC, PRO, ENTERPRISE
}
@JdbcSelect("SELECT :plan")
String bindEnum(PricingPlan plan);

A record with exactly one component can be bound through that component. Use this for small value types such as IDs.

record CustomerId(long value) {
}
@JdbcSelect("SELECT :id")
long bindCustomerId(CustomerId id);

If the component is not directly JDBC-supported, it may use another converter. The conversion chain must end in a JDBC-supported value.

If the value object owns its JDBC representation, put an object converter method on the type.

record InvoiceId(long value) {
@JavaToJdbc
long toJdbc() {
return value;
}
}
@JdbcSelect("SELECT :id")
long bindInvoiceId(InvoiceId id);

If the conversion should stay outside the value object, or the type is not yours, use a static converter.

record ExternalId(String value) {
}
@JavaToJdbc
static String externalIdToJdbc(ExternalId id) {
return id.value();
}
@JdbcSelect("SELECT :id")
String bindExternalId(ExternalId id);

If many value objects share the same JDBC representation, define a converter for a shared interface or superclass. The Java-to-JDBC type hierarchy lookup can then use that converter for concrete subtypes.

interface RowId {
long value();
}
record AccountId(long value) implements RowId {
}
record OrderId(long value) implements RowId {
}
@JavaToJdbc
static long rowIdToJdbc(RowId id) {
return id.value();
}
@JdbcSelect("SELECT :id")
long bindAccountId(AccountId id);
@JdbcSelect("SELECT :id")
long bindOrderId(OrderId id);

This is useful for ID types such as AccountId, OrderId, or CustomerId that all wrap the same database column type.

Use record expansion when a record provides several SQL values.

record NewCustomer(String name,
Integer budge,
@JdbcName("pricing_plan") PricingPlan plan) {
}
@JdbcUpdate("INSERT INTO db_customers (:customer.{names}) VALUES (:customer.*)")
void insertCustomer(NewCustomer customer);

For the record in this example, the SQL marker expansion produces this SQL shape:

INSERT INTO db_customers (name, budge, pricing_plan) VALUES (?, ?, ?)

Record expansion supports these marker formats:

  • :customer.{names} expands component names in declaration order
  • :customer.{values} expands component values in declaration order
  • :customer.* is a short form for :customer.{values}

If a record component uses @JdbcName, :customer.{names} uses that SQL name.

Use :param.{values} or :param.* when an array or list should become several JDBC placeholders.

@JdbcSelect("SELECT x FROM (VALUES :ids.{values}) t(x)")
List<Long> findIds(List<Long> ids);

For a list with three values, the SQL marker expansion produces this SQL shape:

SELECT x FROM (VALUES ?, ?, ?) t(x)

This is useful for IN (...) clauses and other statements that need one SQL value per collection element. The normal :param marker binds the list as one JDBC value instead.

Use a named converter when one parameter needs a special binding strategy.

@JavaToJdbc("upper")
static String externalIdToUpperJdbc(ExternalId id) {
return id.value().toUpperCase();
}
@JdbcSelect("SELECT :id")
String bindExternalIdAsUpper(@JdbcConverterName("upper") ExternalId id);

@JdbcConverterName applies the converter to the whole parameter. Use it with the plain marker format, such as :id. It is invalid with :id.{values}, :id.{names}, and :id.*.

Use a statement converter for low-level binding

Section titled “Use a statement converter for low-level binding”

Use a statement converter only when returning one JDBC-supported value is not enough, or when binding needs direct access to the PreparedStatement.

record LowerText(String value) {
}
@JavaToJdbc
static void bindLowerText(PreparedStatement stmt, int index, LowerText text)
throws SQLException {
stmt.setString(index, text.value().toLowerCase());
}
@JdbcSelect("SELECT :text")
String bindLowerText(LowerText text);

The converter receives the PreparedStatement, the parameter index, and the Java value. It returns void and may throw SQLException.

Check the marker format. :ids binds the list as one JDBC value and therefore needs a converter for the whole list. Use :ids.{values} or :ids.* when each list element should become its own JDBC placeholder.

Use :customer.{names} only for record parameters. Use :customer.{values} or :customer.* for record values. If the record uses @JdbcConverterName, expansion is invalid because the named converter applies to the record as a whole.

This is expected. Named converters select a converter for the whole parameter. Expansion markers select parameter components or elements instead. Use either @JdbcConverterName with :param, or remove the named converter and expand with :param.{values} or :param.*.

Use an unnamed @JavaToJdbc converter for the normal binding strategy of a type. It can be an object method on the type, a static converter for the exact type, or a static converter for a shared interface or superclass. Use a named converter only for special cases. If neither exists, Kaumei JDBC can still create default converters for enums and single-component records.

For exact marker syntax, converter-name restrictions, and nullness rules, see parameter binding. For exact lookup order, scopes, hierarchy lookup, and fallback creation, see converter lookup.