
| 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. |
Weiterführende Links
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. |
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.
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.
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.
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.
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.
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. |
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.
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.
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).