Loading...

June 13, 2016

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<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.