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. Otherwise, Java names are converted to lowercase JDBC names, for example pricingPlan becomes pricing_plan and URLValue becomes url_value.

:customer.{names} writes names into the SQL text. Those names come from compile-time metadata and are validated as simple or qualified SQL name fragments. @JdbcName is a name override, not a place for quoted identifiers or SQL expressions.

Use :param.{values} or :param.* when an array or list should become several JDBC placeholders. Use this expansion in @JdbcSelect and normal @JdbcUpdate methods. Do not use it in a batch update method.

@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. Empty arrays and empty lists are rejected because they would create invalid SQL. Large expansions are guarded by the placeholder limits configured on @JdbcConfig.

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.