In a previous post we learned how to add an extra DataSource
to our Ratpack application. At that time on the Ratpack Slack channel there was a discussion on this topic and Danny Hyun mentioned an idea by Dan Woods to use a Map
with DataSource
objects. So it easier to add more DataSource
and Sql
objects to the Ratpack registry. In this post we are going to take a look at a solution to achieve this.
We are going to use the HikariDataSource
, because it is fast and low on resources, in our example code. First we create a new class to hold the configuration for multiple datasources. The configuration is a Map
where the key is the name of the database and the value an HikariConfig
object. The key, the name of the database, is also used for creating the HikariDataSource
and Sql
objects. And the good thing is that Ratpack uses a Jackson ObjectMapper
to construct a configuration object and it understands Map
structures as well. In the ratpack.groovy
file at the end of this blog post we see how we can have a very clean configuration this way.
// File: src/main/groovy/mrhaki/ratpack/configuration/DataSourcesConfiguration.groovy package mrhaki.ratpack.configuration import com.zaxxer.hikari.HikariConfig class DataSourcesConfig { @Delegate private final Map<String, HikariConfig> configurations = [:] /** * Extra method to add a HikariConfig to the configurations. * Can be used for example in the configuration closure when * adding the DataSourcesModule to the Ratpack bindings. * * @param name Name of database. * @param config Configuration to connect to database. * @return This DataSourcesConfig object. */ DataSourcesConfig addHikariConfig(final String name, final HikariConfig config) { configurations.put(name, config) return this } }
Next we create a class that holds a Map
of HikariDataSource
objects for each database name and HikariConfig
object in the DataSourcesConfiguration
class:
// File: src/main/groovy/mrhaki/ratpack/configuration/DataSources.groovy package mrhaki.ratpack.configuration import com.zaxxer.hikari.HikariConfig import com.zaxxer.hikari.HikariDataSource class DataSources { @Delegate private final Map<String, HikariDataSource> dataSources = [:] DataSources(final DataSourcesConfig config) { // Create a new Map with HikariDataSource objects as // values and the database name as key. dataSources = config.collectEntries { String name, HikariConfig hikariConfig -> [(name): new HikariDataSource(hikariConfig)] } } }
Like with the default HikariModule
we also create a class that implements the ratpack.service.Service
interface, so we can close the datasources when Ratpack is stopped:
// File: src/main/groovy/mrhaki/ratpack/configuration/DataSourcesService.groovy package mrhaki.ratpack.configuration import com.zaxxer.hikari.HikariDataSource import ratpack.service.Service import ratpack.service.StopEvent class DataSourcesService implements Service { private final DataSources dataSources DataSourcesService(final DataSources dataSources) { this.dataSources = dataSources } DataSources getDataSources() { return dataSources } @Override void onStop(final StopEvent event) throws Exception { dataSources.each { String name, HikariDataSource dataSource -> dataSource.close() } } }
Let's put all this together in a ConfigurableModule
. In the module we use the @Provides
annotation to make the objects available in the Ratpack registry:
// File: src/main/groovy/mrhaki/ratpack/configuration/DataSourcesModule.groovy package mrhaki.ratpack.configuration import com.google.inject.Provides import com.google.inject.Singleton import ratpack.guice.ConfigurableModule class DataSourcesModule extends ConfigurableModule<DataSourcesConfig> { @Override protected void configure() { // Objects are provided with the @Provides annotation. } /** * Provide DataSourceService, so Ratpack can use it in the * Ratpack application lifecycle. * * @param config Configuration for datasources. * @return DataSourcesService to close datasources on application stop. */ @Provides @Singleton DataSourcesService dataSourcesServices(final DataSourcesConfig config) { final DataSources dataSources = new DataSources(config) new DataSourcesService(dataSources) } /** * DataSources has a Map with database name as key and * HikariDataSource as value. * * @param dataSourcesService DataSourcesService has already * an instance of DataSources. * @return Object that we can use to get a HikariDataSource by name. */ @Provides @Singleton DataSources dataSources(final DataSourcesService dataSourcesService) { dataSourcesService.dataSources } }
Finally we create another module that uses the DataSources
object to create a Map
with Sql
instances that can be retrieved by the database name. First the class Sqls
that holds the map of database names with a corresponding Sql
:
// File: src/main/groovy/mrhaki/ratpack/configuration/Sqls.groovy package mrhaki.ratpack.configuration import com.zaxxer.hikari.HikariDataSource import groovy.sql.Sql class Sqls { @Delegate private final Map<String, Sql> sqls Sqls(final DataSources dataSources) { // Create new Map with database name as key // and Sql instance as value. sqls = dataSources.collectEntries { String name, HikariDataSource dataSource -> [(name): new Sql(dataSource)] } } }
And the module to make an Sqls
object available in the registry:
// File: src/main/groovy/mrhaki/ratpack/configuration/SqlsModule.groovy package mrhaki.ratpack.configuration import com.google.inject.AbstractModule import com.google.inject.Provides import com.google.inject.Singleton class SqlsModule extends AbstractModule { @Override protected void configure() { // We use @Provides annotation. } /** * Create class with Map containing database names * with the corresponding Groovy Sql instance. * * @param dataSources Datasources to create Sql objects for. * @return Object with reference to Sql instances * identified by database name. */ @Provides @Singleton Sqls sqls(final DataSources dataSources) { new Sqls(dataSources) } }
Finally we change the CustomerSql
class we created in the previous blog post. This time we pass the Sqls
object in the constructor to get both Sql
instances:
// File: src/main/groovy/mrhaki/ratpack/CustomerSql.groovy package mrhaki.ratpack import groovy.sql.GroovyRowResult import groovy.sql.Sql import mrhaki.ratpack.configuration.Sqls import ratpack.exec.Blocking import ratpack.exec.Promise import javax.inject.Inject class CustomerSql implements CustomerRepository { private final Sql customerSql private final Sql locationSql /** * We are using constructor injection to * get both Sql instances. * * @param Map with Sql instances. */ @Inject CustomerSql(final Sqls sqls) { customerSql = sqls.customer locationSql = sqls.location } /** * Get customer information with address. We use * both databases to find the information * for a customer with the given id. * * @param id Identifier of the customer we are looking for. * @return Customer with filled properties. */ @Override Promise<Customer> getCustomer(final String id) { Blocking.get { final String findCustomerQuery = '''\ SELECT ID, NAME, POSTALCODE, HOUSENUMBER FROM CUSTOMER WHERE ID = :customerId ''' customerSql.firstRow(findCustomerQuery, customerId: id) }.map { customerRow -> new Customer( id: customerRow.id, name: customerRow.name, address: new Address( postalCode: customerRow.postalcode, houseNumber: customerRow.housenumber)) }.blockingMap { customer -> final String findAddressQuery = '''\ SELECT STREET, CITY FROM address WHERE POSTALCODE = :postalCode AND HOUSENUMBER = :houseNumber ''' final GroovyRowResult addressRow = locationSql.firstRow( findAddressQuery, postalCode: customer.address.postalCode, houseNumber: customer.address.houseNumber) customer.address.street = addressRow.street customer.address.city = addressRow.city customer } } }
In ratpack.groovy
we now use our new modules to work with multiple datasources. Notice that the mapping from properties to a Map
with HikariConfig
objects in DataSourcesConfiguration
works out of the box:
// File: src/ratpack/ratpack.groovy import mrhaki.ratpack.CustomerHandler import mrhaki.ratpack.CustomerRepository import mrhaki.ratpack.CustomerSql import mrhaki.ratpack.configuration.DataSourcesConfig import mrhaki.ratpack.configuration.DataSourcesModule import mrhaki.ratpack.configuration.SqlsModule import static ratpack.groovy.Groovy.ratpack ratpack { serverConfig { props 'dataSources.customer.jdbcUrl': 'jdbc:postgresql://192.168.99.100:5432/customer' props 'dataSources.customer.username': 'postgres' props 'dataSources.customer.password': 'secret' props 'dataSources.location.jdbcUrl': 'jdbc:mysql://192.168.99.100:3306/location?serverTimezone=UTC&useSSL=false' props 'dataSources.location.username': 'root' props 'dataSources.location.password': 'secret' } bindings { moduleConfig(DataSourcesModule, serverConfig.get('/dataSources', DataSourcesConfig)) module(SqlsModule) // Alternative way to configure DataSourcesModule: //module(DataSourcesModule) { DataSourcesConfig config -> // config.addHikariConfig('customer', serverConfig.get('/dataSources/customer', HikariConfig)) // config.addHikariConfig('location', serverConfig.get('/dataSources/location', HikariConfig)) //} // CustomerSql uses both Sql objects. bind(CustomerRepository, CustomerSql) } handlers { get('customer/:customerId', new CustomerHandler()) } }
Written with Ratpack 1.3.3.