Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Spring or Hibernate multi-tenancy for multi-database Grails app

Grails has a multi-tenancy plugin for a single database and a multi-tenancy plugin for multi-databases, but the one for multi-databases is no longer supported/maintained. Is there some way I can instead use Spring or Hibernate themselves for a multi-tenancy multi-database Grails application?

like image 425
Daniel Avatar asked Dec 15 '22 14:12

Daniel


2 Answers

You can use Hibernate multitenancy described here http://docs.jboss.org/hibernate/orm/4.3/devguide/en-US/html/ch16.html

OR

You can also consider Spring's AbstractRoutingDataSource

The general idea is that a routing DataSource acts as an intermediary - while the ‘real’ DataSource can be determined dynamically at runtime based upon a lookup key.

https://spring.io/blog/2007/01/23/dynamic-datasource-routing/

You can find a newer post, giving an exemplary use with hibernate, the gist of the solution you can find in the following two snippets

public class MyRoutingDataSource extends AbstractRoutingDataSource{
    @Override
    protected Object determineCurrentLookupKey() {
        String language = LocaleContextHolder.getLocale().getLanguage();
        System.out.println("Language obtained: "+ language);
        return language;
    }
}

the return value will be used as a discriminator for a datasource, the following configuration sets the mapping

<bean id="dataSource" class="com.howtodoinjava.controller.MyRoutingDataSource">
   <property name="targetDataSources">
      <map key-type="java.lang.String">
         <entry key="en" value-ref="concreteDataSourceOne"/>
         <entry key="es" value-ref="concreteDataSourceTwo"/>
      </map>
   </property>
</bean>
like image 85
Master Slave Avatar answered Mar 22 '23 22:03

Master Slave


In our case we use LocalContainerEntityManagerFactoryBean where we create a multiTenantMySQLProvider.

 <bean class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean" id="entityManagerFactory">
    <property name="dataSource" ref="dataSource"/>
    <property name="jpaVendorAdapter">
        <bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter" />
    </property>
    <property name="packagesToScan" value="domain"/>
    <property name="jpaPropertyMap">
        <map>
            <entry key="hibernate.dialect" value="org.hibernate.dialect.MySQL5Dialect" />
            <entry key="javax.persistence.jdbc.driver" value="org.mariadb.jdbc.Driver" />
            <entry key="hibernate.show_sql" value="false" />
            <entry key="hibernate.multiTenancy" value="SCHEMA" />
            <entry key="hibernate.multi_tenant_connection_provider" value-ref="mySQLMultiTenantConnectionProvider" />
            <entry key="hibernate.tenant_identifier_resolver" value-ref="tenantIdentifierResolver" />
        </map>
    </property>
</bean>




   <bean id="tenantService"
      class="multitenancy.service.impl.TenantServiceImpl">
    <property name="defaultTenantId" value="${multitenancy.defaultTenantId}" />
    <property name="ldapTemplate" ref="ldapTemplate" />
</bean>

<bean id="connectionProvider"
      class="multitenancy.hibernate.ConnectionProviderImpl"  lazy-init="false">
    <property name="dataSource" ref="dataSource" />
</bean>

<bean id="mySQLMultiTenantConnectionProvider"
      class="multitenancy.hibernate.MySQLMultiTenantConnectionProviderImpl" lazy-init="false">
    <property name="connectionProvider" ref="connectionProvider" />
    <property name="tenantIdentifierForAny" value="${multitenancy.tenantIdentifierForAny}" />
    <property name="schemaPrefix" value="${multitenancy.schemaPrefix}" />
</bean>

<bean id="tenantIdentifierResolver"
      class="multitenancy.hibernate.TenantIdentifierResolverImpl" lazy-init="false">
    <property name="tenantService" ref="tenantService" />
</bean>

<bean id="tenantIdentifierSchedulerResolver"
      class="security.impl.TenantIdentifierSchedulerResolverImpl" lazy-init="false">
    <property name="ldapTemplate" ref="ldapTemplate" />
</bean>

And here the implementation of the MySQLMultiTenantConnectionProviderImpl

public class MySQLMultiTenantConnectionProviderImpl implements MultiTenantConnectionProvider, ServiceRegistryAwareService, Stoppable {


private static final Logger LOGGER = LoggerFactory.getLogger(MySQLMultiTenantConnectionProviderImpl.class);

@Setter
private ConnectionProvider connectionProvider;

@Setter
private String tenantIdentifierForAny;

@Setter
private String schemaPrefix;

@Override
public Connection getAnyConnection() throws SQLException {
    return connectionProvider.getConnection();
}

@Override
public void releaseAnyConnection(Connection connection) throws SQLException {
    connectionProvider.closeConnection( connection );
}

@Override
public Connection getConnection(String tenantIdentifier) throws SQLException {
    final Connection connection = getAnyConnection();
    String schema = schemaPrefix + tenantIdentifier;
    try {
        LOGGER.debug("setting schema in DB Connection : {}"  , schema);
        connection.createStatement().execute( "USE " + schema );
    }
    catch ( SQLException e ) {
        throw new HibernateException(
           "Could not alter JDBC connection to specified schema [" + schema + "]", e
        );
    }
    return connection;
}

@Override
public void releaseConnection(String tenantIdentifier, Connection connection) throws SQLException {
    try {
        connection.createStatement().execute( "USE " + tenantIdentifierForAny );
    }
    catch ( SQLException e ) {
        LOGGER.error(" error on releaseConnection. The connection will be not closed. SQLException : {}" , e);
        // on error, throw an exception to make sure the connection is not returned to the pool.
        throw new HibernateException(
            "Could not alter JDBC connection to specified schema [" + tenantIdentifier + "]", e
        );
    }
    // I follow the hibernate recommendation and we don't return the connetion to the pool.
    connectionProvider.closeConnection( connection );
}

@Override
public boolean supportsAggressiveRelease() {
    return true;
}

@Override
public void stop() {

}

@Override
public boolean isUnwrappableAs(Class unwrapType) {
    return ConnectionProvider.class.equals( unwrapType ) ||
            MultiTenantConnectionProvider.class.equals( unwrapType ) ||
            AbstractMultiTenantConnectionProvider.class.isAssignableFrom( unwrapType );
}

@Override
public <T> T unwrap(Class<T> unwrapType) {
    if ( isUnwrappableAs( unwrapType ) ) {
        return (T) this;
    }
    throw new UnknownUnwrapTypeException( unwrapType );

}

@Override
public void injectServices(ServiceRegistryImplementor serviceRegistry) {


}

}

like image 38
paul Avatar answered Mar 22 '23 22:03

paul