JDO-JPA-Migrationsstrategien

Wäre dieser Artikel vor fünf Jahren entstanden, dann wäre die Migrationsrichtung der meisten Projekte sicherlich diese: Weg vom reinen JDBC oder weg von Container-Managed-Persistence hin zu JDO. Im Jahre 2003 erscheint mit JDO 1.0.1 ein Jahr nach der Verabschiedung von JDO 1.0 das erste kleinere Update der Spezifikation, welches auf Jahre hin die Basis einer Hand voll kommerzieller O/R-Mapper-Implementierungen bildet. JDO war damals aus architektonischer Sicht, wenn auch manchmal leicht polarisierend, die erste Wahl für die Persistenzstrategie einer Java-Enterprise-Anwendung. Im Vertrauen auf den Standard wurde eine JDO-Implementierung auch gerne anderen kommerziellen, aber nicht-standard-konformen O/R-Mapping-Frameworks vorgezogen. Heute, im Jahr 2008 sieht die Situation anders aus: Auch wenn JDO 2.0 die heute fortschrittlichste O/R-Mapping-Spezifikation für Java darstellt, kann das Interesse an ihr kaum geringer sein. Neue Projekte, die JDO den Vorzug geben, gibt es faktisch nicht mehr und in bestehenden Projekten ist ein gewisser Migrationsdruck weg von JDO zu verspüren. Wie eine solche Migration möglichst reibungslos vonstatten gehen kann, versucht dieser Artikel im Folgenden aufzuzeigen.

Die Gründe, warum eine so vielversprechende Technologie wie JDO heute niemanden mehr so richtig zu begeistern vermag, sind schnell aufgezählt und haben mit der Technologie soviel zu tun, wie Lederhosen mit Ostfriesland. Das Geschäftsmodell hinter JDO sah ausnahmslos bei allen bedeutenden Frameworks so aus, dass gegen Lizenzkosten für Entwicklungs- und Laufzeitumgebung eine Closed-Source-Software geliefert wurde, die zusätzlich meist noch obfuskiert war. Letzter Punkt machte es den Kunden unmöglich, selbst auf Fehlersuche zu gehen. Neben den üblichen Lizenzkosten wurden so gleichzeitig noch Ausgaben für den Produktsupport zwingend notwendig. Die zusätzliche Obfuskierung des Java-Bytecodes war der Sache ebenso kaum zuträglich, wie folgende Exception-Meldung gut verdeutlichen kann:

LiDOFatalException: Unsupported associate type: class xcalia.lido.ds.jdbc.meta.e.a.b.f.

Expected either xcalia.lido.ds.jdbc.meta.e.a.b.d or xcalia.lido.ds.jdbc.meta.e.a.b.e

Dieser übereifrige Schutz des geistigen Eigentums machte es Open-Source-Alternativen umso leichter, den Persistenz-Markt für sich zu erobern. Keine Lizenzkosten, offene Quellen und belebte Foren waren für viele Entwickler anziehender als die so oft gepriesene Supportgarantie einer kommerziellen Lösung. Die richtige Open-Source-Lizenz gepaart mit einer guten Implementierung und einem gewissen Gespür für die Nöte eines Entwicklers machte letztendlich Hibernate zum unangefochtenen Marktführer in diesem Bereich.

Andererseits ist der JDO-Standard auch bei dem unsäglichen Gerangel um die Nachfolge von Container-Managed-Persistence nicht gut davon gekommen. Statt auf den bestehenden Mapping-Standard aufzusetzen und diesen den reellen Bedürfnissen anzupassen, fiel der JSR220-Expert-Group zu EJB3 nichts Besseres ein, als die bestehenden JDO-Konzepte mit viel Unschärfe zu garnieren und daraus das Java-Persistence-API zu erschaffen. Dass damit das Ende von JDO beschlossene Sache war, beweisen nicht zuletzt Sätze aus der JDO2.0-Spezifkation, wie

Metadata annotations for persistence are being developed in JSR 220. When that specification is final, an update to the JDO specification to specify support for the annotations will be made. [1]

Die JDO2.1 Spezifikation bringt dieses Update. Wenn das Mapping-Framework JDO-Annotationen anbietet, muss es auch JPA-Annotationen unterstützen. Die Spezifikation setzt noch eines drauf und definiert ein Interface JDOEntityManager, welches sowohl die Methoden des EntityManagers (JPA) als auch die Methoden des PersistenceManagers (JDO) umfasst [2]. Da mag es wenig überraschen, dass das bisher mit aufklärerischer Mission geführte Projekt JPOX, seines Zeichens die Referenz-Implementierung für JDO 2.0, sein Ende gefunden hat [3]. Die Entwickler gehen hier nun eigene Wege auf der Basis des JPOX-Codes [4]. Auch mag es kaum verwundern, dass Craig Russell, der Specification Lead von JSR012 (JDO) und JSR243 (JDO 2.0), sich nun lieber bei OpenJPA einbringt [5].

Schaut man sich die noch bestehenden JDO-Implementierungen an, so ergibt sich ein trauriges Bild. Als Basis soll die Produkt-Auflistung von Robin M. Roos aus dem Jahre 2003 dienen [6]:

  • FastObjects by Poet: in der Folge des Zusammenschluss von Poet Software mit Versant untergegangen; heute bietet Versant unter diesem Namen einen O/R-Mapper für .NET an
  • JDO Genie von Hemisphere Technologies: wurde von Versant aufgekauft und als Versant Open Access fortgeführt, später als JSR220-ORM an die Eclipse Foundation gespendet; dieses Projekt geht später in das JPA-Mapping-Werkzeug Dali auf; der .NET-Zweig von Versant Open Access wird unter anderer Firma als Vanatec OpenAccess fortgeführt
  • JRelay von Object Industries GmbH: im Internet nicht auffindbar
  • Kodo JDO: als Produkt samt Entwicklungsabteilung von TechTrader an SolarMetric verkauft, später wird SolarMetric von Bea, Inc. aufgekauft; Bea nutzt Kodo für die Implementierung eines EJB3-konformen Persistenz-Providers für den WebLogic Application-Server; der JPA-Teil von Kodo wird als OpenJPA offengelegt; mit der Übernahme von Bea durch Oracle verliert Kodo den strategischen Fokus; Oracle favorisiert klar TopLink/EclipseLink als JPA-Persistenz-Provider der zukünftigen WebLogic Application-Server
  • intelliBO von Signsoft: laut eigener Roadmap steht die Unterstützung von JPA noch für 2008 an; ob die JPA-Unterstützung hauptsächlich, aber nicht ausschließlich ist, wird sich zeigen.
  • LiDO von LIBeLIS: LIBeLIS nennt sich in Xcalia um; das Produkt wird unter dem Namen Xcalia Intermediation Core (XIC) fortgeführt; die Progress Software Corporation/ DataDirect Technologies übernehmen ohne viel Presserummel Xcalia in diesem Jahr
  • Open Fusion JDO von PrismTech: unter dem Namen „Open Fusion“ ist heute nur noch eine CORBA-Middleware-Lösung zu finden
  • Orient JDO von Orient Technologies: ein ODBMS mit JDO Interface; auf der Firmenhomepage wird nach Freiwilligen für die Implementierung von JDO 2.0 gesucht [7]
  • PE:J von HYWY: die Produkt-Homepage ist eine Tripod-Seite

Sollten diese Informationen korrekt sein, so sind von den neun Produkten heute noch Xcalia XIC, Bea/Oracle Kodo und Signsoft intelliBO für Java auf dem Markt verfügbar. Ein genauerer Blick lässt allerdings auch daran Zweifel aufkommen: Beim Zugriff auf die Xcalia-Produktdokumentation wird man seit Wochen schon in eine Umleitungsschleife geschickt [8].

$ telnet xdn.xcalia.com 80
GET /xdn/docs/files/XcaliaCore/4.4.1/releaseNotes.txt HTTP/1.0

Host: xdn.xcalia.com
Referer: http://www.xcalia.com/xdn/docs/docs.jsp

HTTP/1.1 301 Moved Permanently
Date: Mon, 04 Aug 2008 07:24:17 GMT
Server: Apache/2.2.4 (Win32) mod_jk/1.2.23
Location: http://xdn.xcalia.com/xdn/docs/files/XcaliaCore/4.4.1/releaseNotes.txt
Content-Length: 278
Connection: close
Content-Type: text/html; charset=iso-8859-1

Im Forum von Signsoft intelliBO gibt es auf Fragen keine Antworten und auf Antworten keine Fragen [9]. Bei Bea Kodo ist die angegebene E-Mail-Adresse für geschäftliche Anfragen stillgelegt:

<kodo@bea.com>: host repmmg01.bea.com[66.248.1xx.xx]
said: 553 5.3.0

<kodo@bea.com>... REJECT 550 Invalid Address
(in reply to RCPT TO command)

Auch wenn man noch weitere Indizien aufführen könnte, sollte doch an dieser Stelle klar sein, dass die JDO-Technologie nun leider der Vergangenheit angehört. Leb wohl, JDO!

Anlass zu übermäßiger Traurigkeit ist nicht gegeben, da die wesentlichen Konzepte von JDO in der JPA-Spezifikation fortexistieren. Das heißt, eine Anwendung, die heute ihre Persistenz auf der Basis von JDO realisiert, muss in ihrer Architektur nicht ansatzweise geändert werden, falls sie auf JPA umgestellt werden muss.

Frühjahrsmüdigkeit

Bevor nun die Migrationsstrategie im Detail vorgestellt wird, fällt es mir schwer, einen Seitenhieb auf das Springframework auszulassen. Spring versprach schon in seinen ersten Versionen, die Persistenz-Schicht so weit abstrahieren zu können, dass welch selbige, sollte der Fall eintreffen, schnell und mühelos ausgetauscht werden kann. Die Muster der Wahl heißen hier: Data-Access-Object, Command und die allgegenwärtige Dependency Injection. DAOs und das Command-Muster verursachten schon zu Zeiten von JDO eine unnütze Abstraktion und, schlimmer noch, sind sie kontraproduktiv für eine effiziente Migration, da sie viel zusätzlich zu migrierenden Code entstehen lassen. Folgt man der Spring-Dokumentation aufs Wort genau, sieht die ideale Aufrufsequenz für JDO folgendermaßen aus [10]:




Abbildung 1: Spring O/R-Mapping

Das Objekt „serviceUser“ benutzt den Service „productService“, um in diesem fiktiven Beispiel den Preis aller Produkte aus der übergebenen Produktkategorie zu erhöhen (productService .increasePriceByCategory( category )). Dieser Methodenaufruf kann über Spring-AOP deklarativ mit Transaktionsgrenzen versehen werden. Der Methodenaufruf benutzt selbst ein injiziertes productDAO-Objekt, um Zugriff auf den Persistenz-Speicher zu bekommen. Dem dort injizierten jdoTemplate-Objekt wird ein JDOCallback-Kommando übergeben, welches letztendlich das JDO-API bedient.

Würde man sich in den ProductService einen Transaction-Aware-PersistenceManager von Spring injizieren lassen, so wird die gesamte weitere Sequenz zur Makulatur. Und das, ohne an Funktionalität einzubüßen. Realistisch betrachtet ist das DAO-Pattern nur bei JDBC sinnvoll, da man hier die CRUD-Operationen selbst implementieren muss und immer eine gewisse Typunschärfe im Code zu ertragen hat. (Ist der Parameter „category“ vom Typ String oder vom Typ „Category“ oder ist es eine ID vom Typ long?) Beide Probleme existieren bei O/R-Mapping-Frameworks nicht, egal welchen Standard sie implementieren. Weiterhin ist bei allen mir bekannten O/R-Mappern das Mischen von Objektgraphen aus verschiedenen Persistenz-Speichern verboten. Damit verliert das DAO-Pattern seine Daseinsberechtigung. Kurzum: Bei einer Migration sollte in Erwägung gezogen werden, derartige Code-Geschwüre zu entfernen.

Mir unverständlich bleibt, warum Spring keine echte Abstraktion zur O/R-Mapping-Technologie schafft. So existieren, mit weitgehend übereinstimmender Schnittstelle JdoTemplate, HibernateTemplate, JpaTemplate und TopLinkTemplate. Wie ähnlich sich die Strukturen sind, müsste jedem aufgefallen sein, der sich mit mehr als einem Framework auseinander gesetzt hat. Die Gemeinsamkeiten, zumindest auf semantischer Ebene umfassen:

  • die generelle Verwendung von Java-Beans (POJOs) als Datenklassen,
  • das automatische Zurückschreiben von Änderungen im Bean-Graph,
  • das automatische Nachladen während der Graph-Traversierung,
  • das Persistieren neuer Daten-Objekte über Erreichbarkeit,
  • das Löschen von Daten-Objekten,
  • die Beachtung der Objekt-Identität,
  • der Lebenszyklus der Daten-Objekte,
  • das Transaktionshandling,
  • das Ressourcen-Management

Auf die letzten beiden Punkte soll im Detail eingegangen werden, da sie für die Migrationsstrategie einen wichtigen Pfeiler bilden. Abbildung 2 beschreibt zunächst die allgemeinen Zusammenhänge, die sich hier als Muster herausgebildet haben: Ein Bootstrapping-Vorgang erzeugt eine Session-Factory, über die einzelne Sessions gestartet werden können, wobei in einer Session einzelne Transaktionen eingebettet sind. Übertragen auf das klassische JDBC heißen die entsprechenden Konzepte: DataSource-Bootstrapping, DataSource, Connection und Transaction.




Abbildung 2: Ressource-Management

Die Bedeutung dieser Konzepte im O/R-Mapping-Kontext umfasst im Wesentlichen die Bedeutung des JDBC-Pendants, erweitert diese aber: Ein Persistenz-Transaktion ist gleichbedeutend mit der Änderung, dem Anlegen oder Löschen von persistenten Objekten. Sie wird typischerweise „Transaction“ (JDO/Hibernate), „EntityTransactiony“ (JPA) oder auch „UnitOfWork“ (TopLink) genannt. Eine Persistenz-Transaktion ist immer an eine Datenbank-Transaktion gekoppelt.

Eine Session ist erforderlich, um Objekte lesen zu können. Sie beinhaltet einen Objekt-Cache mit dessen Hilfe Java-seitig Referenzgleichheit sichergestellt wird, wenn eine Datenbank-Entität über verschiedene Wege geladen wird. Ob und wie lange eine Session Datenbankverbindungen hält, ist ein Implementierungsdetail. Aus Skalierungsgründen wird typischerweise die Ressourcenbindung in der Session auf ein Minimum reduziert. Gleichbedeutende Namen sind „PersistenceManager“ (JDO), „EntityManager“ (JPA), „Session“ (Hibernate) oder „DatabaseSession“ (TopLink).

Eine Session-Factory ist gemäß dem Factory-Muster eine Instanz, über die vorkonfigurierte Sessions bezogen werden können. Eine Session-Factory ist im Two-Tier-Modus weiterhin für das interne Ressourcenmanagement zuständig, im Application-Server-Kontext werden meist fremd-verwaltete Ressourcen in Form von DataSources genutzt.
Namen für Session-Factories sind „PersistenceManagerFactory“ (JDO), „EntityManagerFactory“ (JPA), „SessionFactory“ (Hibernate) und „SessionManager“ (TopLink). In einer Anwendung existiert pro Datenbank meist nur eine Session-Factory. Damit die Session-Factory Verbindungen zu dieser Datenbank in Form von Sessions ausgeben kann, muss sie selbst mit entsprechenden Konfigurationseinstellungen initialisiert werden. Das Bootstrapping ist stark kontextabhängig, beliebt sind hier aber Implementierungen des Abstract-Factory-Musters gepaart mit einer Properties-Datei. Namen in diesem Zusammenhang sind „JDOHelper“ (JDO), „Persistence“ (JPA), „Configuration“ (Hibernate) oder „SessionManager“ (TopLink).

Die Migrationsstrategie

Auf der Basis dieser gemeinsamen Muster ist es für den konkreten Anwendungsfall immer möglich, eine gemeinsame Abstraktion für das JDO-Framework und das Framework, zu dem migriert werden soll, zu finden. Die Migrationsstrategie zerfällt somit in zwei Teile, die nacheinander abgearbeitet werden können:

  1. Finden einer gemeinsamen Abstraktion für die Schnittstellen des alten und neuen O/R-Mapping-Framework
  2. Erstellen der Datenbank-Mappings für das neue Framework

Der Vorteil dieser Vorgehensweise liegt in der Risikominimierung während der Migration. Die gemeinsame Abstraktionsschicht, wenn nicht schon in Teilen vorhanden, wird zunächst nur für das alte Framework implementiert. Es ist somit ein reiner Refaktorierungsschritt. Das neue Mapping-Framework wird an dieser Stelle nur als Anschauungsobjekt gebraucht, um eine sinnvolle Abstraktion zu finden. Auch das Erstellen der neuen Datenbank-Mappings kann ohne Störung der bestehenden Anwendung erfolgen. Am Ende der Migration sollte es möglich sein, zwischen beiden Mapping-Frameworks hin und her zu schalten. Falls unerwartete Probleme mit dem neuen Framework auftreten sollten, kann für diesen Zeitraum mit dem alten Framework weitergearbeitet werden.

Die gemeinsame Abstraktion

Viel Arbeit ist in die Abstraktion des eigenen Persistenz-APIs nicht zu investieren, da im konkreten Fall kaum die ganze Bandbreite an Merkmalen des jeweiligen O/R-Mappers genutzt wird. Erfahrungsgemäß umfassen 80% der verwendeten Features:

  • neue Objekte persistieren,
  • bestehende Objekte über ihre ID laden,
  • bestehende Objekte aus der Datenbank löschen und
  • Objekte suchen

Für letzten Punkt besteht meist schon eine Abstraktion, da Anwendungen oft zu gleichförmigen Abfragen neigen, die der jeweiligen Anwendungsarchitektur und dem Zweck der Anwendung geschuldet sind. Damit muss das abstrahierte Session-API im Wesentlichen nur die Methoden

  • persist(Object),
  • getObjectById(ObjectID),
  • deleteObject(Object) und
  • createQuery() / executeQuery(Query)

vorgeben, um 80% der Anwendungsfälle abzudecken. Die jeweilige Implementierung ist bis auf den letzten Punkt trivial, da sie direkt an das darunterliegende Framework delegiert werden kann. Wie die Query abstrahiert werden kann, ist anwendungsfallbezogen und lässt sich so nur schwer verallgemeinern. Generell sind die Query-APIs der Mapping-Frameworks so angelegt, dass sie zu einer Kandidatenklasse samt Filter und Parameter eine Liste persistenter Objekte zurückliefern. Standardmäßig kann auch der Füllstand der Ergebnismenge (Fetch-Pläne, Fetch-Joins) und Paging-Parameter (Offset, Page-Size) definiert werden. Das eigene Query-API sollte jedoch so entworfen werden, dass es gerade die tatsächlichen Anwendungsfälle abdeckt und nicht alle denkbar möglichen. Nicht verwendete Konfigurationsmöglichkeiten sollte man rigoros weglassen.

Diese Hinweis ist so selbstverständlich wie trivial: Der Entwurf des eigenen Persistenz-APIs ist dann abgeschlossen, wenn keine Abhängigkeiten der Geschäftslogik mehr zu den Klassen/Interfaces des Mapping-Frameworks oder zu den Klassen/Interfaces aus javax.jdo.* bestehen. Wenn hingegen Datenklassen weiterhin Abhängigkeiten zum Mapping-Framework aufweisen, kann dies zunächst vernachlässigt werden. Bestenfalls könnte man hier versuchen, diese Abhängigkeiten so gut es geht zu eliminieren, da sie ohnehin den architektonischen Spielregeln der Mapping-Frameworks widersprechen. Instance-Callbacks können ohne weiteres bestehen bleiben.

Für die Abstraktion des O/R-Framework-APIs sollen nun die Bereiche Bootstrapping, Session-Factory, Session und Transaction im Detail betrachtet werden. Um Namenskonflikte zu vermeiden, werden die Wrapper-Klassen entsprechend der Abbildung 3 PersistenceService, PersistenceSessionFactory, PersistenceSession und PersistenceTransaction genannt.



Abbildung 3: Persistence-Wrapper

Das Bootstrapping wartet mit Stolperfallen auf. Nicht nur hat sich bisher kein einheitliches Muster herausgebildet, die Initialisierung eines Frameworks ist auch von der Einsatzumgebung abhängig. Hier vorgestellt werden soll eine Möglichkeit, die sich hauptsächlich für den EJB3-Fall und nebensächlich für eine Two-Tier-Testumgebung eignet. Andere Umgebungen, wie OSGi oder Spring müssen entsprechend anders behandelt werden - auf diese beiden Fälle wird hier nicht eingegangen. Es sollte dennoch nicht schwer fallen, die hier getroffenen Aussagen auf diese Architektur-Frameworks zu übertragen.

Das Bootstrapping macht sich den Java6-ServiceLoader-Mechanismus zunutze, um eine passende Implementierung zu finden. Die PersistenceService-Klasse könnte wie folgt aussehen

import java.util.ServiceLoader;

public abstract class PersistenceService {

  protected abstract PersistenceSessionFactory providePersistenceSessionFactory(String name);

  public static PersistenceSessionFactory createPersistenceSessionFactory(String name) {
    ServiceLoader<PersistenceService> serviceLoader = ServiceLoader
        .load(PersistenceService.class);
    for (PersistenceService persistenceService : serviceLoader) {
      PersistenceSessionFactory ps = persistenceService
          .providePersistenceSessionFactory(name);
      if (ps != null) {
        return ps;
      }
    }
    return null;
  }

}

Eine Implementierung der abstrakten Klasse PersistenceService muss lediglich die Methode providePersistenceSessionFactory(String) geeignet implementieren. Der Kontrakt dieser Methode sieht so aus, dass im Falle des Fehlschlagen null zurückgeliefert wird (silent fail). Eine Implementierung für JPA im Two-Tier-Fall könnte so aussehen:

public class JPAPersistenceService extends PersistenceService {

  @Override
  protected PersistenceSessionFactory providePersistenceSessionFactory(String name) {
    try {
      EntityManagerFactory delegate = Persistence
            .createEntityManagerFactory(name);
      if (delegate != null) {
        return new JPAPersistenceSessionFactory(delegate);
      }
    } catch (PersistenceException e) {
      //TODO some logging
    }
    return null;
  }
}

Dieser PersistenceService wird über einen Eintrag in der Datei META-INF/services/my.package.PersistenceService bekannt gemacht. In dieser Datei erscheint dazu lediglich die Zeile

my.package.impl.JPAPersistenceService  # JPA, two-tier

Eine PersistenceSessionFactory erhält man anschließend über den Aufruf

PersistenceService.createPersistenceSessionFactory(String)

Der String-Parameter kann für den konkreten Anwendungsfall genutzt werden, um beispielsweise zwischen verschiedenen JPA-PersistenceUnits zu unterscheiden.

Wie man den Quelltexten unschwer entnehmen kann, wird bei wiederholtem Aufruf der Methode createPersistenceSessionFactory eine immer neue PersistenceSessionFactory erzeugt. Das Ergebnis des ersten Aufrufs sollte deshalb aufbewahrt und der Anwendung geeignet verfügbar gemacht werden, beispielsweise über einen Locator.

Das Bootstrapping für JPA innerhalb eines Application-Servers gestaltet sich aus Anwendungssicht grundlegend anders als der Two-Tier-Fall. Es ist auch grundlegend anders als der spezifizierte JDO-Bootstrapping-Mechanismus über JCA. Die JDO-Spezifikation sieht für das Application-Server-Szenario vor, die PersistenceManagerFactory über JNDI verfügbar zu machen. Wird ein PersistenceManager benötigt, so tätigt man zunächst einen JNDI-Lookup, um die PersistenceManagerFactory zu bekommen, nur um anschließend über die Methode getPersistenceManager() einen PersistenceManager zu erhalten. Der PersistenceManager wird als Ressource an die aktuell laufende Transaktion gebunden. Pro Transaktion existiert so nur ein PersistenceManager.
So ist es gefahrlos möglich, mehrfach PersistenceManagerFactory #getPersistenceManager() und PersistenceManager#close() innerhalb einer Transaktion aufzurufen, ohne den First-Level-Objekt-Cache zu zerstören.

Der JSR244 (JEE5) definiert zwei Mechanismen, um an den JPA-Dienst der Anwendung zu gelangen [11]. Der erste erfolgt über die Annotation @PersistenceUnit und führt zur Injektion einer EntityManagerFactory in das annotierte Feld. Die zweite Möglichkeit besteht in der Annotation @PersistenceContext und führt zur Injektion eines EntityManagers. Beide Varianten existieren analog auch für die Injektion in den lokalen JNDI-Baum der Anwendung. Obwohl die erste Möglichkeit der JDO-Variante optisch am nächsten kommt, kann sie nicht genutzt werden.
Der Grund hierfür liegt in einer leicht abweichenden Semantik der Methode EntityManagerFactory#createEntityManager() im Application-Server. Die Methode dient in diesem Umfeld dazu, einen neuen, physisch getrennten EntityManager zu erzeugen. Der vorrangige Anwendungsfall hierfür ist die Kopplung eines EntityManagers an den Lebenszyklus einer StatefulSessionBean. Oder anders ausgedrückt: Wird die Methode createEntityManager() mehrfach pro Transaktion aufgerufen, erhält man verschiedene EntityManager mit getrennten First-Level-Objekt-Caches. Auf verschiedenen Wegen geladene Objekte sind dann nicht mehr referenzgleich.

Leider definiert der JSR244 keinen geeigneten Mechanismus, um einen EntityManager an beliebige Stellen der Java-Enterprise-Anwendung zu injizieren. Die Injektion erfolgt nur in verwaltete Instanzen wie Session-Beans, JSF-Managed-Beans, Sevlets oder Filter. Möchte man sich das Weitereichen des EntityManagers ersparen, so muss man die Injektion in den lokalen JNDI-Baum wählen. Diese Variante besitzt allerdings auch einen Schönheitsfehler: Auch hier ist der Registrierungsvorgang an verwaltete Instanzen des Application-Servers gebunden - in diesem Fall Session-Beans, Entity-Bean, Interceptoren und Message-Driven-Beans. Diese Klassen müssen mit einer @PersistenceContext-Annotation versehen werden, damit eine Registrierung der EntityManagerFactory erfolgt. Da man selten weiß, welche Enterprise-Bean als erste nach dem Deployment der Anwendung angesprochen wird und somit die Registrierung auslöst, müssen ausnahmslos alle Enterprise-Beans mit der gleichen @PersistenceContext-Annotation bedacht werden.

@PersistenceContext(name = "persistence/my-persistence-unit",
                    unitName = "my-persistence-unit")
@Stateless(mappedName = "ejb/MySessionBean")
public class MySessionBeanImpl implements MySessionBean {...}

Die Implementierung der PersistenceSessionFactory ist damit eigentlich fast leer; sie muss nur den lokalen JNDI-Baum prüfen, ob er gegebenenfalls einen EntityManager zur Verfügung stellen könnte.

import javax.naming.InitialContext;
import javax.persistence.EntityManagerFactory;

public class ManagedJPAPersistenceService extends PersistenceService {

  @Override
  protected PersistenceSessionFactory providePersistenceSessionFactory(String name) {
    try {
      InitialContext ic = new InitialContext();
      Object testEM = ic.lookup("java:comp/env/persistence/" + name);
      if (testEM != null) {
        return new JPAEJBPersistenceSessionFactory(name);
      }
    } catch (Exception e) {
      //TODO some logging
    }
    return null;
  }

}

Zu beachten ist hier, dass ein voreiliges Beziehen der PersistenceSessionFactory eine interne NameNotFoundException auslösen kann. Die Initialisierung kann beispielsweise nicht innerhalb von Application-Lifecycle-Listenern erfolgen. Nicht-JPA Mapping-Frameworks bieten in der Regel die Möglichkeit, sich selbst im globalen JNDI-Baum zu registrieren. Damit entfällt der hier skizzierte Eiertanz.

Die nächste Wrapper-Komponente ist die PersistenceSessionFactory. Der Application-Server-Fall wurde bereits diskutiert. Die Implementierung könnte dergestalt ausfallen:

public class ManagedJPAPersistenceSessionFactory implements PersistenceSessionFactory {

  private String persistenceUnit;

  public ManagedJPAPersistenceSessionFactory(String persistenceUnit) {
    this.persistenceUnit = persistenceUnit;
  }

  public PersistenceSession getCurrentEntityManager() {
    InitialContext ic;
    EntityManager delegate = null;
    try {
      ic = new InitialContext();
      delegate = (EntityManager) ic.lookup("java:comp/env/persistence/"
          + persistenceUnit);
    } catch (NamingException e) {
      //TODO some error handling
    }
    return new ManagedJPAPeristenceSession(delegate);
  }

}

Die Implementierung für den Two-Tier-Fall fällt etwas aufwendiger aus, da das Ressourcen-Management selbst erledigt werden muss. Ein häufig anzutreffendes Muster ist die Kopplung der Ressource an den aktuellen Thread über eine ThreadLocal-Variable. Die Implementierung könnte wie folgt aussehen:

public class JPAPersistenceSessionFactory implements PersistenceSessionFactory {

  private EntityManagerFactory emfDelegate;
  private ThreadLocal<EntityManager> currentEMs;

  public JPAPersistenceSessionFactory(EntityManagerFactory delegate) {
    this.emfDelegate = delegate;
  }

  public synchronized PersistenceSession getCurrentEntityManager() {
    EntityManager emDelegate = currentEMs.get();
    if (emDelegate == null || ! emDelegate.isOpen()) {
      emDelegate = emfDelegate.createEntityManager();
      currentEMs.set(emDelegate);
    }
    return new JPAPeristenceSession(emDelegate);
  }

}

Als nächste Wrapper-Komponente steht die PersistenceSession an. Dieses Interface ist eng an die letzte Wrapper-Komponente, die PersistenceTransaction, gekoppelt. Möchte man für (J)Unit-Tests im Two-Tier-Modus das Transaktionsverhalten des Application-Servers nachbilden, so bietet sich folgende Implementierung an:


public class JPAPersistenceSession implements PersistenceSession {

  private EntityManager emDelegate;
  private PersistenceTransaction currentTransaction;

  public JPAPeristenceSession(EntityManager delegate) {
    this.emDelegate = delegate;
  }

  public void persist(Object o) {
    emDelegate.persist(o);
  }

  //...

  public synchronized PersistenceTransaction getCurrentTransaction() {
    if (currentTransaction == null) {
      currentTransaction = new JPAPersistenceTransaction(emDelegate);
    }
    return currentTransaction;
  }

  public void close() {
    emDelegate.flush()
    //emDelegate will be closed by PersistenceTransaction#commit or #rollback
  }

}

Das wichtigste Merkmal ist die Unterdrückung des close()-Events. Ein JPA-EntityManager wird dann geschlossen, wenn die Transaktion committed oder zurückgerollt wird. Die dazugehörige Implementierung des PersistenceTransaction-Interfaces sähe so aus:

public class JPAPersistenceTransaction implements PersistenceTransaction {

  private EntityManager emDelegate;

  public JPAPersistenceTransaction(EntityManager emDelegate) {
    this.emDelegate = emDelegate;
  }

  public void begin() {
    emDelegate.getTransaction().begin();
  }

  public void commit() {
    EntityTransaction tx = emDelegate.getTransaction();
    try {
      tx.commit();
    }
    finally {
      if (tx.isActive()) {
        tx.rollback();
      }
      emDelegate.close();
    }
  }

  public void rollback() {
    try {
      emDelegate.getTransaction().rollback();
    }
    finally {
      emDelegate.close();
    }
  }

}

Für eine JUnit-Testklasse kann mit dieser Implementierung in den @Before- und @After-Methoden die Transaktionsklammer beispielsweise so gesetzt werden, dass letztendlich nur gegen den Session-Cache der Datenbank gearbeitet wird.

public class PersistenceLayerTest {

  private PersistenceSessionFactory persistenceSessionFactory;
  private PersistenceTransaction currentTransaction;

  @Before
  public void setUpPersistence() {
    persistenceSessionFactory = PersistenceService
        .createPersistenceSessionFactory("default");
    currentTransaction = persistenceSessionFactory
        .getCurrentPersistenceSession().getCurrentTransaction();
    currentTransaction.begin();
  }

  @After
  public void tearDownPersistence() {
    currentTransaction.rollback();
    persistenceSessionFactory.close();
  }

  @Test
  public void test1() {
    PersistenceSession session = persistenceSessionFactory
        .getCurrentPersistenceSession();
    Person p = new Person();
    p.setName("xyz")
    session.persist(p);
    session.close();
    //Asserts
     session = persistenceSessionFactory.getCurrentPersistenceSession();
    session.refresh(p);
    Assert.assertEquals("xyz", p.getName())
    session.close();
  }
}

Eine Implementierung der PersistenceSession für den Application-Server ist einfach, da dort das Transaktions-Handling außer Acht gelassen werden kann.

public class ManagedJPAPeristenceSession implements PersistenceSession {

  private EntityManager emDelegate;

  public ManagedJPAPeristenceSession(EntityManager delegate) {
    this.emDelegate = delegate;
  }

  public void persist(Object o) {
    emDelegate.persist(o);
  }

  public PersistenceTransaction getCurrentTransaction() {
    throw new IllegalStateException();
  }

  public void close() {
    emDelegate.flush(); //and ignore
  }

}

Das neue Datenbankmapping

Die Umstellung des Datenbankmappings ist in der Regel der aufwändigste Teil der Migration. Der Grund hierfür liegt nicht in der Technologie selbst, sondern wird durch die schiere Menge an zu migrierenden Klassen bedingt. Es bieten sich prinzipiell drei Strategien für die Umstellung an:

  1. Beibehaltung der Klassen und Ergänzung von JPA-Annotationen
  2. Beibehaltung der Klassen, Definition des Mappings in einer externen Mapping-Beschreibung (orm.xml)
  3. Generierung der neuen Mappings aus einem Klassenstrukturmodell

Die erste Variante ist minimal invasiv, da Annotationen zwar eine Compile-Time-Abhängigkeit bedingen, aber keine Runtime-Abhängigkeit. Annotierte Klassen können auch dann geladen werden, wenn die Annotationsklassen nicht im Classpath zu finden sind. Damit können die Annotationen im „lebenden Projekt“ angebracht werden, ohne Seiteneffekte auszulösen.

Die zweite Variante bedingt noch weniger Störungen des laufenden Projekts, da die Mappings in einer externen Datei definiert werden. Diese Variante hat nur einen kleinen Schönheitsfehler. Die Schöpfer der ORM-Schemabeschreibung für JPA waren nicht der Meinung, Vendor-Extensions zuzulassen. Damit ist man innerhalb einer orm.xml-Datei auf genau die Mappings beschränkt, die die Spezifikation definiert. Und JPA macht einige gravierende Auslassungen.

Die dritte Variante wurde bereits eingangs angerissen. Es scheint zunächst sehr aufwendig, das Datenmodell als echtes Modell nachzumodellieren, um dann wieder Java-Beans samt Mapping-Beschreibung daraus zu generieren. Dies hat jedoch mehrere Vorteile:

  • Das Datenmodell kann Java-neutral formuliert werden.
  • Das Datenmodell kann auch von jemand gepflegt werden, der kein O/R-Experte ist. Ebenso sind tiefgehende Java/JEE-Kenntnisse hierfür nicht zwingend.
  • Datenklassen enthalten keine Business-Logik, stattdessen folgen sie eigenen Strukturmustern (z.B. Bidirektionalität).
  • Datenklassen sind aufwendig zu pflegen, insbesondere bei Verwendung eines DAO-Ansatzes.
  • Fehler in Datenklassen werden nur selten entdeckt; immerhin ist es sehr aufwendig, jedes einzelne Attribut in Unit-Test aufzunehmen.
  • Die generierten Klassen sind absolut erwartungskonform. Sollte bei der Generierung ein Fehler entstehen, so ist dieser Fehler überall zu finden und kann somit schnell erkannt werden.
  • Das Datenmodell ist niemals wirklich fest. Oft ergeben sich noch kleine Änderungen in letzter Sekunde mit unbestimmbaren Konsequenzen für die Anwendungsqualität.
  • Nicht alle JDO-Merkmale, insbesondere die versteckte Datastore-Identität, lassen sich mit JPA-Mitteln ausdrücken. Dies führt ohne Code-Generierung zu unnötigen Kompromissen.

Nutzt man einen generativen Ansatz für das Datenmodell, so lassen sich die folgenden Artefakte sinnvoll generieren:

  • die Beanklassen,
  • Feld-Validatoren,
  • die Datenbank-Mappings,
  • das Datenbank-Schema,
  • Metaklassen mit Reflection-Informationen (z.B. die Spaltenpräzision in der Datenbank) und Reflection-Methoden (newInstance(), set(), get()),
  • Dokumentation / UML-Modell.

Für Datenmodelle kann es sinnvoll sein, eine textuelle Repräsentation des Modells zu wählen. So bleibt man werkzeugunabhängig und kann die üblichen Text-basierten Code-Verwaltungssysteme zur Versionierung verwenden. Ein Modell könnte wie folgt aussehen:

complement AenderungsInformationen {
 attribute aenderer : SystemName;
 attribute aenderungszeitpunkt : Timestamp;
} 

data-type Akteur is-abstract, uses-superclasstable-inheritance, has-hidden-identity {
 complement : AenderungsInformationen;
} 

data-type Person {
 superclass : Akteur;
 attribute name : String is-indexed;
 attribute alter : Number;
 association adresse : Adresse;
 association projekte : Set<Projekt> is-joined-by-table;
 association chef : TeamLeiter is-non-empty, is-inverse-of(teamMitglieder);
 association projektHistorie : List<Projekt> is-ordered-by(endDatum DESC, startDatum DESC);
 association vorgänger : Person is-unidirectional, is-joined-by-column;
} 

data-type TeamLeiter {
 superclass : Akteur;
 association teamMitglieder : Set<Person> is-non-empty, is-joined-by-table;
} 

embeddable-type Address {
  ....
}

Die Formulierung des Metamodells zu einem solchen Modell sprengt den Umfang dieses Artikels Es wird allerdings den Gegenstand einer der folgenden Artikel bilden.

Fazit

Die Migration einer bestehenden Anwendung auf JDO-Basis muss kein unkalkulierbares Risiko darstellen. Der Artikel konnte zeigen, wie nah die Semantik zwischen JDO-Interfaces und JPA-Interfaces sein kann, folglich durch wohlüberlegte Refaktorierung eine bestehende Anwendung schrittweise und reversibel überführbar ist. Im Zuge der Migration ist es sinnvoll, die Struktur der Datenklassen in ein externes Modell auszulagern. So können durch Code-Generierung die Datenbank-Mappings spezifisch für das jeweilige O/R-Framework erzeugt werden. Eine Beschreibung des Datenmodells mithilfe einer textuellen DSL wird der Gegenstand eines zukünftigen Artikels sein.

Referenzen:

[1] http://jcp.org/en/jsr/detail?id=243, Final Release, S. 215,
Kapitel 18 (XML Metadata)
[2] http://jcp.org/en/jsr/detail?id=243, Maintenance Release 2.1, S. 273, Kapitel 20 (Java Persistence API (JSRs 220, 317) Alignment)
[3] http://www.jpox.org
[4] http://www.datanucleus.org/
[5] http://openjpa.markmail.org/search/?q=from%3A%22Craig+L+Russell%22
[6] Robin M. Roos: Java Data Objects. London et al.; Addison-Wesley. 2003.
S. 196 ff.
[7] http://www.orientechnologies.com/cms/
[8] http://www.xcalia.com/xdn/docs/docs.jsp
[9] http://forum.signsoft.com/
[10] http://static.springframework.org/spring/docs/2.5.x/reference/index.html
[11] http://jcp.org/en/jsr/detail?id=244, Final Release, S. 100 ff.
Abschnitt EE.5.12

Sie können diesen Artikel als PDF herunterladen.