Spring Dynamic Datasource

1
9802

En este tutorial veremos como utilizar la clase AbstractRoutingDataSource que proporciona spring.

Índice de contenidos

1. Introducción

En este tutorial vamos a ver como utilizar la clase AbstractRoutingDataSource de spring, que se encuentra el módulo de spring-jdbc, esta clase esta disponible desde la versión 2.0.1 de spring.

¿Qué nos proporciona esta clase?

Pues nos permite cambiar de datasource en tiempo de ejecución.

¿Cómo funciona?

Muy fácil esta clase actúa como intermediario, mientras que el datasource «real» se puede determinar de forma dinámica en tiempo de ejecución en base a una clave de búsqueda.

2. Motivación

Debido a la gran demanda de gente preguntando como hacer para poder cambiar de datasource en tiempo de ejecución. Mucha gente ha creado sus propias soluciones, la gente de spring han sacado una solución para este problema.

Este tutorial vamos hacer un pequeño ejemplo que tenemos 2 bases de datos donde guardamos los equipos de fútbol de cada país, mediante un servicio rest que vamos a obtener todos los equipos pasando como parámetro el código del país

3. Entorno

El tutorial está escrito usando el siguiente entorno:

  • Hardware: MacBook Pro 15′ (2.5 GHz Intel Core i7, 16GB DDR3 SDRAM)
  • Sistema Operativo: Mac OS X Yosomite 10.10.5
  • NVIDIA GeForce GT 750M 2048 MB
  • Eclipse Luna 4.4.2
  • Java 1.8
  • Docker 1.9.0
  • Spring Framework 4.1.6.RELEASE
  • Spring MVC 4.1.6.RELEASE
  • Tomcat 8.0.28
  • curl 7.43.0

4. Creación del proyecto

Vamos a crear una proyecto web para exponer nuestro servicio rest, para facilitar el ejemplo vamos a utilizar bases de datos en memoria, en concreto H2 y HSQL

Lo primero es crear un proyecto web con maven y añadimos las dependencias de Spring y Spring MVC, como puede verse a continuación.

pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<groupId>autentia</groupId>
	<artifactId>springDynamicDataSource</artifactId>
	<version>1.0.0</version>
	<packaging>war</packaging>

	<properties>
		<java.version>1.8</java.version>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
		<spring.version>4.1.6.RELEASE</spring.version>
		<fasterxml.jackson.version>2.6.3</fasterxml.jackson.version>
		<mybatis.version>3.2.8</mybatis.version>
		<mybatis.spring.version>1.2.2</mybatis.spring.version>
		<slf4j.version>1.7.12</slf4j.version>
		<log4j.version>2.2</log4j.version>
		<junit.version>4.12</junit.version>
		<hamcrest.version>1.3</hamcrest.version>
		<mockito.version>1.10.19</mockito.version>
	</properties>

	<build>
		<finalName>springDynamicDataSource</finalName>
		<plugins>
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-war-plugin</artifactId>
				<version>2.3</version>
				<configuration>
					<webResources>
						<resource>
							<directory>src/main/webapp</directory>
							<includes>
								<include>WEB-INF/web.xml</include>
							</includes>
						</resource>
					</webResources>
				</configuration>
			</plugin>
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-release-plugin</artifactId>
				<version>2.4</version>
			</plugin>
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-compiler-plugin</artifactId>
				<version>2.5.1</version>
				<configuration>
					<source>${java.version}</source>
					<target>${java.version}</target>
				</configuration>
			</plugin>
		</plugins>
	</build>

	<dependencies>

		<dependency>
			<groupId>javax.servlet</groupId>
			<artifactId>javax.servlet-api</artifactId>
			<version>3.1.0</version>
			<scope>provided</scope>
		</dependency>

		<!-- ======================== Spring ============================ -->

		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-webmvc</artifactId>
			<version>${spring.version}</version>
			<exclusions>
				<exclusion>
					<groupId>commons-logging</groupId>
					<artifactId>commons-logging</artifactId>
				</exclusion>
			</exclusions>
		</dependency>

		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-jdbc</artifactId>
			<version>${spring.version}</version>
		</dependency>

		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-tx</artifactId>
			<version>${spring.version}</version>
		</dependency>

		<!-- ======================== Mybatis ============================ -->

		<dependency>
			<groupId>org.mybatis</groupId>
			<artifactId>mybatis</artifactId>
			<version>${mybatis.version}</version>
		</dependency>

		<dependency>
			<groupId>org.mybatis</groupId>
			<artifactId>mybatis-spring</artifactId>
			<version>${mybatis.spring.version}</version>
		</dependency>

		<dependency>
			<groupId>com.h2database</groupId>
			<artifactId>h2</artifactId>
			<version>1.3.157</version>
		</dependency>

		<dependency>
			<groupId>org.hsqldb</groupId>
			<artifactId>hsqldb</artifactId>
			<version>2.2.9</version>
		</dependency>

		<!-- ======================== JSON ============================ -->

		<dependency>
			<groupId>com.fasterxml.jackson.core</groupId>
			<artifactId>jackson-core</artifactId>
			<version>${fasterxml.jackson.version}</version>
		</dependency>

		<dependency>
			<groupId>com.fasterxml.jackson.core</groupId>
			<artifactId>jackson-annotations</artifactId>
			<version>${fasterxml.jackson.version}</version>
		</dependency>

		<dependency>
			<groupId>com.fasterxml.jackson.core</groupId>
			<artifactId>jackson-databind</artifactId>
			<version>${fasterxml.jackson.version}</version>
		</dependency>

		<dependency>
			<groupId>com.fasterxml.jackson.datatype</groupId>
			<artifactId>jackson-datatype-jdk8</artifactId>
			<version>${fasterxml.jackson.version}</version>
		</dependency>

		<dependency>
			<groupId>com.fasterxml.jackson.datatype</groupId>
			<artifactId>jackson-datatype-jsr310</artifactId>
			<version>${fasterxml.jackson.version}</version>
		</dependency>

		<!-- ======================== Logging ============================ -->

		<dependency>
			<groupId>org.slf4j</groupId>
			<artifactId>slf4j-api</artifactId>
			<version>${slf4j.version}</version>
		</dependency>

		<dependency>
			<groupId>org.slf4j</groupId>
			<artifactId>jcl-over-slf4j</artifactId>
			<version>${slf4j.version}</version>
		</dependency>

		<dependency>
			<groupId>org.apache.logging.log4j</groupId>
			<artifactId>log4j-slf4j-impl</artifactId>
			<version>${log4j.version}</version>
		</dependency>

		<dependency>
			<groupId>org.apache.logging.log4j</groupId>
			<artifactId>log4j-api</artifactId>
			<version>${log4j.version}</version>
		</dependency>

		<dependency>
			<groupId>org.apache.logging.log4j</groupId>
			<artifactId>log4j-core</artifactId>
			<version>${log4j.version}</version>
		</dependency>

		<dependency>
			<groupId>org.apache.commons</groupId>
			<artifactId>commons-lang3</artifactId>
			<version>3.4</version>
		</dependency>

		<!-- ====================== Tests ====================== -->

		<dependency>
			<groupId>junit</groupId>
			<artifactId>junit</artifactId>
			<version>${junit.version}</version>
			<scope>test</scope>
		</dependency>

		<dependency>
			<groupId>org.hamcrest</groupId>
			<artifactId>hamcrest-library</artifactId>
			<version>${hamcrest.version}</version>
			<scope>test</scope>
		</dependency>

		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-test</artifactId>
			<version>${spring.version}</version>
			<scope>test</scope>
		</dependency>

		<dependency>
			<groupId>org.mockito</groupId>
			<artifactId>mockito-all</artifactId>
			<version>${mockito.version}</version>
			<scope>test</scope>
		</dependency>

	</dependencies>

</project>

Para facilitar el ejemplo solo vamos a ver el código que se refiere a la clase AbstractRoutingDataSource, el resto estará disponible en el zip de descarga.

CountryRoutingDatasource.java
package es.autentia.spring.util;

import org.springframework.jdbc.datasource.lookup.*;

public class CountryRoutingDatasource extends AbstractRoutingDataSource {

    @Override
    protected Object determineCurrentLookupKey() {
        return CountryDbContextHolder.getDbType();
    }

}
	

Como vemos solo tenemos que implemetar el método «determineCurrentLookupKey» donde tenemos que devolver la key del map que contiene AbstractRoutingDataSource, en este caso tenemos definido un CountryDbContextHolder que guarda en un ThreadLocal la key que va usar.

Lo siguiente es ver la configuración de la clase CountryRoutingDatasource en fichero de configuración de spring

configuration-spring-datasource.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context"
	xmlns:p="http://www.springframework.org/schema/p" xmlns:mvc="http://www.springframework.org/schema/mvc"
	xmlns:tx="http://www.springframework.org/schema/tx"
	xmlns:jdbc="http://www.springframework.org/schema/jdbc"
	xsi:schemaLocation="http://www.springframework.org/schema/beans
            http://www.springframework.org/schema/beans/spring-beans.xsd
            http://www.springframework.org/schema/context
            http://www.springframework.org/schema/context/spring-context.xsd
            http://www.springframework.org/schema/tx
            http://www.springframework.org/schema/tx/spring-tx.xsd
            http://www.springframework.org/schema/mvc
            http://www.springframework.org/schema/mvc/spring-mvc.xsd
            http://www.springframework.org/schema/jdbc
            http://www.springframework.org/schema/jdbc/spring-jdbc.xsd">

	<!-- Datasource -->

	<bean id="dataSource" class="es.autentia.spring.util.CountryRoutingDatasource">
		<property name="targetDataSources">
			<map key-type="es.autentia.spring.model.Country">
				<entry key="SPAIN" value-ref="spainDataSource" />
				<entry key="GERMANY" value-ref="germanyDataSource" />
			</map>
		</property>
		<property name="defaultTargetDataSource" ref="spainDataSource" />
	</bean>

	<!-- Datasource embedded Spanish -->

	<jdbc:embedded-database id="spainDataSource" type="H2">
		<jdbc:script location="classpath:bd/schema/schema_spain.sql" />
		<jdbc:script location="classpath:bd/data/data_spain.sql" />
	</jdbc:embedded-database>

	<!-- Datasource embedded Germany -->

	<jdbc:embedded-database id="germanyDataSource" type="HSQL">
		<jdbc:script location="classpath:bd/schema/schema_germany.sql" />
		<jdbc:script location="classpath:bd/data/data_germany.sql" />
	</jdbc:embedded-database>

</beans>
	

Como vemos al definir el bean de dataSource (CountryRoutingDatasource) le pasamos a la propiedad targetDataSources que es un map que contiene todos los datasource, en este caso son 2 spainDataSource y germanyDataSource

Otra propiedad importante pero no obligatoria que es defaultTargetDataSource, que en caso de no obtener una key válida en el método determineCurrentLookupKey entonces usar por defecto ese dataSource

Por último vamos a ver nuestro servicio rest

web.xml
package es.autentia.spring.controller;

import java.util.*;
import java.util.stream.*;

import org.springframework.beans.factory.annotation.*;
import org.springframework.web.bind.annotation.*;

import es.autentia.spring.model.*;
import es.autentia.spring.service.*;
import es.autentia.spring.util.*;

@RestController
@RequestMapping(value = "/team")
public class TeamController {

    private static final String COUNTRY = "country";

    private final TeamService teamService;

    private final Map<String, League> mapLeagues;

    @Autowired
    public TeamController(TeamService teamService) {
        this.teamService = teamService;
        this.mapLeagues = Stream.of(League.values()).collect(Collectors.toMap(League::getCode, item -> item));
    }

    @RequestMapping(method = RequestMethod.GET)
    public List<Team> getTeams(@RequestParam(required = false, value = COUNTRY) String code) {
        CountryDbContextHolder.setDbType(mapLeagues.get(code.toUpperCase()));
        return teamService.getTeams();
    }

}
	

Como vemos obtenemos el código del país por medio de una RequestParam y se lo pasamos a nuestro CountryDbContextHolder la key del país para seleccionar correctamente el datasource de ese país.

5. Ejemplo práctico

Para relizar el ejemplo práctico vamos a utilizar docker para desplegar nuestra aplicación en un contendor con tomcat y java 8.

Lo primero es crear nuestro war con el siguiente comando

mvn clean package

Una vez creado nuestro war, procedemos a ejecutar nuestro war en contenedor de docker

docker run -d -p 8080:8080 --name springDynamicDataSource -v `pwd`/target:/usr/local/tomcat/webapps tomcat:8.0.28-jre8

Para comprobar si ha desplegado correctamente vamos a ver el log del contenedor, con el siguiente comando.

docker logs springDynamicDataSource

imagen1

Lo primero es ejecutar con el comando curl para obtener los equipos de España

curl  `docker-machine ip dev`:8080/springDynamicDataSource/rest/team?country=ES

Donde “dev” es el nombre de la máquina virtual donde se esta ejecutando docker.

Ahora vamos a obtener los equipos de Alemania

curl  `docker-machine ip dev`:8080/springDynamicDataSource/rest/team?country=DE

Por último vamos hacer una petición sin pasar el country

curl  `docker-machine ip dev`:8080/springDynamicDataSource/rest/team

Como vemos obtenemos los esquipos de España como es lógico al no pasarle ningun código, el CountryRoutingDatasource va usar el datasource por defecto, en caso de no tener definido provocaria un IllegalStateException.

Con esto termina nuestra prueba con la clase AbstractRoutingDataSource.

6. Conclusiones

Como hemos visto spring no proporciona una clase para poder cambiar de dataSource en tiempo de ejecución de forma fácil y rápida. Solo implementado la clase AbstractRoutingDataSource

Espero que el tutorial os haya animado a usar (o al menos probar) esta utilidad que nos ofrece spring.

Puedes descargar el ejemplo de este tutorial desde aquí

7. Referencias

1 COMENTARIO

DEJA UNA RESPUESTA

Por favor ingrese su comentario!

He leído y acepto la política de privacidad

Por favor ingrese su nombre aquí

Información básica acerca de la protección de datos

  • Responsable:
  • Finalidad:
  • Legitimación:
  • Destinatarios:
  • Derechos:
  • Más información: Puedes ampliar información acerca de la protección de datos en el siguiente enlace:política de privacidad