| Titel | Inhalt | Suchen | Index | DOC | Handbuch der Java-Programmierung, 7. Auflage |
| << | < | > | >> | API | Kapitel 47 - Objektorientierte Persistenz |
Die voranstehenden Abschnitte haben gezeigt, wie man einfache Tabellen der Datenbank mit Hilfe von Java Beans abbilden und Datensätze über den EntityManager anlegen, manipulieren und löschen kann. Dabei wurde die Tabelle eins zu eins als Java-Objekt abgebildet, ohne auf die objektorientierte Struktur der verknüpften Datensätze einzugehen.
Das E/R-Diagramm unserer kleinen Datenbank aus Abschnitt 44.3 zeigt allerdings nicht nur eine, sondern zwei Tabellen, die miteinander verknüpft sind:
Abbildung 47.2: E/R-Diagramm für DirDB
Ein Verzeichnis besitzt zunächst nur eine ID did und einen Namen dname. Der in Listing 47.2 modellierte Schlüssel fatherdid verweist dagegen auf einen übergeordneten Datensatz, also ein Elternverzeichnis, und statt lediglich die Anzahl der Verzeichniseinträge entries abzubilden, wäre es schön, wenn wir gleich Zugriff auf die entsprechenden Objekte hätten. Wie dies mit der JPA realisiert werden kann, wollen wir uns im zweiten Teil dieses Kapitels ansehen.
Zunächst wollen wir uns dem Abbilden von Datenbanktabellen noch einmal von der objektorientierten Herangehensweise annähern. Dabei besteht ein Verzeichnis-Objekt zunächst einmal aus einem Namen und einer ID. Während der Name vom Anwender selbst vergeben werden muss und damit ein Pflichtattribut darstellt, handelt es sich bei der ID um einen technischen Schlüssel in der Datenbank, den der Anwender zwar auslesen, aber nicht einfach ändern kann. Das folgende Listing zeigt die Java Bean im Ausgangsstadium:
Die Klasse Verzeichnis bildet zunächst nur die beiden Attribute id in Zeile 012 und name in Zeile 013 ab. Die Namen der Attribute orientieren sich an den Java-Konventionen und wir achten darauf, dass der öffentliche Konstruktor das Pflichtfeld name übernimmt, so dass kein namenloses Verzeichnis erstellt werden kann.
Der parameterlose Konstruktor in Zeile 018 ist dem Persistenz-Framework geschuldet, das zwingend einen Standardkonstruktor benötigt. Um dessen Aufruf zu erschweren, schränken wir die Sichtbarkeit mit Hilfe des Modifiers protected ein. Da die ID des Datensatzes ausnahmslos von der Persistenzschicht verwaltet werden soll, ist in Zeile 036 auch der Zugriff auf dieses Attribut eingeschränkt.
Eine weitere Neuerung gegenüber Listing 47.2 findet sich schließlich in der Verwendung des Objekttyps Integer statt des Basistyps int für die ID des Datensatzes. Der Objekttyp hat gegenüber den Basistypen den Vorteil, dass er den Wert null annehmen und damit einen nicht definierten Zustand abbilden kann. Das ist beispielsweise dann der Fall, wenn die Instanz zwar über einen Konstruktor erzeugt, aber noch nicht in der Datenbank gespeichert wurde.
Abschließend wird die Klasse Verzeichnis um die Methoden equals und hashCode aus Abschnitt 9.1.2 erweitert, um die Identität eines Datensatzes überprüfen zu können. Diese Methoden stellen sicher, dass Hibernate einen in der Datenbank eindeutig referenzierten Datensatz über dessen ID auch javaseitig identifizieren und so beispielsweise das doppelte Laden einer logisch identischen Instanz vermeiden kann.
Als Nächstes versehen wir die Java Bean mit den für die Persistenzschicht notwendigen Metainformationen und verknüpfen sie so mit der im Hintergrund arbeitenden Datenbank. Als Alternative zu Listing 47.2 werden wir die Annotationen diesmal direkt an den Attributen, statt an den zugehörigen Getter-Methoden anbringen:
Die Annotationen @Entity, @Table, @Id und Column wurden ja bereits mit Listing 47.2 eingeführt. Neu hinzugekommen ist @GeneratedValue in Zeile 015, um der Persistenzschicht anzuzeigen, dass der Wert des Attributs automatisch erzeugt und nicht vom Benutzer gesetzt werden soll.
Analog zur Klasse Verzeichnis können wir nun auch die Klasse Datei mit dem Pflichtattribut name definieren. Dabei bilden wir der Einfachheit halber das Änderungsdatum einer Datei über ein einzelnes Attribut mit Namen date ab:
001 /* Datei.java */
002
003 import javax.persistence.*;
004 import java.util.Date;
005
006 /**
007 * Diese Klasse repräsentiert die Tabelle 'file' der 'DirDB'
008 * Jede Instanz der Klasse repräsentiert wiederum einen
009 * Datensatz
010 */
011
012 @Entity
013 @Table( name = "file" )
014 public class Datei
015 {
016 @Id
017 @GeneratedValue
018 @Column(name = "fid")
019 private Integer id;
020
021 @Column(name = "fname")
022 private String name;
023
024 @Column(name = "dsize")
025 private Integer size;
026
027 @Column(name = "fdate")
028 private Date date;
029
030 /**
031 * Geschützter Minimalkonstruktor zur Verwendung durch
032 * die Persistenzschicht
033 */
034 protected Datei() {
035 }
036
037 /**
038 * Öffentlicher Konstruktor zur Verwendung durch den Entwickler.
039 * Dieser Konstruktor definiert alle Pflichtfelder der JavaBean.
040 * @param name - Name der Datei
041 */
042 public Datei(String name)
043 {
044 this.name = name;
045 }
046
047 public Integer getId()
048 {
049 return id;
050 }
051
052 protected void setId(Integer id)
053 {
054 this.id = id;
055 }
056
057 public String getName()
058 {
059 return name;
060 }
061
062 public void setName(String name)
063 {
064 this.name = name;
065 }
066
067 public Integer getSize()
068 {
069 return size;
070 }
071
072 public void setSize(Integer size)
073 {
074 this.size = size;
075 }
076
077 public Date getDate()
078 {
079 return date;
080 }
081
082 public void setDate(Date date)
083 {
084 this.date = date;
085 }
086
087 public boolean equals(Object o)
088 {
089 if (this == o) return true;
090 if (o == null || getClass() != o.getClass()) return false;
091
092 Datei file = (File) o;
093 return !(id != null ? !id.equals(file.id) : file.id != null);
094 }
095
096 public int hashCode()
097 {
098 return id != null ? id.hashCode() : 0;
099 }
100
101 public String toString()
102 {
103 return "File[id:"+ id + ", name:" + name + "]";
104 }
105 }
|
Um auch das Datei-Objekt über den EntityManager der Persistenzschicht verwalten zu können, müssen wir die Klasse noch im Persistence Deskriptor (persistence.xml) angeben. Hierfür erweitern wir Listing 47.3 wie folgt (Zeile 015):
001 <?xml version="1.0" encoding="ISO-8859-1"?> 002 003 <!-- Persistenz Descriptor zur Konfiguration --> 004 <persistence> 005 006 <!-- Hinterlegen eines symbolischen Namens --> 007 <persistence-unit name="persistenceExample" 008 transaction-type="RESOURCE_LOCAL"> 009 010 <!-- Zu verwendende Implementierung --> 011 <provider>org.hibernate.ejb.HibernatePersistence</provider> 012 013 <!-- Persistierbare Klassen --> 014 <class>Verzeichnis</class> 015 <class>Datei</class> 016 017 <!-- Konfiguration der Hibernate Implementierung --> 018 <properties> 019 <!-- Name des intern verwendeten JDBC-Treibers --> 020 <property name="hibernate.connection.driver_class" 021 value="org.hsqldb.jdbcDriver"/> 022 023 <!-- URL der zu verwendenden Datenbank --> 024 <property name="hibernate.connection.url" 025 value="jdbc:hsqldb:hsqldbtest"/> 026 027 <!-- SQL-Dialect, den Hibernate verwenden soll --> 028 <property name="hibernate.dialect" 029 value="org.hibernate.dialect.HSQLDialect"/> 030 031 <!-- Benutzername und Passwort; Standardwerte der HSQLDB --> 032 <property name="hibernate.connection.username" value="SA"/> 033 <property name="hibernate.connection.password" value=""/> 034 035 <!-- Flag, ob Tabellen automatisch erzeugt werden sollen --> 036 <property name="hibernate.hbm2ddl.auto" value="create"/> 037 038 <!-- Flag, ob SQL-Statements ausgegeben werden sollen --> 039 <property name="hibernate.show_sql" value="true"/> 040 041 <!-- Flag, ob SQL-Statements formatiert werden sollen --> 042 <property name="hibernate.format_sql" value="true"/> 043 </properties> 044 </persistence-unit> 045 </persistence> |
persistence.xml.ext |
Das Speichern, Manipulieren und Löschen der Persistenz Beans erfolgt ganz analog zum ersten Beispiel in Listing 47.6. Der einzige Unterschied ist, dass wir die ID des Datensatzes nicht mehr selbst vorgeben, sondern von der Persistenzschicht generieren lassen.
001 /* Listing4713.java */
002
003 import javax.persistence.*;
004
005 public class Listing4713
006 {
007 public static void main(String[] args)
008 {
009 //Erzeugen einer EntityManagerFactory mit Hilfe des symbolischen
010 //Namens aus dem Persistenz Descriptor (persistence.xml)
011 EntityManagerFactory emf =
012 Persistence.createEntityManagerFactory("persistenceExample");
013
014 //Erzeugen eines EntityManagers für den Zugriff auf
015 //die Datenbank
016 EntityManager manager = emf.createEntityManager();
017
018 //Beginn einer neuen Transanktion
019 EntityTransaction tx = manager.getTransaction();
020 tx.begin();
021
022 //Erzeugen eines neuen Java-Objekts
023 Verzeichnis dir = new Verzeichnis("temp");
024
025 //Speichern des Java-Objekts mit Hilfe des EntityManagers
026 manager.persist(dir);
027
028 //Abschluss der Transaktion mit einem Commit
029 tx.commit();
030
031 // Ausgabe der Id des Datensatzes
032 System.out.println(dir.toString());
033
034 //Freigabe der Ressourcen des EntityManagers
035 manager.close();
036
037 //Schließen der EntityManagerFactory und Freigeben der
038 //belegten Ressourcen
039 emf.close();
040 }
041 }
|
Listing4713.java |
Das Verzeichnis-Objekt wird
in Zeile 023 nur
noch mit einem Namen initialisiert und erhält seine ID erst mit
dem Speichern in Zeile 026.
Die Ausgabeanweisung in Zeile 032
führt zu folgendem Ergebnis:
Directory[id:1, name:temp]
Relationale Datenbanken verknüpfen Datensätze unterschiedlicher Tabellen mit Hilfe von Fremdschlüsseln. In diesem Abschnitt wollen wir uns ansehen, wie die referentielle Integrität, also die fachliche Konsistenz der verschiedenen Fremdschlüsselbeziehungen der Datenbank, objektorientiert modelliert werden kann.
Solche Referenzen können in unterschiedlichen Kardinalitäten vorliegen:
| Kurzform | Name | Bedeutung |
| 1:1 | Eins-zu-Eins | Jeder Datensatz einer Tabelle ist höchstens einem Datensatz in der anderen Tabelle zugeordnet. |
| 1:N | Eins-zu-N | Dem Datensatz dieser Tabelle können mehrere Datensätze einer anderen Tabelle zugordnet sein. |
| N:1 | N-zu-Eins | Mehrere Datensätze dieser Tabelle können auf ein und denselben Datensatz einer anderen Tabelle verweisen. |
| M:N | M-zu-N | Der Datensatz kann von verschiedenen Datensätzen referenziert werden und gleichzeitig auf mehrere Datensätze verweisen. |
Tabelle 47.5: Kardinalitäten für Datenbankbeziehungen
In unserem Beispiel speichert jeder file-Datensatz die ID des zugehörigen dir-Datensatzes, um eindeutig anzuzeigen, zu welchem Verzeichnis die jeweilige Datei gehört. Zwischen Datei und Verzeichnis besteht also eine N:1-Beziehung, da mehrere Dateien zu genau einem Verzeichniseintrag gehören können. Aus Sicht des Verzeichnis-Objekts würde es sich umgekehrt um eine 1:N-Beziehung handeln.
Diese Referenzen lassen sich auch mit der Persistenzschicht abbilden, indem ein Objekt auf das oder die anderen Objekte verweist. Wir erweitern dazu das Verzeichnis-Objekt um eine Liste von Datei-Objekten, um die zum Verzeichnis gehörenden Dateien aufzunehmen:
Genau wie die Basisattribute einer Tabelle werden auch die referenzierten Datensätze über Annotationen mit den notwendigen Metainformationen verknüpft. Für eine 1:N-Relation genügt dabei die Angabe von @OneToMany in Zeile 024. Mit dem Attribut cascade wird das Persistenz-Framework angewiesen, bestimmte Datenbankoperationen auch auf die referenzierten Datensätze anzuwenden. Über den Parameter CascadeType.ALL in Listing 47.14 weisen wir das Framework beispielsweise an, alle Datenbankoperationen auf Verzeichnisebene auch auf die anhängenden Dateien anzuwenden.
Da die Dateiobjekte zu einem Verzeichnis jedoch nicht als unsortiertes java.util.Set, sondern als nach Namen geordnete Liste ausgelesen werden sollen, fügen wir in Zeile 025 noch eine zweite Annotation @OrderBy hinzu. Sie bewirkt, dass die Datensätze nach dem Namen der referenzierten Java Bean sortiert werden. Die Bezeichnung der Datenbankspalte, unter der der Name einer Datei abgespeichert wird, spielt hier keine Rolle, es zählt einzig der Name des Attributs in der Java Bean Datei.
Zu guter Letzt wollen wir verhindern, dass es bei einem Zugriff auf die Datei-Objekte eines Verzeichnis aufgrund einer nicht initialisierten Liste zu einer NullPointerException kommt. Deshalb erweitern wir den öffentlichen Konstruktor und initialisieren die Liste in Zeile 042. Bemerkenswert an dieser Stelle ist, dass wir den vom Persistenz-Framework verwendeten Minimalkonstruktor in Zeile 031 nicht entsprechend erweitern müssen: Auch wenn zu einem Verzeichnis keine Dateien existieren, wird das Persistenz-Framework refenzierende Collections stets leer initialisieren.
| Annotation | Beschreibung |
| @OneToMany | Modelliert eine 1:N-Relation |
| @ManyToOne | Modelliert eine N:1-Relation |
| @OneToOne | Modelliert eine 1:1-Relation |
| @ManyToMany | Modelliert eine M:N-Relation |
Tabelle 47.6: Annotationen zur Modellierung von Datenbankreferenzen
Alle Annotationen aus Tabelle 47.6 können über eine Reihe von Attributen konfiguriert werden:
| Attribut | Beschreibung |
| cascade | Welche Datenbankoperationen sollen auch auf das referenzierte Objekt angewendet werden? |
| fetch | Wann sollen die referenzierten Datensätze geladen werden? FetchType.EAGER lädt die Datensätze sofort, FetchType.LAZY lädt die Datensätze nur bei Bedarf nach. |
| mappedBy | Name des Attributs im referenzierten Datensatz, das die inverse Relation abbildet. Wird nur benötigt, wenn mehrere Rückreferenzen in Frage kommen. |
| targetEntity | Typ des referenzierten Datensatzes. Wird nur benötigt, wenn dies nicht aus der typisierten Liste hervorgeht. |
Tabelle 47.7: Attribute der Annotationen für Datenbankreferenzen
Wir können auch die inverse Relation von einer Datei auf das zugehörige Verzeichnis abbilden, also eine @ManyToOne-Beziehung:
001 /* Datei.java */
002
003 import javax.persistence.*;
004 import java.util.Date;
005
006 /**
007 * Diese Klasse repräsentiert die Tabelle 'file' der 'DirDB'
008 * Jede Instanz der Klasse repräsentiert wiederum einen
009 * Datensatz
010 */
011
012 @Entity
013 @Table( name = "file" )
014 public class Datei
015 {
016 @Id
017 @GeneratedValue
018 @Column(name = "fid")
019 private Integer id;
020
021 @Column(name = "fname")
022 private String name;
023
024 @Column(name = "dsize")
025 private Integer size;
026
027 @Column(name = "fdate")
028 private Date date;
029
030 @ManyToOne
031 @JoinColumn(name = "did")
032 private Verzeichnis directory;
033
034 /**
035 * Geschützter Minimalkonstruktor zur Verwendung von Hibernate
036 */
037 protected Datei() {
038 }
039
040 /**
041 * Öffentlicher Konstruktor zur Verwendung durch den Entwickler.
042 * Dieser Konstruktor definiert alle Pflichtfelder der JavaBean.
043 * @param name - Name der Datei
044 */
045 public Datei(String name)
046 {
047 this.name = name;
048 }
049
050 public Integer getId()
051 {
052 return id;
053 }
054
055 protected void setId(Integer id)
056 {
057 this.id = id;
058 }
059
060 public String getName()
061 {
062 return name;
063 }
064
065 public void setName(String name)
066 {
067 this.name = name;
068 }
069
070 public Integer getSize()
071 {
072 return size;
073 }
074
075 public void setSize(Integer size)
076 {
077 this.size = size;
078 }
079
080 public Date getDate()
081 {
082 return date;
083 }
084
085 public void setDate(Date date)
086 {
087 this.date = date;
088 }
089
090 public Verzeichnis getDirectory()
091 {
092 return directory;
093 }
094
095 public void setDirectory(Verzeichnis directory)
096 {
097 this.directory = directory;
098 }
099
100 public boolean equals(Object o)
101 {
102 if (this == o) return true;
103 if (o == null || getClass() != o.getClass()) return false;
104
105 Datei file = (Datei) o;
106 return !(id != null ? !id.equals(file.id) : file.id != null);
107 }
108
109 public int hashCode()
110 {
111 return id != null ? id.hashCode() : 0;
112 }
113
114 public String toString()
115 {
116 return "File[id:"+ id + ", name:" + name + "]";
117 }
118 }
|
Datei.java |
Nun sind die beiden Persistenz Beans Verzeichnis und Datei miteinander verknüpft und referenzieren sich in beide Richtungen. Aus Sicht der Java-Objekte können wir jetzt auf einfache Weise die Dateien eines Verzeichnisses ausgeben oder das Elternverzeichnis einer Datei ermitteln. Dank der Annotationen des Persistenz-Frameworks können wir uns dabei ganz auf die objektorientierte Sichtweise konzentrieren und die Realisierung mittels Fremdschlüsseln vollständig vergessen.
Das folgende Listing zeigt, wie die miteinander verknüpften Datensätze mit Hilfe des EntityManager gespeichert werden können. Hierbei machen wir uns das Attribut cascade aus Listing 47.14 zu nutze, das den Aufruf von persist auch auf die referenzierten Datei-Objekte anwendet.
001 /* Listing4716.java */
002
003 import javax.persistence.*;
004
005 public class Listing4716
006 {
007 public static void main(String[] args)
008 {
009 //Erzeugen einer EntityManagerFactory mit Hilfe des symbolischen
010 //Namens aus dem Persistenz Descriptor (persistence.xml)
011 EntityManagerFactory emf =
012 Persistence.createEntityManagerFactory("persistenceExample");
013
014 //Erzeugen eines EntityManagers für den Zugriff auf
015 //die Datenbank
016 EntityManager manager = emf.createEntityManager();
017
018 //Beginn einer neuen Transanktion
019 EntityTransaction tx = manager.getTransaction();
020 tx.begin();
021
022 //Erzeugen und Verknüpfen der Java-Objekte
023 Verzeichnis dir = new Verzeichnis("temp");
024
025 Datei fileTest = new Datei("test.txt");
026 dir.getFiles().add(fileTest);
027 fileTest.setDirectory(dir);
028
029 Datei fileInfo = new Datei("info.txt");
030 dir.getFiles().add(fileInfo);
031 fileInfo.setDirectory(dir);
032
033 //Speichern des Verzeichnisses und der anhängenden Objekte
034 manager.persist(dir);
035
036 //Abschluss der Transaktion mit einem Commit
037 tx.commit();
038
039 //Freigabe der Ressourcen des EntityManagers
040 manager.close();
041
042 //Schließen der EntityManagerFactory und Freigeben der
043 //belegten Ressourcen
044 emf.close();
045 }
046 }
|
Listing4716.java |
| Titel | Inhalt | Suchen | Index | DOC | Handbuch der Java-Programmierung, 7. Auflage, Addison Wesley, Version 7.0 |
| << | < | > | >> | API | © 1998, 2011 Guido Krüger & Heiko Hansen, http://www.javabuch.de |