Core Concepts
Parameter Binding
When you execute a SQL statement, Pyranid will convert it to a java.sql.PreparedStatement
and ask your PreparedStatementBinder
to provide values for any ?
parameter placeholders. Use of java.sql.PreparedStatement
is important: among other things, it prevents SQL injection, permits your DBMS to re-use execution plans for performance, and enables typesafe parameter binding.
@FunctionalInterface
public interface PreparedStatementBinder {
<T> void bindParameter(
@Nonnull StatementContext<T> statementContext,
@Nonnull PreparedStatement preparedStatement,
@Nonnull Integer parameterIndex,
@Nonnull Object parameter
) throws SQLException;
}
@FunctionalInterface
public interface PreparedStatementBinder {
<T> void bindParameter(
@Nonnull StatementContext<T> statementContext,
@Nonnull PreparedStatement preparedStatement,
@Nonnull Integer parameterIndex,
@Nonnull Object parameter
) throws SQLException;
}
The out-of-the-box implementation supports binding common JDK types and generally "just works" as you would expect.
For example:
UUID departmentId = ...;
Long accountId = ...;
List<Employee> = database.queryForList("""
SELECT *
FROM employee
WHERE department_id=?
""", Employee.class, departmentId);
database.execute("""
INSERT INTO account_award (
account_id,
award_type
) VALUES (?,?)
""", accountId, AwardType.BIG);
UUID departmentId = ...;
Long accountId = ...;
List<Employee> = database.queryForList("""
SELECT *
FROM employee
WHERE department_id=?
""", Employee.class, departmentId);
database.execute("""
INSERT INTO account_award (
account_id,
award_type
) VALUES (?,?)
""", accountId, AwardType.BIG);
If you need to customize binding behavior, you might bring your own list of CustomParameterBinder
...
// See "Custom Parameters" section below for details
PreparedStatementBinder preparedStatementBinder =
PreparedStatementBinder.withCustomParameterBinders(List.of(...));
Database database = Database.withDataSource(dataSource)
.preparedStatementBinder(preparedStatementBinder)
.build();
// See "Custom Parameters" section below for details
PreparedStatementBinder preparedStatementBinder =
PreparedStatementBinder.withCustomParameterBinders(List.of(...));
Database database = Database.withDataSource(dataSource)
.preparedStatementBinder(preparedStatementBinder)
.build();
...or you may choose to directly implement the PreparedStatementBinder
interface for fine-grained control:
PreparedStatementBinder preparedStatementBinder =
new PreparedStatementBinder() {
@Override
public <T> void bindParameter(
@Nonnull StatementContext<T> statementContext,
@Nonnull PreparedStatement preparedStatement,
@Nonnull Integer parameterIndex,
@Nonnull Object parameter
) throws SQLException {
// Do your binding here
}
};
Database database = Database.withDataSource(dataSource)
.preparedStatementBinder(preparedStatementBinder)
.build();
PreparedStatementBinder preparedStatementBinder =
new PreparedStatementBinder() {
@Override
public <T> void bindParameter(
@Nonnull StatementContext<T> statementContext,
@Nonnull PreparedStatement preparedStatement,
@Nonnull Integer parameterIndex,
@Nonnull Object parameter
) throws SQLException {
// Do your binding here
}
};
Database database = Database.withDataSource(dataSource)
.preparedStatementBinder(preparedStatementBinder)
.build();
Supported Primitives
Byte
Short
Integer
Long
Float
Double
Boolean
Character
String
byte[]
Supported JDK Types
Enum<E>
UUID
BigDecimal
BigInteger
Date
Instant
LocalDate
LocalTime
LocalDateTime
OffsetTime
OffsetDateTime
java.sql.Timestamp
java.sql.Date
java.sql.Time
ZoneId
TimeZone
Locale
(IETF BCP 47 "language tag" format)Currency
Special Parameters
Special support is provided for JSON/JSONB, vector, and SQL ARRAY parameters.
JSON/JSONB
Useful for storing "stringified" JSON data by taking advantage of the DBMS' native JSON storage facilities, if available (e.g. PostgreSQL's JSONB
type).
Supported methods:
You might create a JSONB storage table...
CREATE TABLE example (
example_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
data JSONB NOT NULL
);
CREATE TABLE example (
example_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
data JSONB NOT NULL
);
...and write JSON to it:
String json = "{\"testing\": 123}";
database.execute("INSERT INTO example (data) VALUES (?)",
Parameters.json(json)
);
String json = "{\"testing\": 123}";
database.execute("INSERT INTO example (data) VALUES (?)",
Parameters.json(json)
);
By default, Pyranid will use your database's binary JSON format if supported and fall back to a text representation otherwise.
If you want to force text storage (e.g. if whitespace is important), specify a binding preference like this:
database.execute("INSERT INTO example (data) VALUES (?)",
Parameters.json(json, BindingPreference.TEXT)
);
database.execute("INSERT INTO example (data) VALUES (?)",
Parameters.json(json, BindingPreference.TEXT)
);
Vector
Useful for storing pgvector
data, often used for Artificial Intelligence tasks. Currently only supported for PostgreSQL.
Supported methods:
Parameters::vectorOfDoubles(double[])
Parameters::vectorOfDoubles(List<Double>)
Parameters::vectorOfFloats(float[])
Parameters::vectorOfFloats(List<Float>)
Parameters::vectorOfBigDecimals(List<BigDecimal>)
You might create a vector storage table...
CREATE TABLE vector_embedding (
vector_embedding_id UUID DEFAULT uuid_generate_v4() PRIMARY KEY,
embedding VECTOR(1536) NOT NULL,
content TEXT NOT NULL
);
CREATE TABLE vector_embedding (
vector_embedding_id UUID DEFAULT uuid_generate_v4() PRIMARY KEY,
embedding VECTOR(1536) NOT NULL,
content TEXT NOT NULL
);
...and write vector data to it:
double[] embedding = ...;
String content = "...";
database.execute("INSERT INTO vector_embedding (embedding, content) VALUES (?,?)",
Parameters.vectorOfDoubles(embedding), content
);
double[] embedding = ...;
String content = "...";
database.execute("INSERT INTO vector_embedding (embedding, content) VALUES (?,?)",
Parameters.vectorOfDoubles(embedding), content
);
SQL ARRAY
Single-dimension array binding is supported out-of-the-box.
Supported methods:
You might create a table with some array columns...
CREATE TABLE product (
product_id UUID DEFAULT uuid_generate_v4() PRIMARY KEY,
name TEXT NOT NULL,
vendor_flags INTEGER[] NOT NULL,
tags VARCHAR[]
);
CREATE TABLE product (
product_id UUID DEFAULT uuid_generate_v4() PRIMARY KEY,
name TEXT NOT NULL,
vendor_flags INTEGER[] NOT NULL,
tags VARCHAR[]
);
...and write array data to it:
String name = "...";
int[] vendorFlags = { 1, 2, 3 };
List<String> tags = List.of("alpha", "beta");
database.execute("""
INSERT INTO product (
name,
vendor_flags,
tags
) VALUES (?,?,?)
""",
name,
Parameters.arrayOf("INTEGER", vendorFlags),
Parameters.arrayOf("VARCHAR", tags)
);
String name = "...";
int[] vendorFlags = { 1, 2, 3 };
List<String> tags = List.of("alpha", "beta");
database.execute("""
INSERT INTO product (
name,
vendor_flags,
tags
) VALUES (?,?,?)
""",
name,
Parameters.arrayOf("INTEGER", vendorFlags),
Parameters.arrayOf("VARCHAR", tags)
);
If you need support for multidimensional array binding, implement a CustomParameterBinder
as outlined below.
Custom Parameters
You may register instances of CustomParameterBinder
to bind application-specific types to java.sql.PreparedStatement
however you like.
This allows you to use your objects as-is with Pyranid instead of sprinkling "convert this object to database format" code throughout your system.
Arbitrary Types
Let's define a simple type.
class HexColor {
int r, g, b;
HexColor(int r, int g, int b) {
this.r = r; this.g = g; this.b = b;
}
String toHexString() {
return String.format("#%02X%02X%02X", r, g, b);
}
static HexColor fromHexString(String s) {
int r = Integer.parseInt(s.substring(1, 3), 16);
int g = Integer.parseInt(s.substring(3, 5), 16);
int b = Integer.parseInt(s.substring(5, 7), 16);
return new HexColor(r, g, b);
}
}
class HexColor {
int r, g, b;
HexColor(int r, int g, int b) {
this.r = r; this.g = g; this.b = b;
}
String toHexString() {
return String.format("#%02X%02X%02X", r, g, b);
}
static HexColor fromHexString(String s) {
int r = Integer.parseInt(s.substring(1, 3), 16);
int g = Integer.parseInt(s.substring(3, 5), 16);
int b = Integer.parseInt(s.substring(5, 7), 16);
return new HexColor(r, g, b);
}
}
Then, we'll register a CustomParameterBinder
to handle binding it:
PreparedStatementBinder preparedStatementBinder =
PreparedStatementBinder.withCustomParameterBinders(List.of(
new CustomParameterBinder() {
@Nonnull
@Override
public Boolean appliesTo(@Nonnull TargetType targetType) {
return targetType.matchesClass(HexColor.class);
}
@Nonnull
@Override
public BindingResult bind(
@Nonnull StatementContext<?> statementContext,
@Nonnull PreparedStatement preparedStatement,
@Nonnull Integer parameterIndex,
@Nonnull Object parameter
) throws SQLException {
HexColor hexColor = (HexColor) parameter;
// Bind to the PreparedStatement as a value like "#6a5acd"
preparedStatement.setString(parameterIndex, hexColor.toHexString());
// Or return BindingResult.fallback() to indicate "I don't want to do custom binding"
// and Pyranid will fall back to the registered PreparedStatementBinder's binding behavior
return BindingResult.handled();
}
}
));
Database database = Database.withDataSource(dataSource)
.preparedStatementBinder(preparedStatementBinder)
.build();
PreparedStatementBinder preparedStatementBinder =
PreparedStatementBinder.withCustomParameterBinders(List.of(
new CustomParameterBinder() {
@Nonnull
@Override
public Boolean appliesTo(@Nonnull TargetType targetType) {
return targetType.matchesClass(HexColor.class);
}
@Nonnull
@Override
public BindingResult bind(
@Nonnull StatementContext<?> statementContext,
@Nonnull PreparedStatement preparedStatement,
@Nonnull Integer parameterIndex,
@Nonnull Object parameter
) throws SQLException {
HexColor hexColor = (HexColor) parameter;
// Bind to the PreparedStatement as a value like "#6a5acd"
preparedStatement.setString(parameterIndex, hexColor.toHexString());
// Or return BindingResult.fallback() to indicate "I don't want to do custom binding"
// and Pyranid will fall back to the registered PreparedStatementBinder's binding behavior
return BindingResult.handled();
}
}
));
Database database = Database.withDataSource(dataSource)
.preparedStatementBinder(preparedStatementBinder)
.build();
With the custom binder in place, your application code might look like this:
// Given a reference to a hex color...
UUID themeId = ...;
HexColor backgroundColor = HexColor.fromHexString("#6a5acd");
// ...we use the reference as-is and Pyranid will apply the custom binder
database.execute("""
UPDATE theme
SET background_color=?
WHERE theme_id=?
""", backgroundColor, themeId);
// Given a reference to a hex color...
UUID themeId = ...;
HexColor backgroundColor = HexColor.fromHexString("#6a5acd");
// ...we use the reference as-is and Pyranid will apply the custom binder
database.execute("""
UPDATE theme
SET background_color=?
WHERE theme_id=?
""", backgroundColor, themeId);
Standard Collections
Runtime binding of generic types is made difficult by type erasure. For convenience, Pyranid offers special parameters that perform type capture for standard List<E>
, Set<E>)
, and Map<K,V>)
types:
Parameters::listOf(Class<E>, List<E>)
Parameters::setOf(Class<E>, Set<E>)
Parameters::mapOf(Class<K>, Class<V>, Map<K,V>)
This makes it easy to create custom binders for common scenarios.
For example, this code...
List<UUID> ids = List.of(
UUID.fromString("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"),
UUID.fromString("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb")
);
database.execute("INSERT INTO t(v) VALUES (?)", Parameters.listOf(UUID.class, ids));
List<UUID> ids = List.of(
UUID.fromString("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"),
UUID.fromString("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb")
);
database.execute("INSERT INTO t(v) VALUES (?)", Parameters.listOf(UUID.class, ids));
...would be handled by this custom binder, because targetType.matchesParameterizedType(List.class, UUID.class)
returns true
thanks to runtime type capturing:
PreparedStatementBinder preparedStatementBinder =
PreparedStatementBinder.withCustomParameterBinders(List.of(
new CustomParameterBinder() {
@Nonnull
@Override
public Boolean appliesTo(@Nonnull TargetType targetType) {
// For Parameters::mapOf(Class<K>, Class<V>, Map<K,V>), you'd say:
// matchesParameterizedType(Map.class, MyKey.class, MyValue.class)
return targetType.matchesParameterizedType(List.class, UUID.class);
}
@Nonnull
@Override
public BindingResult bind(
@Nonnull StatementContext<?> statementContext,
@Nonnull PreparedStatement preparedStatement,
@Nonnull Integer parameterIndex,
@Nonnull Object parameter
) throws SQLException {
// Convert UUIDs to a comma-delimited string, or null for the empty list
List<UUID> uuids = (List<UUID>) param;
String uuidsAsString = uuids.isEmpty()
? null
: uuids.stream().map(Object::toString).collect(Collectors.joining(","));
// Bind to the PreparedStatement
preparedStatement.setString(parameterIndex, uuidsAsString);
return BindingResult.handled();
}
}
));
PreparedStatementBinder preparedStatementBinder =
PreparedStatementBinder.withCustomParameterBinders(List.of(
new CustomParameterBinder() {
@Nonnull
@Override
public Boolean appliesTo(@Nonnull TargetType targetType) {
// For Parameters::mapOf(Class<K>, Class<V>, Map<K,V>), you'd say:
// matchesParameterizedType(Map.class, MyKey.class, MyValue.class)
return targetType.matchesParameterizedType(List.class, UUID.class);
}
@Nonnull
@Override
public BindingResult bind(
@Nonnull StatementContext<?> statementContext,
@Nonnull PreparedStatement preparedStatement,
@Nonnull Integer parameterIndex,
@Nonnull Object parameter
) throws SQLException {
// Convert UUIDs to a comma-delimited string, or null for the empty list
List<UUID> uuids = (List<UUID>) param;
String uuidsAsString = uuids.isEmpty()
? null
: uuids.stream().map(Object::toString).collect(Collectors.joining(","));
// Bind to the PreparedStatement
preparedStatement.setString(parameterIndex, uuidsAsString);
return BindingResult.handled();
}
}
));
Heads Up!
If you use Parameters::listOf(Class<E>, List<E>)
, Parameters::setOf(Class<E>, Set<E>)
, or Parameters::mapOf(Class<K>, Class<V>, Map<K,V>)
, you must define a corresponding CustomParameterBinder
to handle them. These special parameter types do not automatically work out-of-the-box because Pyranid cannot reliably guess how you intend to bind them.
Pyranid will detect this missing-binder scenario and throw an exception to indicate programmer error.