Ratpacked: Revisited Using Multiple DataSources
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 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 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 {
@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 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 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.