Core Concepts
Configuration
All data access in Pyranid is performed through a Database
instance. You can configure it by providing your own implementations of "hook" interfaces to a builder at construction time. Once the Database
is created, it's threadsafe and designed to be shared across your app.
By design, there is a 1:1 relationship between your Database
and its javax.sql.DataSource
. For larger systems, you might opt to have one instance that points to a writable primary and another that points to a load-balanced pool of read replicas.
Minimal Setup
This approach uses out-of-the-box Pyranid defaults. It gets you up and running quickly to kick the tires.
// Create a Database backed by a DataSource
DataSource dataSource = obtainDataSource();
Database database = Database.forDataSource(dataSource).build();
// Create a Database backed by a DataSource
DataSource dataSource = obtainDataSource();
Database database = Database.forDataSource(dataSource).build();
Customized Setup
This example shows all of the different "hook" interfaces you might use to customize behavior.
In production systems, at a minimum you will want to provide your own StatementLogger
to keep an eye on your SQL and its performance. If your app relies on Dependency Injection, you'll want to wire in your own InstanceProvider
.
// Useful if your JVM's default timezone doesn't match
// your Database's default timezone
ZoneId timeZone = ZoneId.of("UTC");
// Controls how Pyranid creates instances of objects
// that represent ResultSet rows
InstanceProvider instanceProvider = new DefaultInstanceProvider() {
@Override
@Nonnull
public <T> T provide(@Nonnull StatementContext<T> statementContext,
@Nonnull Class<T> instanceType) {
// You might have your DI framework vend regular object instances
return guiceInjector.getInstance(instanceType);
}
@Override
@Nonnull
public <T extends Record> T provideRecord(
@Nonnull StatementContext<T> statementContext,
@Nonnull Class<T> recordType,
@Nullable Object... initargs) {
// If you use Record types, customize their instantiation here.
// Default implementation will use the canonical constructor
return super.provideRecord(statementContext, recordType, initargs);
}
};
// Copies data from a ResultSet row to an instance of the specified type
ResultSetMapper resultSetMapper = new DefaultResultSetMapper(timeZone) {
@Nonnull
@Override
public <T> Optional<T> map(@Nonnull StatementContext<T> statementContext,
@Nonnull ResultSet resultSet,
@Nonnull Class<T> resultSetRowType,
@Nonnull InstanceProvider instanceProvider) {
// Customize mapping here if needed
return super.map(statementContext, resultSet,
resultSetRowType, instanceProvider);
}
};
// Binds parameters to a SQL PreparedStatement
PreparedStatementBinder preparedStatementBinder =
new DefaultPreparedStatementBinder(timeZone) {
@Override
public <T> void bind(@Nonnull StatementContext<T> statementContext,
@Nonnull PreparedStatement preparedStatement,
@Nonnull List<Object> parameters) {
// Customize parameter binding here if needed
super.bind(statementContext, preparedStatement, parameters);
}
};
// Optionally logs SQL statements
StatementLogger statementLogger = new StatementLogger() {
@Override
public void log(@Nonnull StatementLog statementLog) {
// Send to whatever output sink you'd like
out.println(statementLog);
}
};
Database customDatabase = Database.forDataSource(dataSource)
.timeZone(timeZone)
.instanceProvider(instanceProvider)
.resultSetMapper(resultSetMapper)
.preparedStatementBinder(preparedStatementBinder)
.statementLogger(statementLogger)
.build();
// Useful if your JVM's default timezone doesn't match
// your Database's default timezone
ZoneId timeZone = ZoneId.of("UTC");
// Controls how Pyranid creates instances of objects
// that represent ResultSet rows
InstanceProvider instanceProvider = new DefaultInstanceProvider() {
@Override
@Nonnull
public <T> T provide(@Nonnull StatementContext<T> statementContext,
@Nonnull Class<T> instanceType) {
// You might have your DI framework vend regular object instances
return guiceInjector.getInstance(instanceType);
}
@Override
@Nonnull
public <T extends Record> T provideRecord(
@Nonnull StatementContext<T> statementContext,
@Nonnull Class<T> recordType,
@Nullable Object... initargs) {
// If you use Record types, customize their instantiation here.
// Default implementation will use the canonical constructor
return super.provideRecord(statementContext, recordType, initargs);
}
};
// Copies data from a ResultSet row to an instance of the specified type
ResultSetMapper resultSetMapper = new DefaultResultSetMapper(timeZone) {
@Nonnull
@Override
public <T> Optional<T> map(@Nonnull StatementContext<T> statementContext,
@Nonnull ResultSet resultSet,
@Nonnull Class<T> resultSetRowType,
@Nonnull InstanceProvider instanceProvider) {
// Customize mapping here if needed
return super.map(statementContext, resultSet,
resultSetRowType, instanceProvider);
}
};
// Binds parameters to a SQL PreparedStatement
PreparedStatementBinder preparedStatementBinder =
new DefaultPreparedStatementBinder(timeZone) {
@Override
public <T> void bind(@Nonnull StatementContext<T> statementContext,
@Nonnull PreparedStatement preparedStatement,
@Nonnull List<Object> parameters) {
// Customize parameter binding here if needed
super.bind(statementContext, preparedStatement, parameters);
}
};
// Optionally logs SQL statements
StatementLogger statementLogger = new StatementLogger() {
@Override
public void log(@Nonnull StatementLog statementLog) {
// Send to whatever output sink you'd like
out.println(statementLog);
}
};
Database customDatabase = Database.forDataSource(dataSource)
.timeZone(timeZone)
.instanceProvider(instanceProvider)
.resultSetMapper(resultSetMapper)
.preparedStatementBinder(preparedStatementBinder)
.statementLogger(statementLogger)
.build();
Interfaces
InstanceProvider
- When results are returned from a query, Pyranid needs to create an instance of an object to hold data for each row in the resultset. The default implementation assumes you have either a Record instantiable via its canonical constructor or an Object instantiable via
Class<T>::getDeclaredConstructor(java.lang.Class...)
. In production systems, you might find it useful to have a Dependency Injection library like Google Guice vend these instances.
ResultSetMapper
- For each instance created by your
InstanceProvider
, data from the resultset needs to be mapped to it. By default, reflection is used to determine how to map DB column names to Java property names, withsnake_case
being automatically mapped tocamelCase
. More details are available in the ResultSet Mapping documentation.
PreparedStatementBinder
- Parameterized SQL statments are ultimately converted to
java.sql.PreparedStatement
and passed along to your JDBC driver for processing. This interface allows you to control per-statement how this mapping should be performed.
StatementLogger
- Every database operation has diagnostic information captured and made accessible via a
StatementLog
instance. You might want to write log slow queries to a logging system like Logback, send tracing information to New Relic or just write everything to stdout - it's up to you! More details are available in the Logging and Diagnostics documentation.
Addendum: Obtaining a DataSource
Pyranid works with any javax.sql.DataSource implementation. If you have the freedom to choose, HikariCP (application-level) and PgBouncer (external; Postgres-only) are good options.
// HikariCP
DataSource hikariDataSource = new HikariDataSource(new HikariConfig() {{
setJdbcUrl("jdbc:postgresql://localhost:5432/my-database");
setUsername("example");
setPassword("secret");
setConnectionInitSql("SET TIME ZONE 'UTC'");
}});
// PgBouncer (using Postgres' JDBC driver-provided DataSource impl)
DataSource pgBouncerDataSource = new PGSimpleDataSource() {{
setServerNames(new String[] {"localhost"});
setPortNumber(5432);
setDatabaseName("my-database");
setUser("example");
setPassword("secret");
setPreferQueryMode(PreferQueryMode.SIMPLE);
}};
// HikariCP
DataSource hikariDataSource = new HikariDataSource(new HikariConfig() {{
setJdbcUrl("jdbc:postgresql://localhost:5432/my-database");
setUsername("example");
setPassword("secret");
setConnectionInitSql("SET TIME ZONE 'UTC'");
}});
// PgBouncer (using Postgres' JDBC driver-provided DataSource impl)
DataSource pgBouncerDataSource = new PGSimpleDataSource() {{
setServerNames(new String[] {"localhost"});
setPortNumber(5432);
setDatabaseName("my-database");
setUser("example");
setPassword("secret");
setPreferQueryMode(PreferQueryMode.SIMPLE);
}};