JPA: Dynamic Datasource Routing

Man stelle sich vor, in einer (Web-)Anwendung müssen verschiedene Datasources verwendet werden. Ob bei den unterschiedlichen Datasourcen es sich immer um die andere/gleiche Datenbank handelt, sei mal dahingestellt. Die Datasourcen unterscheiden sich mind. in einer Eigenschaft, z.B. User.

Im Spring-Kontext gilt folgendes: In JPA braucht man pro EntityManagerFactory eine Datasource. Der EMF ist einen TransactionManager zugeordnet.

Man könnte alle Datasource, EMF, TX-Manager und PUs in Spring konfigurieren. Man hat jedoch ein Problem, wenn man n verschiedene Datasourcen hat, die zur Laufzeit ausgewählt werden müssen.

  • Die Konfiguration ist umfangreich und unübersichtlich

    • Pro Datasource ist ein Persistence-Unit notwendig, denn für die Ermittlung des richtigen EntityManager kann @PersistenceContext(unitName = "PU_NAME") verwendet werden
    • Pro Datasource einen EntityManagerFactory mit der Verknüpfung zur PersistenceUnit
    • Pro EntityManagerFactory einen TransactionManager, ggf. mit Qualifier.
  • Die Wartung ist entsprechend nicht optimal. Pro neue Datasource müssen 3 Stellen angepasst werden
  • Das Transaction-Handling wird kompliziert. @Transactional muss nun immer den richtigen TransactionManager nutzen. Die Entscheidung soll natürlich zur Laufzeit passieren. @Transactional("TX_MANAGER_NAME") reicht nicht aus, da es eine statische Kopplung verursacht.

Eine andere Lösungsvariante wäre, statt einen EntityManager durch @PersitenceContext eher direkt den EntityManagerFactory durch @PersistenceUnit zu bekommen. Hier modifiziert man die EntityManager-Erstellung soweit, dass man den EntityManager mit der gewünschten Datasource bekommt. Falls sich bei den Datasourcen nur die User unterscheiden und Eclipselink verwendet wird, siehe u.a. [1].

Perfekt wäre jedoch eine Lösung, in der nur EntityManagerFactory, ein TransactionManager und ein PersistenceUnit verwendet wird. Zur Laufzeit soll dann der vorhandene EntityManagerFactory für die gewünschte Datasource einen EntityManager erstellen.

Das funktioniert. Der EntityManagerFactory erwartet eine Referenz auf die DataSource. Hier könnte man eine eigene Implementierung dieses Interface anbieten, welches die Unterscheidung bzgl. der verschiedenen Datasourcen durchführt. DataSource.getConnection() muss dann abhängig von fest definierten Eigenschaften die Connection von der richtigen Datasource zurückgeben. Um die Eigenschaften in der eigenen DataSource-Implementierung abzufragen, ist die Nutzung von ThreadLocal bzw. InheritableThreadLocal möglich.

Eine Spring-spezifische Lösung die in diese Richtung geht, ist mittels AbstractRoutingDatasource möglich [2]. Hier muss die abstrakte Methode determineCurrentLookupKey() implementiert werden. In dieser Methode muss die Logik verpackt werden, damit anhand des resultierenden Keys die richtige Datasource verwendet werden kann. Das Mapping zwischen Key und Datasource wird in AbstractRoutingDatasources#targetDataSources gehalten. Wird kein Key zurückgeliefert, wird die vorher definierte Standard-Datasource verwendet.

Eine Beispiel-Implementierung, welches mit Spring 3.x und jeweils mit Eclipselink und Hibernate funktioniert, sieht wie folgt aus:

Die relevante Spring-Konfiguration:

<bean id="baseDataSource" abstract="true">
<property name="driverClassName" value="${db.driverClassName}"/>
<property name="url" value="${db.url}"/>
</bean>

<bean id="dataSource1" parent="baseDataSource">
<property name="user" value="${db.1.user}"/>
<property name="password" value="${db.1.pw}"/>
</bean>
<bean id="dataSource2" parent="baseDataSource">
<property name="user" value="${db.2.user}"/>
<property name="password" value="${db.2.pw}"/>
</bean>
<bean id="dataSource3" parent="baseDataSource">
<property name="user" value="${db.3.user}"/>
<property name="password" value="${db.3.pw}"/>
</bean>

<bean id="dynamicDatasource" class="info.center-of.spring.jpa.DynamicDataSourceRouting">
<property name="targetDataSources">
<entry key="${db.1.user}" value-ref="dataSource1"/>
<entry key="${db.2.user}" value-ref="dataSource2"/>
<entry key="${db.3.user}" value-ref="dataSource3"/>
</property>
<property name="defaultTargetDataSource" ref="dataSource1"/>
</bean>

<bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalEntityManagerFactoryBean">
<property name="datasource" ref="dynamicDatasource" />
<property name="jpaVendorAdapter">
<!-- Fuer Hibernate -->
<bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter" p:showSql="true" generateDdl="true" />
<!-- Fuer Eclipselink
<bean class="org.springframework.orm.jpa.vendor.EclipselinkJpaVendorAdapter" p:showSql="true" generateDdl="true" />
-->
</property>
</bean>

<bean class="org.springframework.orm.jpa.JpaTransactionManager">
<property name="entityManagerFactory" ref="entityManagerFactory" />
</bean>

<tx:annotation-driven />

Die Klasse DynamicDataSourceRouting überschreibt die Methode zur Ermittlung des Keys. In unserem Fall ist dies immer der aktuelle/gewünschte DB-User:

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

public class DynamicDataSourceRouting extends AbstractRoutingDataSource {

@Override
protected Object determineCurrentLookupKey() {
return DBUserContextHolder.getDBUser();
}
}

DBUserContextHolder ist eine Klasse mit einer ThreadLocal-Variable, die in dem aktuellen Thread den DB-User hält

public class DBUserContextHolder {

private static final InheritableThreadLocal<String> contextHolder = new InheritableThreadLocal<String>();

public static void setDBUser(final String dbUser) {
contextHolder.set(dbUser);
}

public static String getDBUser() {
return (String) contextHolder.get();
}

public static void clear() {
contextHolder.remove();
}
}

Bevor der EntityManager nun erstellt wird, muss

DBUserContextHolder.setDBUser("dbUser1");

aufgerufen werden.

Mit dieser Lösung hat man "nur" eine enge Kopplung mit Spring. Die Lösung funktioniert mit verschiedenen ORMs.

Links:

comment

Comments

arrow_back

Previous

Derby Export

Next

.NET: XmlSerializer ohne dynamische Code-Generierung
arrow_forward