GEDOPLAN
Jakarta EE (Java EE)

JPA mit dynamischer DB-Verbindung

Jakarta EE (Java EE)

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.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert

Bitte füllen Sie dieses Feld aus.
Bitte füllen Sie dieses Feld aus.
Bitte gib eine gültige E-Mail-Adresse ein.
Sie müssen den Bedingungen zustimmen, um fortzufahren.

Autor

Diesen Artikel teilen

LinkedIn
Xing

Gibt es noch Fragen?

Fragen beantworten wir sehr gerne! Schreibe uns einfach per Kontaktformular.

Kurse

weitere Blogbeiträge

Work Life Balance. Jobs bei Gedoplan

We are looking for you!

Lust bei GEDOPLAN mitzuarbeiten? Wir suchen immer Verstärkung – egal ob Entwickler, Dozent, Trainerberater oder für unser IT-Marketing! Schau doch einfach mal auf unsere Jobseiten! Wir freuen uns auf Dich!

Work Life Balance. Jobs bei Gedoplan

We are looking for you!

Lust bei GEDOPLAN mitzuarbeiten? Wir suchen immer Verstärkung – egal ob Entwickler, Dozent, Trainerberater oder für unser IT-Marketing! Schau doch einfach mal auf unsere Jobseiten! Wir freuen uns auf Dich!