Home ORDIX AG             Dienstleistung             Trainingsshop    Kunden / Referenzen Aktuelles    Kontakt
Home  Pfeil  ORDIX News  Pfeil  3/2007  Pfeil  Java/J2EE/JEE
suche: 
Dieser Artikel richtet sich an Entwickler, die einen Überblick über die neuen Möglichkeiten von Vererbung und Polymorphie mit der Java Persistence API erhalten möchten.

Glossar

Java SE/JSE
Java Standard Edition
Java EE/JEE
Java Enterprise Edition. Erweiterung von Java für Server-Anwendungen.
POJO
Plain Old Java Object. Dabei handelt es sich um ein normales Objekt in der Programmiersprache Java.
Annotations
Anmerkungen im Java Source Code, die zur Übersetzungs- oder zur Laufzeit des Programms ausgewertet werden können. Annotations sind seit der Version 5 Bestandteil von Java.
EJB
Enterprise Java Beans sind standardisierte Komponenten, aus denen JEE-konforme Anwendungen erstellt werden, die auf einem JEE Application Server laufen. Für unterschiedliche Zwecke definiert der JEE-Standard verschiedene Arten von EJBs: Session Beans, Entity Beans und Message-driven Beans.
Java Persistence API
Die JPA ist Standard für die Persistenzabbildung im Java EE- und Java SE-Bereich. Ein wichtiges Merkmal der Java Persistence API ist, dass sie für beliebige POJOs verwendet werden kann und nicht auf EJBs beschränkt ist. Somit steht sie auch den Entwicklern zur Verfügung, die keine EJB-Komponenten in ihren Applikationen benötigen.
Java Persistence Query Language (JP-QL)
Die Java Persistence Query Language ist die Abfragesprache der Enterprise JavaBeans Komponentenarchitektur. Sie ist angelehnt an SQL, fokussiert aber Objekte. Die Java Persistence Query Language ist im Vergleich zur EJB QL erheblich erweitert und teilweise neu entwickelt worden. Viele grundlegende und wichtige Funktionalitäten, die in EJB QL nicht vorhanden waren, stehen mit der Java Persistence Query Language nun zur Verfügung. Sie hat einen wesentlich größeren Umfang an (wichtigen) Funktionalitäten und ähnelt sehr stark der Hibernate HQL-Abfragesprache.
Diskriminator
Er wird bei der Vererbung verwendet. Er ist das Entscheidungsmerkmal in den Daten, nach dem in die entsprechende Unterklasse abgeleitet wird.
Transient
Gegenteil von persistent. Persistenz bezeichnet die Fähigkeit, Daten (oder Objekte) in nicht-flüchtigen Speichermedien wie Dateisystemen oder Datenbanken zu speichern. Daten, die diese Fähigkeit nicht besitzen, existieren nur im Hauptspeicher des Computers und gehen verloren, sobald das Programm endet, von dem sie angelegt wurden. Solche „flüchtigen" Daten werden transient genannt.


Reihe EJB 3.0 (Teil IV)

Endlich Vererben!


Vererbungsstrategien und Polymorphie sind Grundlagen der objektorientierten Programmierung. Die Prinzipien der Vererbung und Polymorphie waren bisher für Entity Beans nicht vorgesehen bzw. deren Mechanismen waren in der EJB-Spezifikation nicht definiert. Dieses Manko wird nun mit der EJB 3.0-Spezifikation und der darin definierten Java Persistence API (JPA) beseitigt.

Im 3. Teil der EJB 3.0-Serie wurde die neue Java Persistence API vorgestellt. In diesem Teil gehen wir nun näher auf die neuen Möglichkeiten der Vererbung und Polymorphie von Entities ein. In der vorherigen Spezifikation von EJBs war die Vererbung von Entity Beans rein technisch nicht vorgesehen bzw. deren Mechanismen waren in der EJB-Spezifikation nicht definiert. Mit der EJB 3.0-Spezifikation und der Java Persistence API sind Entities nun in Form von POJOs implementiert und verfügen jetzt über die Fähigkeit der Vererbung und Polymorphie.

In der Spezifikation sind drei Strategien zur Abbildung von Vererbungshierarchien auf relationale Datenbankschemata definiert:

Im Folgenden wollen wir nun auf die verschiedenen Strategien eingehen. Für die Beispiele wird die Vererbungshierarchie aus Abbildung 1 verwendet.

Abb. 1: Vererbungshierarchie der folgenden Beispiele.

Eine Tabelle pro Hierarchie (SINGLE_TABLE)

Bei dieser Strategie werden alle Klassen einer Vererbungshierarchie auf eine einzige Tabelle in der Datenbank abgebildet. Es werden also alle Informationen aller Klassen in denormalisierter Form gespeichert. Deklariert wird diese Strategie durch die Annotation @Inheritance in der Basisklasse.

Da die Daten aller beteiligten Klassen in derselben Tabelle gespeichert werden, benötigt man hier ein Unterscheidungsmerkmal. Zur Identifizierung des konkreten Typs wird daher eine so genannte Diskriminatorspalte in der Tabelle hinzugefügt. Der Wert in dieser Spalte identifiziert den konkreten Typ der Subklasse. Dies erfolgt über die Annotation @DiscriminatorColumn in der Basisklasse und in den jeweiligen Subklassen über @DiscriminatorValue("Girokonto") bzw. @DiscriminatorValue("Kredit"). Den Aufbau der Tabelle können Sie in der Abbildung 2 sehen, den dazugehörigen Quellcode in Abbildung 3.

Abb. 2: Beispieltabelle für die Strategie SINGLE_TABLE.

@Entity
@Table(name="KONTO")

@Inheritance(strategy= InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name=”TYPE”)
@DiscriminatorValue(”Konto”)

public class Konto {
   private Integer id;
   private Integer kontonr;
   private String inhaber;
   ...
}


@Entity

@DiscriminatorValue(”Giro”)

public class Girokonto extends Konto {
   private Float zinsen;
   private Long dispo;
   ...
}
@Entity

@DiscriminatorValue(”Kredit”)

public class Kreditkonto extends Konto {
   private Integer laufzeit;
   private Long rate;
   ...
}
Abb. 3: Beispielklassen für die Strategie SINGLE_TABLE.

Lässt man @DiscriminatorColumn und @DiscriminatorValue als Annotationen weg, so wird als Spalte in der Datenbank dtype verwendet und als Diskriminatorwerte die jeweiligen unqualifizierten Klassennamen. Alle Attribute der Subklassen müssen in der Tabelle „nullable" sein, denn in einer Subklasse sind insbesondere die direkten Attribute der anderen Subklassen zwangsläufig nicht gefüllt.

Diese Strategie unterstützt polymorphe Abfragen mit der Java Persistence Query Language (JP-QL). Sie ist relativ leicht zu implementieren, denn in der Datenbank wird nur eine Tabelle benötigt. Da nur ein Datenbankzugriff erfolgt, ist diese Strategie sehr schnell. Sie steht allerdings im Gegensatz zur Normalisierung, welche in relationalen Datenbanken angestrebt wird.

Eine Tabelle pro konkreter Klasse (TABLE_PER_CLASS)

Bei dieser Strategie wird jede Klasse einer Vererbungshierarchie auf eine separate Tabelle abgebildet. Jede dieser Tabellen enthält alle Daten einer Klasse inklusive ihrer geerbten Attribute. Deklariert wird diese Strategie durch die Annotation @Inheritance (strategy=InheritanceType.TABLE_ PER_CLASS) in der Basisklasse. Den Aufbau der Tabellen sehen Sie in der Abbildung 4, den dazugehörigen Quellcode in Abbildung 5.

Abb. 4: Beispieltabellen für die Strategie TABLE_PER_CLASS.

@Entity
@Table(name="KONTO")

@Inheritance(strategy= InheritanceType.TABLE_PER_CLASS)

public class Konto {
   private Integer id;
   private Integer kontonr;
   private String inhaber;
   ...
}


@Entity
public class Girokonto extends Konto {
   private Float zinsen;
   private Long dispo;
   ...
}


@Entity

@Table(name="KREDIT")

public class Kreditkonto extends Konto {
   private Integer laufzeit;
   private Long rate;
   ...
}
Abb. 5: Beispielklassen für die Strategie TABLE_PER_CLASS.

Nicht polymorphe Abfragen sind auch bei dieser Strategie sehr schnell, da nur ein Datenbankzugriff benötigt wird. Allerdings sind im Standard keine polymorphen Abfragen vorgesehen. Einzelne Implementierungen, wie z. B. Hibernate, ermöglichen diese dennoch.

Jede Tabelle in der Datenbank entspricht genau einer Klasse. Die Attribute müssen daher in dieser Strategie nicht „nullable" sein. So können also Constraints auf Spalten der Subklassen definiert werden. Auch diese Strategie erzeugt Redundanzen auf der Datenbankseite und widerspricht somit dem Normalisierungsgedanken. Ferner erfordern Änderungen an der Basisklasse Änderungen in allen Tabellen der Subklassen.

Eine Tabelle pro Subklasse (JOINED)

Auch bei dieser Strategie wird jede Klasse einer Vererbungshierarchie auf eine separate Tabelle abgebildet. Allerdings enthält die Tabelle der Subklasse nur die spezifischen Attribute der jeweiligen Subklasse, nicht aber die geerbten Attribute. Diese werden in der Tabelle der Basisklasse gespeichert. Deklariert wird diese Strategie durch die Annotation @Inheritance (strategy=InheritanceType.JOINED) in der Basisklasse. Den Aufbau der Tabellen zeigt Abbildung 6, den dazugehörigen Quellcode Abbildung 7.

Abb. 6: Beispieltabellen für die Strategie JOINED.

@Entity
@Table(name="KONTO")

@Inheritance(strategy= InheritanceType.JOINED)

public class Konto {
   private Integer id;
   private Integer kontonr;
   private String inhaber;
...
}


@Entity
public class Girokonto extends Konto {
   private Float zinsen;
   private Long dispo;
   ...
}


@Entity

@PrimaryKeyJoinColumn(name="KID")

public class Kreditkonto extends Konto {
   private Integer laufzeit;
   private Long rate;
   ...
}
Abb. 7: Beispielklassen für die Strategie JOINED.

Diese Strategie entspricht auf der Datenbankseite der Normalisierung von Tabellen (dritte Normalform). Es sind polymorphe Abfragen mit JP-QL möglich. Punktuelle Änderungen im Konzept erfordern bei dieser Strategie auch nur punktuelle Änderungen in den Tabellen. Allerdings sind Abfragen langsamer als bei den anderen Strategien. Daher besteht bei tiefgehenden Objektbäumen die Gefahr der Inperformanz.

Mapped Superclass

Eine Mapped Superclass wird benutzt, wenn eine Basisklasse keine Entity sein soll, aber Attribute für abgeleitete Klassen, die Entities sind, zur Verfügung stellen soll. Dies wird über die Annotation @MappedSuperclass in der Basisklasse erreicht. Die abgeleiteten Klassen übernehmen die Felder der Basisklasse, die nicht als „transient" deklariert sind. Die Mapped Superclass sollte abstrakt sein. Sie muss es aber nicht. Sie hat keine assoziierte Tabelle in der Datenbank. Man kann eine Mapped Superclass verwenden, wenn Klassen einige übereinstimmende Attribute enthalten, aber kein fachlicher Zusammenhang zwischen den Klassen besteht.

Klassen, die als @MappedSuperclass gekennzeichnet wurden, dürfen nicht als Argumente für Operationen des Entity Managers oder der Schnittstelle Query verwendet werden. Das heißt, man kann keine polymorphen Abfragen über diese Basisklasse durchführen.

Transiente Klassen in der Vererbungshierarchie

Eine Entity-Klasse kann auch von einer Nicht-Entity-Klasse erben, auch wenn diese nicht durch die Annotation @MappedSuperclass gekennzeichnet ist. Alle Attribute dieser Basisklasse werden nicht in der Datenbank gespeichert. Sie sind also für den Persistenz-Provider transient. Die Attribute der Basisklasse werden also von diesem während des LifeCycles einer Entity ignoriert.

Polymorphe Abfragen

Mit der Java Persistence API sind nun polymorphe Abfragen möglich. In JP-QL kann man nach der Basisklasse fragen und erhält als Ergebnismenge Entitäten von allen Subklassen. Die Basisklasse muss bei diesen Abfragen mit @Entity annotiert sein. Sie darf eine konkrete aber auch eine abstrakte Klasse sein. Eine Mapped Superclass oder eine transiente Basisklasse sind dabei nicht zulässig. Anhand des Beispiels in den Abbildungen 8 und 9 kann man die Funktionsweise bei polymorphen Abfragen nachvollziehen.

Beim Aufruf der Methode alleKonten() aus Abbildung 9 könnte es zu folgender Ausgabe kommen:

Girokonto mit KontoNr. 4711
Girokonto mit KontoNr. 4712
Girokonto mit KontoNr. 4713
Kreditkonto mit KontoNr. 4714
Kreditkonto mit KontoNr. 4715

Es wird also die überschriebene Methode von getKontoTyp() der jeweiligen konkreten Subklasse verwendet.

@Entity
@Table(name="KONTO")
@Inheritance(strategy= InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name="TYPE")
@DiscriminatorValue("Konto")
public class Konto {
   ...

   public String toString() { return getKontoTyp()+" mit KontoNr. " + getKontoNr(); }
   public String getKontoTyp() { return "Konto"; }

}


@Entity
@DiscriminatorValue("Giro")
public class Girokonto extends Konto {
   ...

   public String getKontoTyp() { return "Girokonto"; }

}


@Entity
@DiscriminatorValue("Kredit")
public class Kreditkonto extends Konto {

   public String getKontoTyp() { return "Kreditkonto"; }

}
Abb. 8: Beispielklassen für polymorphe Abfragen.

public alleKonten() {

   Query q = entityManager.createQuery ("Select k from Konto k");
   List<Konto> konten = (List<Konto>) q.getResultList();

 
   for(Konto konto:konten) {
      System.out.println(konto);
   }
}
Abb. 9: Beispiel für polymorphe Abfragen.

Polymorphe Assoziationen

Die Java Persistence API erlaubt polymorphe Assoziationen. Die Basisklasse muss bei diesen Assoziationen mit @Entity annotiert sein. Es darf eine konkrete aber auch eine abstrakte Klasse sein. Eine Mapped Super Class oder eine transiente Basisklasse sind dabei nicht zulässig. Die Funktionsweise einer polymorphen Assoziation ist anhand der Abbildung 10 gut zu erkennen.

@Entity
@Table(name="KUNDE")
public class Kunde {
   Integer id;

   Collection<Konto> konten;

   ...
   @OneToMany(cascade={CascadeType.ALL})
   public Collection<Konto> getKonten() {
      return konten;
   }

   public void addKonto(Konto konto) {
      konten.add(konto);
   }
}
Abb. 10: Ein Beispiel für polymorphe Assoziationen.

Man kann also bei einer eins-zu-viele-Beziehung in einer Entity die Collection mit der Basisklasse definieren. Die Collection enthält dann aber Entities in den Ausprägungen der jeweiligen Subklassen.

Fallstricke

Bei der Verwendung von Vererbung und Polymorphie kann man über einige Fallstricke stolpern. Gegeben sei z. B. ein Kreditkartenkonto, das einem um einige Attribute erweiterten Direktkonto entspricht (siehe Abbildung 11).

@Entity
@Table(name="KONTO")
@Inheritance(strategy= InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name="TYPE")

@DiscriminatorValue("Direkt")
public class Direktkonto {

   private Integer id;
   private Integer kontonr;
   private Integer gegenkonto;
   ...
}

@Entity

@DiscriminatorValue("Karte")

public class Kreditkartenkonto extends Direktkonto {
   private Long kartennr;
   ...
}
Abb. 11: Beispiel für Fallstricke.

Von beiden Kontotypen gibt es konkrete Ausprägungen. Nun möchte man in einer Abfrage alle Direktkonten erhalten. Der Versuch über die Abfrage in Abbildung 12 scheitert, da ich durch den Mechanismus der Polymorphie als Ergebnis sowohl die Direktkonten als auch die Kreditkartenkonten erhalte. Die Lösung für dieses Problem wäre die Einführung einer Basisklasse (eventuell abstrakt), in der alle gemeinsamen Attribute stehen. Von dieser Basisklasse leitet man die beiden Klassen Direktkonto und Kreditkartenkonto ab. Nun kann man Abfragen auf die jeweiligen Subklassen durchführen und erhält das gewünschte Ergebnis.

Query q = entityManager.createQuery 
	("Select d from Direktkonto d");
List direktkonten = q.getResultList();
Abb. 12: Abfrageversuch über alle Direktkonten.

Eine weitere Möglichkeit Fehler zu machen, besteht in der Verwendung von einfachen Datentypen bei der Strategie SINGLE_TABLE. Bei dieser Strategie müssen die Spalten in der Tabelle als „nullable" definiert sein. Es kann also passieren, dass die Werte in diesen Spalten von anderen Programmen auf NULL gesetzt werden, da dies von der Datenbank nicht verhindert werden kann. Beim Instanziieren einer Entity durch den Persistenz-Provider kann es so zu einer Exception kommen.

Fazit

Vererbung und Polymorphie sind Eckpfeiler jeder objektorientierten Sprache. Umso bedauerlicher war es bisher, diese bei EJBs nicht verwenden zu können. Mit der Java Persistence API ist dieser Schwachpunkt nun endlich beseitigt. Dies ist ein großer Schritt nach vorne und hilft, neben all den anderen Verbesserungen im EJB 3.0-Standard, die Verwendung von JEE im Allgemeinen und Entity Beans im Besonderen in Projekten verstärkt einzusetzen.

Oliver Kaluza (info@ordix.de).