In Java-EE-Anwendungen, die JPA zum DB-Zugriff nutzen, wird im Normalfall eine Datasource zur Spezifikation der Datenbankverbindung genutzt:
<persistence ...> <persistence-unit name="seminar"> <jta-data-source>jdbc/seminar</jta-data-source>
Für eine so definierte Persistence Unit lässt man dann einen EntityManager
injizieren – üblicherweise in einem CDI Producer:
public class EntityManagerProducer { @PersistenceContext(unitName = "seminar") EntityManager entityManager; @Produces public EntityManager getEntityManager() { return this.entityManager;
Das funktioniert so auch sehr gut, ist allerdings in Bezug auf die genutzte Datenbank recht statisch: Die Datasource – im Beispiel mit dem JNDI-Namen jdbc/seminar
– muss zum Deployment-Zeitpunkt der Anwendung im Server konfiguriert sein. Zur Anwendungslaufzeit lässt sie sich nicht wechseln.
Um eine dynamische Zuordnung der DB zur Laufzeit zu ermöglichen, muss man die EntityManagerFactory
per API initialieren und dabei die Connect-Parameter für die gewünschte Datenbank als Properties mitgeben:
<persistence ...> <persistence-unit name="showcase" transaction-type="JTA"> <properties> <!-- Gemeinsame Parameter für die im Beispiel genutzten Datenbanken --> <property name="javax.persistence.jdbc.driver" value="org.h2.Driver" /> <property name="javax.persistence.jdbc.user" value="showcase" /> <property name="javax.persistence.jdbc.password" value="showcase" /> <!-- Diese Property wird eigentlich nicht benötigt, da sie beim Aufbau der EntityManagerFactory übergeben wird --> <property name="javax.persistence.jdbc.url" value="jdbc:h2:mem:" />
public class EntityManagerProducer { private EntityManagerFactory fetchEntityManagerFactory() { private ConcurrentHashMap<String, EntityManagerFactory> factoryMap = new ConcurrentHashMap<>(); // Aktueller URL kommt aus anderem Service, z. B. "jdbc:h2:mem:showcase_1;DB_CLOSE_DELAY=-1" String url = ...; // Falls nötig, Map-Eintrag für URL erstellen this.factoryMap.computeIfAbsent(url, u -> { Map<String, String> prop = new HashMap<>(); // dabei URL als Property übergeben prop.put("javax.persistence.jdbc.url", url); // und DDL erlauben prop.put("eclipselink.ddl-generation", "create-or-extend-tables"); prop.put("eclipselink.ddl-generation.output-mode", "database"); prop.put("hibernate.hbm2ddl.auto", "update"); return Persistence.createEntityManagerFactory("showcase", prop); }); return this.factoryMap.get(url); } ...
Im Beispiel wird nur der Datenbank-URL dynamisch übergeben: Die Property javax.persistence.jdbc.url
wird in Zeile 14 in ein Map
-Obekt eingetragen und anschliessend zur Initialisierung der EntityManagerFactory
verwendet. Im Beispiel wird ein ConcurrentHashMap
genutzt, um die erzeugten Factories pro URL als Singletons zu führen.
Der Producer liefert nun einen EntityManager
aus der so erzeugten EntityManagerFactory
. Der Synchronisationstyp SynchronizationType.SYNCHRONIZED
sorgt dafür, dass der EntityManager
sich automatisch mit der JTA-Transaktion verbindet.
Da der EntityManager
nun Application-managed ist, muss ein Disposer zum Schliessen vorgesehen werden:
... @Produces @RequestScoped EntityManager createEntityMnager() { return fetchEntityManagerFactory().createEntityManager(SynchronizationType.SYNCHRONIZED); } void closeEntityManager(@Disposes EntityManager entityManager) { if (entityManager.isOpen()) { entityManager.close();
Auf diese Weise gelingt die Umschaltung der DB-Verbindung zur Laufzeit. Im Beispielprojekt sind die DB-URLs in einem separaten Service fest definiert. Sie könnten aber auch problemlos aus einer anderen, noch dynamischeren Quelle kommen.
Es zeigt sich hier einmal mehr, wie mächtig das Konzept der Producer in CDI ist: Die Dynamisierung der DBs geschieht alleine dort. Die konkreten DB-Zugriffe in der restlichen Anwendung bleiben unverändert.
Der sich nun möglicherweise einstellenden Euphorie muss allerdings ein „Aber“ entgegengestellt werden: Die gezeigte Lösung nutzt ein Feature aus, das in der JPA-Spezifikation nur schwach spezifiziert ist. Um einen EntityManager
an JTA anbinden zu können, muss die Persistence Unit mit dem Parameter transaction-type="JTA"
versehen sein. In dem Fall wird in der Spezifikation die Nutzung einer Datasource „erwartet“ – nicht etwa „verlangt“. Es ist sehr schade, dass die JPA-Spezifikation eine ganze Reihe solcher unterschiedlich interpretierbaren Sätze enthält.
In der Beispiellösung wird die DB-Verbindung über Properties angegeben, was Hibernate als JPA-Provider ohne Murren akzeptiert, während EclipseLink den Dienst verweigert. Insofern läuft die gezeigte Lösung nicht auf einem GlassFish (mit EclipseLink), wohl aber auf WildFly (mit Hibernate).
Den gezeigten Code finden Sie in einem kompletten Demoprojekt auf GitHub.