Crear datasource dinámico con Spring

Crear datasource dinámico con Spring


Vaya, hace tiempo ya desde mi último post. Estos últimos meses he andado un poco liado entre vida familiar y trabajo y ya iba siendo hora de escribir algo. Para darle un poco más de vida a esto trataré de escribir sobre cosas que hago diaríamente además de las que hago por hobby.

Hoy voy a hablar un poco del routing datasource de Spring el cual nos permite usar multiples bases de datos desde un unico datasource. Esto que a priori no parece muy genial se vuelve tremendamente utíl en cuanto lo unimos a sesiones de usuario, asi que ya podeis imaginaros sus aplicaciones para cambiar de entornos tan rápido como queramos.

Para empezar nos creamos una clase que herede AbstractRoutingDataSource, sobre escribir determineCurrentLookupKey. determineCurrentLookupKey debe retornar un valor que determinara cual es el datasource a usar:

  
public class RoutingDataSourceImpl extends AbstractRoutingDataSource {  
    @Override
    protected Object determineCurrentLookupKey() {
        return "ds1";
    }
}
Una vez hecho lo configuramos nuestro applicationContext.xml para ello en este ejemplo he agregado:
  • Una referencia a un datasource haciendo uso de JNDI.
  • Nuestro routing datasource y sus propiedades:
    • targetDataSources, la cual contiene un map con nuestros datasources y sus claves asociadas.
    • defaultTargetDataSource, nuestro datasource por defecto en caso de no disponer de una clave valida.
En este ejemplo incluyo un sqlMapClient de IBatis

<bean id="dataSource1" class="org.springframework.jndi.JndiObjectFactoryBean">
    <property name="jndiName" value="java:comp/env/jdbc/master/dataSource1" />
</bean>

<bean id="routingDataSource" class="com.nc.datasource.RoutingDataSourceImpl">
    <property name="targetDataSources">
        <map>
            <entry key="ds1" value-ref="dataSource1" />
        </map>
        </property>
    <property name="defaultTargetDataSource" ref="dataSource1" />
</bean>

<bean id="sqlMapClient" class="org.springframework.orm.ibatis.SqlMapClientFactoryBean">
    <property name="configLocation" value="classpath:com/nc/ibatis/SQLMapConfig.xml" />
    <property name="dataSource" ref="routingDataSource" />
</bean>
El funcionamiento es sencillo, cada vez que hacemos uso del routingDataSourceImpl el método determineCurrentLookupKey es invocado, retornando la clave del datasource que será usado, y en caso de que determineCurrentLookupKey no retorne una clave valida se usara la configurada en defaultTargetDataSource. En este punto ya estamos en disposición de dinamizar un poco más nuestros datasources ¿por qué no realizar el cambio en función a la sesión del usuario?. Para ello basta con agregar un bean de sesión en nuestro applicationContext.xml:
<bean id="user" class="com.nc.User" scope="session">  
    <aop:scoped-proxy />
</bean>
Nuestro bean podría tener este aspecto:
class User implements Serializable {

    private Long id;
    private String name;
    private String environment;

    public User(){

    }

    // ...
    // getters and setters
    // ...
}
Ya solo falta agregar la inyección de nuestro bean de sesión a nuestro routingDatasourceImpl:
public class RoutingDataSourceImpl extends AbstractRoutingDataSource {

     @Autowired
    private User user;

    public void setUser(User user) {
        this.user = user;
    }

    @Override
    protected Object determineCurrentLookupKey() {
        return user.getEnvironment();
    }
}
Agregamos un par de datasources:
  
<bean id="dataSource1" class="org.springframework.jndi.JndiObjectFactoryBean">
    <property name="jndiName" value="java:comp/env/jdbc/master/dataSource1" />
</bean>
<bean id="dataSource2" class="org.springframework.jndi.JndiObjectFactoryBean">
    <property name="jndiName" value="java:comp/env/jdbc/master/dataSource2" />
</bean>
<bean id="dataSource3" class="org.springframework.jndi.JndiObjectFactoryBean">
    <property name="jndiName" value="java:comp/env/jdbc/master/dataSource3" />
</bean>
\n<bean id="routingDataSource" class="com.nc.datasource.RoutingDataSourceImpl">
    <property name="targetDataSources">
    <map>
        <entry key="ds1" value-ref="dataSource1" />
        <entry key="ds2" value-ref="dataSource2" />
        <entry key="ds3" value-ref="dataSource3" />
    </map>
    </property>
    <property name="defaultTargetDataSource" ref="dataSource1" />
</bean>
\n<bean id="sqlMapClient" class="org.springframework.orm.ibatis.SqlMapClientFactoryBean">
    <property name="configLocation" value="classpath:com/nc/ibatis/SQLMapConfig.xml" />
    <property name="dataSource" ref="routingDataSource" />
</bean>

Y ya esta todo listo, basta con cambiar el valor de environment desde alguno de nuestros Controller a uno de los valores definidos (ds1,ds2,ds3) para que las siguientes consultas se ejecuten sobre el datasource seleccionado.

Bueno, espero que os haga gustado y os sea de utilidad!

Actualización: ejemplo completo en https://github.com/makensi/examples/tree/master/routingdatasource