Pyranid Logo

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();

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();

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, with snake_case being automatically mapped to camelCase. 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);
}};
Previous
Contributing