JPA mit dynamischer DB-Verbindung

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.

Advertisements

Über Dirk Weil
Dirk Weil ist seit 1998 als Berater im Bereich Java tätig. Als Geschäftsführer der GEDOPLAN GmbH in Bielefeld ist er für die Konzeption und Realisierung von Informationssystemen auf Basis von Java EE verantwortlich. Seine langjährige Erfahrung in der Entwicklung anspruchsvoller Unternehmenslösungen machen ihn zu einem kompetenten Ansprechpartner und anerkannten Experten auf dem Gebiet Java EE. Er ist Autor in Fachmagazinen, hält Vorträge und leitet Seminare und Workshops aus einem eigenen Java-Curriculum.

Kommentar verfassen

Trage deine Daten unten ein oder klicke ein Icon um dich einzuloggen:

WordPress.com-Logo

Du kommentierst mit Deinem WordPress.com-Konto. Abmelden / Ändern )

Twitter-Bild

Du kommentierst mit Deinem Twitter-Konto. Abmelden / Ändern )

Facebook-Foto

Du kommentierst mit Deinem Facebook-Konto. Abmelden / Ändern )

Google+ Foto

Du kommentierst mit Deinem Google+-Konto. Abmelden / Ändern )

Verbinde mit %s

%d Bloggern gefällt das: