Java Persistence API © Arno Schmidhauser Letzte Revision: November 2006 Email: [email protected] Webseite: http://www.sws.bfh.ch/db Dieses Skript stützt sich wesentlich auf die Spezifikation JSR 220 Version 3.0 von Sun, Final Release, 2. Mai 2006 Inhalt I Wichtige Java Konstrukte II Übersicht Java Persistence API III Beziehungen zwischen Entities IV Spezielles V Objektverwaltung und Transaktionsmanagement VI JPA Query Language JPA – Java Persistence API Über diesen Kurs • Inhalt – Anwendung des Persistence API von EJB 3.0 • Voraussetzungen – Java 1.4, JDBC, SQL • Nicht Ziele – Einbettung der Persistenztechnologie in Container/AppServer ( Æ S. Fischli Business Tier) 2 Arno Schmidhauser November 2006 Seite 2 JPA – Java Persistence API I Wichtige Java Konstrukte 3 Java 5 hat einige wichtige Neuerungen. Drei davon, Enumerations, Annotations und Generics werden kurz vorgestellt. Diese Konstrukte sind für das JPA wichtig. Arno Schmidhauser November 2006 Seite 3 JPA – Java Persistence API Enumerations • Eine Aufzählung ist eine Klasse mit einer abschliessend definierten Menge von benannten Objekten. • Wichtige Teile einer Enumeration sind: – Der Name (die Klasse) der Aufzählung als Ganzes – Die Namen der einzelnen Aufzählungselemente – Eventuell der zugrundeliegende Wert (int, String usw.) • Eine Enumeration wird intern wie eine Klasse behandelt, und besitzt auch äusserlich die Eigenschaften einer Klasse: – Sie gehört zu einem Package – Sie kann public, protected, private sein – sie kann Attribute und Methoden besitzen 4 Enumerations ersetzen die bisher verwendeten Konstanten in Form statischer Klassen- oder Interface-Variablen. Enumerations sind in Java-API Dokumentation auf der gleichen Ebene wie Klassen, Interfaces, Exceptions und die noch zu besprechenen Annotations aufgeführt. Im Rahmen der JPA Spez. werden Enumeration innerhalb von Annotations verwendet, zur Beschreibung verschiedenster Optionen, beispielsweise CascadeType.PERSIST, CascadeType.REMOVE, LockModeType.READ, LockModeType.WRITE usw. In diesen Beispielen sind CascadeType und LockModeType die Aufzählungen, PERSIST, REMOVE, READ, WRITE die Elemente der Aufzählungen. Auch als Datentyp für persistente Daten kommen Enumerations in Frage, zum Beispiel für die Status-Beschreibung von Objekten. In der Datenbank können wahlweise der Name des jeweiligen Elementes einer Enumeration (in obigen Beispielen "PERSIST", "REMOVE" etc.) oder die dahinterliegenden Ordnungszahlen (0, 1 usw.) gespeichert werden. Die nachfolgende Anotation beim jeweiligen Klassenmember einer Entitiy wird dazu verwendet. @Enumerated(STRING) meineEnumerationMember oder @Enumerated(ORDINAL) meineEnumerationMember Arno Schmidhauser November 2006 Seite 4 JPA – Java Persistence API Enumerations, Beispiel 1 1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. public enum Status { OPEN, CLOSED } ... Status s1 = Status.OPEN; Status s2 = Status.CLOSED; System.out.println( s1.toString() ); System.out.println( s1.ordinal() ); if(s1 == s2) System.out.println("Beide gleich."); Status s3 = Status.valueOf( "OPEN" ); Status all[] = Status.values(); for( int i = 0; i < all.length; i++ ) { System.out.println( all[i].toString() ); } 5 Obiges Beispiel zeigt die Definition und den Gebrauch einer einfachen Enumeration: Zeile 1 definiert die Enumeration Status. Die Definition ist in einem File Status.java abzulegen. Zeile 3 und 4 erstellen je eine Variable (Objekt) dieser Enumeration. Zeile 5 liefert den String "OPEN" als Ausgabe. Zeile 6 liefert die Zahl 0 als Ausgabe. Für das Element CLOSED wäre die Ausgabe die Zahl 1. Zeile 7: Zwei Elemente sind gleich, wenn sie denselben Namen haben. Das folgend Stück Code liefert bei der Bedingung true als Ergebenis: Status s1 = Status.OPEN; Status s2 = Status.OPEN; if ( s1 == s2 ) // liefert true Zeile 8: Die Methode valueOf() ermöglicht das dynamische Erzeugen eines EnumElements auf Grund eines gleichnamigen Strings. Zeile 9: Die Methode values() liefert sämtliche Elemente der Enumeration. Arno Schmidhauser November 2006 Seite 5 JPA – Java Persistence API Enumerations, Beispiel 2 Eigene Attribute und Methoden 1. public enum Status { 2. OPEN( "offen" ), CLOSED( "geschlossen" ); 3. protected String zusatzWert; 4. Status( String zusatzWert) { 5. this. zusatzWert = zusatzWert; 6. } 7. public String getZusatzWert(){ 8. return this. zusatzWert; 9. } 10. } 6 Das Verhalten ist grundsätzlich gleich wie im vorhergehenden Beispiel. Ein Enumeration-Element kann im Code beispielsweise wie folgt deklariert werden: Status s = Status.OPEN Zusätzlich kann von s die Methode getZusatzWert() aufgerufen werden, wie bei jedem anderen Java-Objekt eine definierte Methode aufgerufen wird, z.B. System.out.println( s2.getZusatzWert() ); Der Konstruktor muss private oder unspezifiziert sein. Arno Schmidhauser November 2006 Seite 6 JPA – Java Persistence API Annotations • Annotations fügen dem Java-Code zusätzliche Informationen hinzu, die nicht ausgeführt, aber zur Laufzeit abgefragt werden können. • Annotations sind typsicher, sie werden vom Compiler geprüft. • Annotations gehören zu Paketen, Klassen, Konstruktoren, Membern, Parametern, zu Annotationen und lokalen Variablen. • Annotations werden intern als Interfaces behandelt. 7 Arno Schmidhauser November 2006 Seite 7 JPA – Java Persistence API Annotation, Beispiel für Definition 1. import java.lang.annotation.*; 2. 3. 4. 5. 6. 7. 8. 9. 10. @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Inherited @Documented @interface MyAnnotation { String was() default "Testen"; String[] wann(); Status status() default Status.OPEN; } 8 In diesem Beispiel wird eine Annotation definiert. Die Annotation ist einem File MyAnnotation.java abzulegen und zu kompilieren, wie Klassen, Interfaces, Exceptions und Enumerations. Zu 2: Das Target bestimmt, worauf die Annotation angewendet werden kann. In diesem Fall auf eine Klasse oder ein Interface (TYPE). Ist kein Target angegeben, ist jedes Target erlaubt. Zu 3: Die Retention (übersetzt etwa "Vorbehalt" oder "Halterung") bestimmt, ob die Annotation nur im Source-Code (SOURCE), zusätzlich im kompilierten ByteCode(CLASS), oder auch in der laufenden Applikation(RUNTIME). Default ist CLASS. Im JPA haben alle Annotations die Retention RUNTIME. Zu 4: Wenn @Inherited angegeben ist, gilt die Annotation auch für abgeleitete Klassen. Zu 5: Wenn @Documented angegeben ist, soll die Annotation in Dokumentation-Tools (javadoc) dokumentiert werden. Zu 6: Die eigentliche Annotation. Zu 7-9: Als Felder der Annotation können Strings, primitive Typen, Enumerations, andere Annotationen, oder Arrays von all diesen angegeben werden. Andere Klassen, ausser die Klasse Class, sind nicht erlaubt. Felder ohne Default müssen beim Gebrauch der Annotation angegeben werden, ansonsten tritt ein Compilerfehler auf. Im Rahmen der JPA Spez. sind Annotations mit komplexer Struktur durchaus üblich (z.B. @JoinTable). Eine Klasse kann auch ohne Weiteres mehrere vorangestellte Annotationen haben. Die Annotationen @Target, @Retention, @Inherited und @Documented heissen MetaAnnotationen, weil es Annotationen für Annotationen sind. Arno Schmidhauser November 2006 Seite 8 JPA – Java Persistence API Annotation, Beispiel für Anwendung 1. 2. 3. 4. 5. @MyAnnotation( was="Pruefen", wann={ "Anfang", "Ende" } ) public class MyAnnotatedClass { // ... } 9 Mit diesem Stück Code ist der Klasse MyAnnotatedClass eine Annotation des Typs MyAnnotation zugewiesen. Die gesamte Persistenz-Information im JPA wird via Annotations definiert. Der Vorteil liegt darin, dass die Angaben bereits zur Kompilationszeit typengeprüft werden können. Der Nachteil liegt darin, dass unter Umständen sehr viel Meta-Information im Source Code steht, insbesondere über das Mapping von Klassen-Feldern auf Datenbank-Felder. Damit kann der Source Code sehr unübersichtlich werden, respektive muss neu aufgebaut werden. Arno Schmidhauser November 2006 Seite 9 JPA – Java Persistence API Annotations abfragen 1. public class AnnotationsAbfragen { 2. public static void main( String args[] ) 3. throws Exception 4. { 5. Class c = Class.forName("MyAnnotatedClass"); 6. for (Annotation a : c.getAnnotations()) { 7. System.out.println( a.toString() ); 8. } 9. } 10. } 10 Mit der toString()-Methode erhält man eine textuelle Darstellung zurück: @pmq.MyAnnotation(status=OPEN, was=Pruefen, wann=[Anfang, Ende]) Die toString()-Methode ist für die Analyse von Annotationen meist zu ungenau. Man kann daher mit der Methode Annotation.annotationType() ein Objekt der Klasse Class abholen, und dieses dann mittels des Reflection API weiter analysieren. Arno Schmidhauser November 2006 Seite 10 JPA – Java Persistence API Generics • Bis Java 1.4: Object als generische Klasse. Beispiel: List list = new LinkedList(); list.add( new String( "foo" ) ); list.add( new Integer( 7 ) ); String s = (String) list.get(0) • Vorteil: übersichtlich und einfach. • Nachteile: – nicht typsicher, eine Collection kann fälschlicherweise Objekte verschiedener Klassen enthalten. – schwach dokumentiert, Klasse der Collection unklar. – Häufige Cast-Operation erforderlich. 11 Arno Schmidhauser November 2006 Seite 11 JPA – Java Persistence API Generics • Ab Java 5: Generische Klassen mit Angabe des konkreten Typs zum Zeitpunkt der Verwendung. Beispiel: List<String> list = new LinkedList<String>(); list.add( new String( "foo" ) ); list.add( new Integer( 7 ) ); // Compiler Fehler String s = list.get(0) // kein Cast nötig • Im Rahmen der Java Standard Edition treten Generics vorallem im Package java.util auf, z.B. List<T>, Set<T>, Map<K,V>, Comparator<T>, Iterator<T> usw. 12 Die alte Form kann nach wie vor verwendet werden. Der Compiler erzeugt jedoch eine Warnung. Die Angabe List<T> heisst generische Klasse Die Angabe T heisst Typvariable Die Angage String heisst Typargument Die Angabe List<String> heisst generischer Typ Arno Schmidhauser November 2006 Seite 12 JPA – Java Persistence API Eigene Generics, Definition public class Node<T> { protected T content; protected Node<T> parent; public Node( T content ) { this.content = content; } public T getContent() { return( this.content ); } public Node<T> getParent() { return parent; } public void setParent( Node<T> parent ) { this.parent = parent; } 13 Sehr oft hat eine generische Klasse nur einen Typparameter. Es ist aber durchaus möglich, mit mehreren zu arbeiten. Beispielsweise: public class Edge<K, T> { protected Node<T> left, right; protected K weight; public Edge( Node<T> left, Node<T> right, K weight ) { this.left = left; this.right = right; this.weight = weight; } } Weil K ein beliebiges Typargument aufnehmen kann, z.B. Double oder Integer, aber auch String, können keine spezifischen Methoden, wie etwa das Zusammenzählen von Kantengewichten implementiert werden. Dieses Problem lösen die Generics mit Wildcards (siehe weiter hinten). Generische Klassen können abgeleitet werden wie andere Klassen. Diese Situation ist beispielsweise bei Collections anzutreffen: TreeSet ist von AbstractSet, dieses von AbstractCollection abgeleitet. public class SpecialNode<T> extends Node<T> { public SpecialNode( T content ) { super( content ); } Für generische Interfaces gilt dasselbe wie für generische Klassen. Arno Schmidhauser November 2006 Seite 13 JPA – Java Persistence API Generics, Variablen // korrekt Node<Integer> ni = new Node<Integer>( 7 ); Node<Double> nd = new Node<Double>( 3.14 ); Node<String> ns = new Node<String>( "Heinz" ); System.out.println( ni.toString() ); // nicht korrekt, Kompilationsfehler: Node<Number> n = new Node<Integer>( 1 ); // geht auch nicht, Kompilationsfehler: Node<Number> n; n = (Node<Number>) new Node<Integer>( 1 ); 14 Ein Ziel der Generics ist die Möglichkeit einer strengen Typenprüfung zur Kompilationszeit. Werden generische Klassen, die mit unterschiedlichen Typparametern instanziert sind, als inkompatibel angesehen, ist diese strenge Typprüfung gewährleistet, und es können Laufzeitfehler, wie man sie von der Vererbung her kennt, vermieden werden. Typroblem bei der normalen Vererbung: Number ni = new Integer( 1 ); Number nd = (Double) ni; // Kompilation ok, aber dynamischer Fehler! Dieses Typproblem kann bei Generics nicht auftreten: Node<Number> nti = new Node<Integer>( 1 ); // Kompilations-Fehler! Node<Number> ntd = (Node<Double>) nti; Arno Schmidhauser November 2006 // Kompilations-Fehler! Seite 14 JPA – Java Persistence API Generics und Vererbung • Generics führen keine neue Art der Verbung ein! 1. Nicht erlaubt: List<Person> list1 = new LinkedList<Manager>(); 2. Erlaubt: List<Person> list1 = new LinkedList<Person>(); list1.add( new Person() ); list1.add( new Manager() ); 15 Der erste Fall betrifft die Kompatibilität der generischen Typen selbst. Hier gilt das im vorhergehenden Beispiel gesagte. Die Verwandtschaft von Typargumenten begründet keine Verwandtschaft der generischen Typen als Ganzes. Damit wird eine hohe Typsicherheit zum Kompilationszeitpunkt erreicht. Der zweite Fall betrifft die Kompatibilität in einer normalen Vererbungshierarchie. Hier will man natürlich nicht vom bisherigen Verhalten von Java abweichen. Ein Objekt der untypisierten Klasse Person kann jederzeit durch ein Objekt der abgeleiteten und untypisierten Klasse Manager vertreten werden. Arno Schmidhauser November 2006 Seite 15 JPA – Java Persistence API Generics, Wildcards 1. Wildcardvariable zur Aufnahme irgendeines Typs einer generischen Klasse: Node<?> x = new Node<Integer>(); 2. Wildcard, eingeschränkt auf Zahlen: Node<? extends Number> x; x = new Node<Integer>(); // ok x = new Node<String>(); // Fehler 3. Einschränkung des Typparameters einer generischen Klasse: public class NumberNode< T extends Number > { ... } 16 Im Fall 1 kann die Variable x nun einen beliebigen generischen Typ aufnehmen. Die Situation ist gegenüber der Deklaration Objekt o = new Node<Integer>(); anders, indem x nur Node-Typen aufnehmen kann. Im Fall 2 kann man die möglichen Zuweisungen an die Variable x weiter einschränken, indem x nur noch Nodes mit Zahlentypen entgegennehmen kann. Die Variable x kann gemäss Fall 1 und Fall 2 ein Objekt der Klasse NodeNumber entgegennehmen. Im Fall 3 wird der Typparameter einer generischen Klasse eingeschränkt (sogenannter Typebound). Beispielsweise kann man sagen, dass ein Node-Typ nur Sinn macht, wenn er eine Zahl enthält. In diesem Fall kann man in der Klassendefinition bereits darauf abstellen, dass das Typargument sicher alle Methoden der Klassen Number kennt, und man kann diese auch im Code verwenden. In diesem Beispiel stellt die Methode getDouble() darauf ab, dass als Typargument eine Zahl angegeben wird. public class NumberNode<T extends Number> { protected T aNumber; public double getDouble() { return aNumber.doubleValue(); // content ist mindestens Number } } Die Modifikation von Objekten über eine Wildcard-Variable oder einen Typebound ist nicht erlaubt. Der folgende Code führt zu einem Kompilationsfehler, weil x keine Kenntnis über den erlaubten Parameteryp von setContent() hat. Node<?> x = new Node<Integer>( 7 ); x.setContent( 5 ); // Kompilations-Fehler // ebenso zum Beispiel für Listen List<?> list = new LinkedList<Integer>; list.add( 123 ); // Kompilations-Fehler Arno Schmidhauser November 2006 Seite 16 JPA – Java Persistence API Frage: Wie ist folgende Deklaration aus der Java Standard Edition zu lesen? Collections.sort( List<T> list, Comparator<? super T> c ) Antwort: Die Operation sort() kann eine Liste vom Typ T sortieren. Der Comparator c muss Objekte vom Typ T vergleichen können. Damit dies möglich ist, müssen die Parameter der Methode compare( K obj1, K obj2 ) den Typ T oder eine Unterklasse davon haben. K muss also gleich T oder eine Oberklasse von T sein. Beispiel: import java.util.*; public class MySorter { public static void main( String args[] ) { List<Double> myList = new Vector<Double>(); myList.add( new Double( 3.14 ) ); myList.add( new Double( 1.41 ) ); Collections.sort( myList, new DoubleComparator() ); for ( Double d : myList ) { System.out.println( d.toString() ); } } } class DoubleComparator implements Comparator<Double> { public int compare ( Double n1, Double n2 ) { return Double.compare( n1, n2 ); } } Statt einen Comparator für Double-Werte, könnte man einen allgemeineren Comparator für Number erstellen. Dieser Comparator kann auch sehr grosse Zahlen miteinander vergleichen: class NumberComparator implements Comparator<Number> { public int compare ( Number n1, Number n2 ) { return ( (new BigDecimal(n1.toString())).compareTo( new BigDecimal(n2.toString())) ); } } Da ein Double ein Spezialfall einer Number ist, kann ohne weiteres der NumberComparator auf Double angewendet werden. Number ist ein Obertyp von Double, womit die Deklaration Comparator<? super T> nun verständlich ist. Arno Schmidhauser November 2006 Seite 17 JPA – Java Persistence API Generics, Eigenschaften • Generics sind Typkontrollen, die zur Kompilationszeit vorgenommen werden. • Nach der Kompilation wird die Typinformation mehrheitlich gelöscht im Byte-Code: // Vor Kompilation class Node<Number> { private T content; } // Nach Kompilation class Node { private Object content; } Æ Keine Abfrage der Typinformation zur Laufzeit mit Reflection. Æ Statische Attribute können keinen generischen Typ haben. Æ Primitive Typen (int, double, byte, char) können nicht als Typargumente verwendet werden. Æ Keine Arrays von Typvariablen erlaubt. 18 Nach der Kompilation wird nur noch soviel Typinformation über die Klasse und ihre Methoden in den Bytecode eingebaut, wie der Compiler für die Übersetzung anderer Java-Klassen benötigt. Aus Sicht der JVM ist die Typinformation für das Laufzeitsystem entfernt. Das Reflection API kann das Typargument nicht eruieren. Jedoch kann der Typ von Objekten an sich ermittelt werden. Um den Typ einer Collection festzustellen, müsste man den Typ der Objekte darin ermitteln, was grundsätzlich über Reflection möglich ist. Weil sich alle Typen einer generischen Klasse eine einzige Implementation dieser Klasse teilen, sind die statischen Attribute nur einmal vorhanden und tragen zur Laufzeit den Typ Object. Würden zur Laufzeit den Klassenvariablen verschiedene Typen von Objekten zugewiesen, zum Beispiel Double, Integer und String, entstünden völlig unvorhersgesehene Resultate, respektiv dynamische ClassCastExceptions beim Lesen und Schreiben dieser Objekte. Statische Attribute können daher keinen Typ haben. Primitive Typen können zur Laufzeit nicht als Objekte vom Typ Object behandelt werden, was aber mit der Art der Realisierung von Generics in Java notwendig wäre. Primitive Typen sind daher nicht erlaubt. Arrays von Typvariablen sind nicht möglich, weil der Typ zu Laufzeit nicht mehr bekannt ist, und damit die Speicherallokation nicht definiert ist. Arno Schmidhauser November 2006 Seite 18 JPA – Java Persistence API II Übersicht Java Persistence API 19 Referenz: JSR 220, EJB 3.0 Java Persistance API, Final Release 2. Mai 2006 Kurzform in diesem Skript: JPA oder JAPI Spez. Arno Schmidhauser November 2006 Seite 19 JPA – Java Persistence API Moderne Persistenz API's ↑ Arbeiten mit gewöhnlichen Java-Klassen für Daten (POJO's) ↑ Objekte können transient oder persistent sein ↑ Innerhalb und ausserhalb von Appservern verwendbar ↑ Vererbung, Aggregation, Komposition abbildbar ↑ Transitive Persistenz oder Persistence by Reachabiltiy • Lazy Fetching • Automatic Dirty Checking • Datenbank-Roundtrips minimieren, Outer Join Fetching • SQL-Generierung zur Laufzeit 20 Mit der Lösung des JPA in EJB 3.0 wurden wesentliche Verbesserungen ( ↑ ) erreicht werden gegenüber EJB 2.x Technologien. Das JPA ist ein Mix der Erfahrungen aus dem EJB 2.x Ansatz und dem JDO Konzept. JDO ist ein sehr ambitiöser Ansatz, mit dem Anspruch, den Source Code 100% von Mapping und Verhaltensangaben frei zu halten. Arno Schmidhauser November 2006 Seite 20 JPA – Java Persistence API Technologie Stack Session Beans / Java Applikation Java 1. & Annotations Persistence API EJB 3.0 Spezifikation Persistence API Implemetation Hibernate & CGLIB, ... JDBC 3.0 JDBC API Herstellerspez. JDBC - Driver SQL 2003 oder Dialekte SQL RDB 21 Auf der logischen Ebene sind Java-Annotations das Kernstück des JPA. Mit Ihnen wird das Mapping und das Verhalten der persistenten Klassen gesteuert. Die Anforderungen des Dirty Checking, Lazy Fetching und der transitiven Persistenz erfordern viel Aufwand in der JAPI Implementation: Wenn immer ein Programm eine Modifikation an Datenobjekten vornimmt, muss vorerst sichergestellt werden, dass das Objekt sich überhaupt im Speicher befindet (Lazy Fetch, Eager Fetch). Dann muss aufgezeichnet werden, dass das Objekt modifiziert wurde (Dirty Checking) und letztlich muss aufgezeichnet werden, ob ein Objekt zum CommitZeitpunkt transient oder persistent ist. Im letzteren Fall muss sein Zustand in die Datenbank zurückgeschrieben werden. Als Realisierungstechniken kommen typischerweise Source Code Modifikationen beim Deployment oder Byte Code Enhancement zur Kompilations- oder Laufzeit in Frage. Hibernate verwendet Byte Code Enhancement zur Laufzeit, indem jede Datenklasse über einen eigenen Classloader geladen wird. Dieser Classloader modifiziert den Code so, dass für jede Zugriffsoperation (Zuweisung eines Wertes, Increment/Decrement, Dereferenzierung, Array-Index-Auflösung) zusätzlich Verwaltungs-Code eingeschoben wird. Wichtige Datentypen, z.B. Set und List werden durch eigene Hibernate-Typen ersetzt. Dieses Verhalten ist gut im Debugger ersichtlich! Arno Schmidhauser November 2006 Seite 21 JPA – Java Persistence API JPA • Packages javax.persistence javax.persistence.spi • Classes Persistence • Interfaces EntityManagerFactory EntityManager EntityTransaction Query • Runtime Exceptions ( ~8 ) RollbackExceptione ption • Annotations ( ~64 ) Entity Id OneToOne OneToMany • Enumerations ( ~10 ) InheritanceType CascadeType FetchType 22 Das Package javax.persistence ist für die Entwicklerseite, das Pakage javax.persistence.spi definiert drei Interfaces für das Management der persistenten Klassen. Der Entwickler hat mit dem Package javax.persistence.spi nichts zu tun. Die Klasse Persistence ist quasi die Bootstrap-Klasse (im Container-Umfeld übernimmt dieser die Funktion von Persistence). Sie erzeugt eine EntityManagerFactory und in diesem Zug interne Instanzen von spi.PersistenceProvider, spi.PersistenceUnit und spi.ClassTransformer. Ausserdem wird ein neuer ClassLoader in der JVM installiert. Dieser ruft für die Entity-Klassen und alle Klassen, die mit Entities arbeiten, den Transformer auf, bevor diese Klassen in der JVM aktiv werden. Mit dem Transformer wird den geladenen Klassen zusätzliche Funktionalität für das Management der Persistenz hinzugefügt. Im Container-Umfeld übernimmt dieser die Aufgabe der Klasse Persistence. Ein Programm beginnt daher seine Persistenz-Aufgaben mit: EntityManagerFactor emf = Persistence.createEntityManagerFactory( persistenceUnitName ); EntityManager em = emf.createEntityManager() resp. im Container-Umfeld mit @PersistenceContext EntityManager em; Arno Schmidhauser November 2006 Seite 22 JPA – Java Persistence API Entities • • • • • Kennzeichnung mit Annotation @Entity Klasse kann Basisklasse oder abgeleitet sein Klasse kann abstrakt oder konkret sein. Serialisierbarkeit ist bezüglich Persistenz nicht erforderlich. Anforderungen: 1. No-Arg Konstruktur muss vorhanden sein. 2. Klasse darf nicht final, kein Interface und keine Enumeration sein und keine final-Methoden enthalten. 3. Felder müssen private oder protected sein. 4. Zugriff von Clients auf Felder nur über get/set- oder Business-Methoden erlaubt. 5. Jede Entity muss einen Primärschlüssel (@Id) haben. 23 Die Anforderungen 1 bis 4 sind technisch bedingt. Sie erlauben vernünftige Realisierungen der Persistenz, indem das Byte-Code Enhancement beispielsweise abgeleitete Klassen erzeugt, mit denen eine Applikation dann effektiv arbeitet. Die Anforderung 3 garantiert, dass nur über die –abgeleiteten- Methoden auf Felder zugegriffen wird, und in diesen Methoden das Persistenz-Management stattfinden kann. Arno Schmidhauser November 2006 Seite 23 JPA – Java Persistence API Entity, Beispiel @Entity public class Message { @Id protected long id; protected String sender; protected String receiver; protected int priority; @Temporal( TemporalType.TIMESTAMP ) protected Date date; public Message() {} // get, set and business methods } 24 Sehr pragmatisch gelöst ist das Mapping der Felder auf Datenbanktabellen: Ohne zusätzliche Angaben werden der Klassenname als Tabellenname und die Feldnamen als Spaltennamen verwendet. Mit folgenden Annotations kann man andere Mappings vornehmen: @Entity @Table( name="MyMessageTable" ) public class Message { // ... @Column( name="senderName" ) protected String sender; // ... } Zahlreiche weitere Mappingangabe, z.B. Datenbank- und Schema-Name bei der Tabelle, sind möglich. Siehe Kapitel 9.1 JPA Spez. Für gewisse Datentypen kann man Präzisierungen anbringen, z.B. @Temporal für die Java Typen Date und Calendar. Mit @Temporal wird angegeben, ob das Mapping in ein Datenbankattribut als DATE, TIME oder TIMESTAMP verstanden werden soll. Arno Schmidhauser November 2006 Seite 24 JPA – Java Persistence API persistente Datentypen • erlaubt: – Alle primitiven Typen, String – Alle Wrapperklassen und serialisierbaren Typen (z.B. Integer, BigDecimal, Date, Calendar) – byte[], Byte[], char[], Character[] – Enumerations – Beliebige weitere Entity-Klassen – Collections von Entities, welche als Collection<>, List<>, Set<> oder Map<> deklariert sind. • Nicht erlaubt: – Alle Arten von Arrays ausser die obgenannten – Collections von etwas anderem als Entities, also z.B. Wrapperklassen und andere serialiserbare Typen. 25 Der Umfang an persistenten Typen ist für die meisten Anwendungen ausreichend. Für technische Anwendungen könnte das Verbot von Array-Typen lästig sein. Dazu ist allerdings zu sagen, dass z.B. Hibernate wesentlich weiter geht als die JPA Spez., indem Arrays und Collections für sämtliche Arten von Objekten erlaubt sind, insbesondere auch Arrays von primitiven Typen. Felder von serialisierbaren Klassen, sei es von Java vordefinierte oder eigene Klassen, sind ebenfalls persistent. Während einige vordefinierte Typen wie Date, Calendar, Integer, BigDecimal usw. in die entsprechenden SQL-Typen gemapped werden, findet bei eigenen serialisierbaren Klassen einfach nur eine Ablage des serialisierten Byte-Stromes in der Datenbank statt (varbinary Datentyp o.ä.). Solche Objekte können nicht in einer Abfragebedingung von Queries auftreten. Zur Problematik der Elementreihenfolge in Listen siehe Kapitel IV. Transiente Felder können mit Java transient oder mit der @Transient Annotation markiert werden. Arno Schmidhauser November 2006 Seite 25 JPA – Java Persistence API Java-Type / SQL-Type Mapping • Implizit durch JDBC 3.0, Data Type Conversion Table, definiert. • Explizit durch die @Column Annotation, z.B. ... @Column( name="sender", columnDefinition="VARCHAR(255) NOT NULL" ) protected String sender; ... • Produktspezifisch durch Framework (Hibernate) oder im JDBC-Driver für die jeweilige Datenbank. 26 JDBC 3.0 Mapping Arno Schmidhauser November 2006 Seite 26 JPA – Java Persistence API Access-Typ • Es gibt zwei Zugriffspunkte für das Persistenz-Framework auf die Daten einer Klasse: das eigentliche Datenfeld, oder die zugehörigen set()/get() Methoden. • Field-based Access @Entity public class Message { @Id protected long id; } • Property-based Access @Entity public class Message { protected long id; @Id public long getId() {...} public void setId( long id ) {...} } 27 Welche Zugriffsart vorliegt, ist durch die Position der Mapping-Annotations gegeben. Alle Mapping Annotations müssen entweder bei Feldern stehen ( Field-based Access) oder bei der get() Methode für eine Eigenschaft. Bei Property-basedAccess müssen alle set- und get-Methoden zwingend paarweise vorkommen. Bei Field-based Access können get-Methoden oder anders benannte Methoden für den Zugriff auf private Felder verwendet werden. Bei Property-based Access wird in der Datenbank der von der get()-Methode zurückgelieferte Wert abgelegt, beim Erzeugen des Objektes in der Applikation wird die set()-Methode für das Setzen des Zustandes verwendet. Wäre also beispielsweise ein Personename in einem Java-Objekt in Kleinschreibung abgelegt, die getMethode liefert jedoch immer eine Grosschreibung zurück, so enthält die Datenbanktabelle den Personennamen in Grosschrift. Arno Schmidhauser November 2006 Seite 27 JPA – Java Persistence API Entity Identity - Primärschlüssel • Jede Entity-Klasse muss einen mit @Id bezeichneten Primärschlüssel besitzen. • Primärschlüssel können in Zusammenarbeit mit der Datenbank generiert werden. Beispiel: @Entity public class Message { @Id @GeneratedValue(strategy=IDENTITY ) public long id; • Stragegien sind Identity, Table, Sequence und Auto 28 Identity und Sequence: Das Datenbankssystem erzeugt eine fortlaufende Nummer. Sequence lehnt sich an die Oracle-Technologie an. Auto: vom Framework gewählte Strategie. Meist gleichbedeutend mit Identity. Table: Erzeugung via Tabelle in Datenbank. Diese Tabelle wird durch den Entwickler, resp. DBA, erstellt und initialisiert. Beispiel: public class Message { @TableGenerator( name="MyIdGenerator", table="KeyStore", pkColumnName="keyName", valueColumnName="keyValue", pkColumnValue="MessageId", initialValue=1000, allocationSize=1) @Id @GeneratedValue( strategy=GenerationType.TABLE, generator="MyIdGenerator") protected long id; // ... } create table KeyStore ( keyName nvarchar( 32 ) not null unique, keyValue bigint not null default 1 ); insert into KeyStore ( keyName, keyValue ) values ( 'MessageId', 1000 ); Arno Schmidhauser November 2006 Seite 28 JPA – Java Persistence API Zusammengesetzte Primärschlüssel • Es wird eine spezielle ID-Klasse benötigt. Beispiel: public class DocumentId implements Serializable { protected String name; protected BigDecimal version; // verlangte Methoden … @IdClass( pmq.DocumentId.class ) // Variante 1 @Entity public class Document { @Id protected String name; @Id protected BigDecimal version; @Entity public class Document { // Variante 2 @EmbeddedId public DocumentId docId; 29 Die ID-Klasse ist notwendig, damit ein einheitlicher Gebrauch aller Arten von Primärschlüsseln möglich wird, z.B. beim Aufruf der find() oder getReferenceMethode(), um bestimmte Objekte in der Datenbank zu finden. Genau genommen wird ja auch bei einfachen Primärschlüsseln mit einer ID-Klasse gearbeitet, allerdings von einem vordefinierten Java-Typ. An die ID-Klasse werden folgende Anforderungen gestellt: 1. No-arg Konstruktor 2. Serializable 3. Der Access-Typ (field oder property) muss dem Access-Typ in der verwendeten Entity-Klasse entsprechen. 4. Fields oder properties müssen public oder protected sein. 5. Die hashCode() und equals()-Methode müssen implementiert werden. 6. Name und Typ der einzelnen Felder/Properties müssen in der ID-Klasse und in der verwendeten Entity-Klasse übereinstimmen, wenn nicht @EmbeddedId verwendet wird. Bedingung 5 kann wie folgt realisiert werden: public int hashCode() { return (name + version.toString()).hashCode(); } public boolean equals( Object o ) { DocumentId oid = (DocumentId) o; return ( this.name.equals( oid.name ) && this.version.equals( oid.version ) ); } Arno Schmidhauser November 2006 Seite 29 JPA – Java Persistence API Eine Applikation darf nicht direkt die Felder eines ID-Objektes ändern (DocumentIDObjektes), sondern nur die entsprechenden Felder in der Entity-Klasse (Document), wenn mit @IdClass gearbeitet wird. wenn mit @EmbeddedID gearbeitet wird, kann ein Primärschlüsselobjekt zum Beispiel im Konstruktor der Datenklasse wie folgt erzeugt werden: public Document( String name, BigDecimal version, StringBuffer content ) { this.docId = new DocumentId( name, version ); // ... } Die zugehörige Datentabelle sieht in beiden Fällen(@IdClass oder @EmbeddedId) wie folgt aus: create table "Document" ( name nvarchar(64) not null, version numeric(4,2) not null default '1.0', -- ... primary key ( name, version ) ); Arno Schmidhauser November 2006 Seite 30 JPA – Java Persistence API Persistence Unit • Eine Persistence Unit ist eine logische Einheit von Entities. Sie wird beschrieben durch: – – – – – – – Einen Namen Die zu dieser Unit gehörenden Entity-Klassen Angaben zum Persistence Provider Angabe zum Transaktionstyp Angaben zur Datenquelle Weitere Properties Namen von XML OR-Mapping Files • Technisch wird die Beschreibung einer Persistence Unit in der Datei META-INF/persistence.xml abgelegt. 31 Der Name ist eine Identifikation, um in der Applikation dafür eine EntityManagerFactory erstellen zu können. Die Entity-Klassen werden als Information für den Class Transformer und die Generierung der SQL-Befehle angegeben. Der PersistenceProvider ist die implementationsspezifische "Urklasse" für das Persistenzmanagement in dieser Persistence Unit. Sie liefert die EntityManagerFactory zurück. Der Transaktionstyp (transaction-type) wird als JTA oder RESOURCE_LOCAL angegeben. eine JTA Persistence Unit muss u.A. verteilte Transaktionen über eine XA-Schnittstelle erlauben. Die Datenquelle kann als JNDI-Name angegeben werden im Rahmen einer J2EE-Umgebung. Je nachdem ob die Datenquelle JTA-fähig ist (verteiltes Transaktionsmanagement) verwendet man den Tag <jta-data-source> oder <non-jata-data-source>. Falls nicht mit JNDI gearbeitet wird, können in den Properties die üblichen JDBC-Verbindungsparameter angegeben werden. Beispiel: <persistence> <persistence-unit name="PMQ" transaction-type="RESOURCE_LOCAL"> <provider>org.hibernate.ejb.HibernatePersistence</provider> <class>pmq.PMQ</class> <class>pmq.Message</class> <non-jta-data-source>jdbc/MyPMQDB</non-jta-data-source> <jta-data-source>jdbc/MyPMQDB</jta-data-source> <properties> <property name="hibernate.connection.driver_class" value="com.microsoft.sqlserver.jdbc.SQLServerDriver"/> <property name="hibernate.connection.username" value="hibuser"/> <property name="hibernate.connection.password" value="hibuser"/> <property name="hibernate.connection.url" value="jdbc:sqlserver://localhost;databaseName=hib_db"/> <property name="dialect" value="org.hibernate.dialect.SQLServerDialect"/> </properties> </persistence-unit> </persistence> Arno Schmidhauser November 2006 Seite 31 JPA – Java Persistence API Persistence Context • Der Persistence Context definiert das physische Umfeld von Entities zur Laufzeit: – Die Menge aller Managed Entities in der Applikation – Der Entity Manager für diese Entities – Die laufende Transaktion – Der Contexttyp @PersistenceContext(type=PersistenceContextType.EXTENDED) public class MessageProducer { // ... EntityManagerFactory emf = Persistence.createEntityManagerFactory("PMQ"); EntityManager em = emf.createEntityManager(); Transaction tx = em.getTransaction(); // ... 32 Arno Schmidhauser November 2006 Seite 32 JPA – Java Persistence API Persistence Context, Contexttyp • Contexttyp TRANSACTION – Lesender und schreibender Zugriff nur innerhalb der Transaktion. – Gelesene Objekte sind nach der Transaktion im Zustand detached. – Wiedereinkopplung in eine Transaktion mit merge(). • Contexttyp EXTENDED – Alle Objekte sind lesend und schreibend zugreifbar. – Modifikationen finden lokal statt. – Effekt von persist(), remove() usw. wird aufbewahrt. – Propagation von Efffekten und Änderungen in die DB aber nur, wenn nachträglich begin()/commit() ausgeführt wird. 33 Bei Hibernate ist für Java SE Applikationen immer der Transaktionstyp EXTENDED gesetzt. Ein Rollback führt in beiden Contexttypen dazu, dass bereits geladene Entities in den Zustand detached versetzt werden. Nach einem Rollback sollte nicht mehr mit den bestehenden Enitities gearbeitet werden (Kap. 3.3.2 JPA Spez.), weil der lokale Zustand beibehalten, der Zustand in der DB jedoch zurückgesetzt wird. Das kann z.B. Probleme mit generierten Primärschlüsseln verursachen, weil eine bereits vergebene ID seitens Datenbank nochmals vergeben wird. Dasselbe gilt sinngemäss für Versionennummern bei optimistischem Concurrency Control. Datenbankzugriffe arbeiten gemäss Spezifikation im Isolationsgrad read committed. Dies gilt auch für den Contexttyp Extended. Arno Schmidhauser November 2006 Seite 33 JPA – Java Persistence API Erzeugen persistenter Objekte // ... EntityManagerFactory emf = Persistence.createEntityManagerFactory("PMQ"); EntityManager em = emf.createEntityManager(); EntityTransaction tx = em.getTransaction(); tx.begin(); Message m = new Message( ... ); em.persist( m ); tx.commit(); // ... 34 Arno Schmidhauser November 2006 Seite 34 JPA – Java Persistence API Kaskadierte Persistenz (1) • Kaskadierte Persistenz heisst: Alle von einem persistenten Objekt aus erreichbaren Objekte sind ebenfalls persistent. PMQ pmq = new PMQ( "Dringendes" ); em.persist( pmq ); Message m = new Message( ... ) pmq.append( m ); m wird automatisch persistent // ... em.remove( pmq ); m wird ebenfalls aus der db gelöscht • Es spielt keine Rolle, ob die persist() Methode vor oder nach der append()-Methode aufgerufen wird! 35 Arno Schmidhauser November 2006 Seite 35 JPA – Java Persistence API Kaskadierte Persistenz (2) • In JPA muss die Kaskadierung deklariert werden. • Die Kaskadierung kann für das Erstellen und das Löschen der Persistenz separat eingestellt werden. public class PMQ { @OneToMany( cascade={CascadeType.PERSIST, CascadeType.REMOVE } ) List<Message> messages; // ... } • Achtung: Die Kaskadierung bezieht sich nur auf die persist() und die remove()-Methode, nicht etwa auf das Ein- oder Austragen in der Message-Liste mit gewöhnlichen Java-Methoden! 36 Die Kaskadierung kann auch auf die Methoden EntityManager.refresh() und EntityManager.merge() bezogen werden, mit CascadeType.REFRESH resp. CascadeType.MERGE. Arno Schmidhauser November 2006 Seite 36 JPA – Java Persistence API Aufsuchen persistenter Objekte // ... tx.begin(); Message m = em.find( Message.class, Long.parseLong(1) ); // oder ... Message m = em.getReference(Message.class, Long.parseLong(1)); // oder ... Query qry = em.createQuery( "select m from Message m where m.id = 1 ); List list = qry.getResultList(); Iterator it = list.iterator(); // ... tx.commit(); // ... 37 Es sind zwei Varianten für das Auffinden von Objekten vorgestellt: Suchen via Primärschlüssel mit find() oder getReference() Es muss die Klasse und der Primärschlüsselwert im gleichen Datentyp wie in der Entity definiert übergeben werden. Die Methode find() liefert null, wenn das Objekt nicht existiert, die Methode getReference() erzeugt eine Exception, wenn das Objekt nicht existiert (spätestens beim ersten eigentlichen Zugriff auf das Objekt). Die Methode find() lädt auf jeden Fall die unmittelbaren Felder des gesuchten Objektes. Suchen mit einer Abfrage Objekte werden via eine Abfrage gesucht. Das Resultat wird als Liste zurückgegeben. Die resultierende Liste hat keinen Typparameter. Dies deshalb, weil der Resultattyp vom Query abhängt und dieses in jedem Fall dynamisch ausgewertet wird. Die Typsicherheit würde sich durch Verwendung von Typparametern nicht erhöhen. Im Weiteren ergäben sich Schwierigkeiten, wenn die select-Klausel mehrere Objekte enthält, das heisst eine Liste von Arrays zurückgeliefert wird. Ein Array von Typvariablen ist in Java nicht erlaubt. Arno Schmidhauser November 2006 Seite 37 JPA – Java Persistence API Aufgaben • Übung Persistent Message Queue PMQ. Erstellen einer Grundausstattung. MessageProducer MessageConsumer main() main() «verwendet» «Entity» PMQ «verwendet» messages id qname append() get() getAll() remove() isEmpty() size() 0..* «Entity» Message sender receiver priority date id 38 Aufgaben 1. Hibernate-Umfeld und Datenbank aufsetzen. 2. Entity-Klassen PMQ und Message definieren. 3. persistence.xml zusammenstellen. 4. Datenbanktabellen erstellen. 5. Producer (MessageProducer.java) und Consumer (MessageConsumer.java) für Messages programmieren, ev. Zusatzprogramm zum erstellen einer leeren PMQ (PMQInit.java). 6. Testen. Arno Schmidhauser November 2006 Seite 38 JPA – Java Persistence API III Beziehungen zwischen Entities 39 Arno Schmidhauser November 2006 Seite 39 JPA – Java Persistence API Beziehungen • Beziehungen zwischen Entities sind prinzipiell gegeben durch entsprechende Referenzen oder Collection-Member in den Entity-Klassen. Sie müssen jedoch deklariert werden, und sehr oft sind Details zum O/R-Mapping und zum Verhalten notwendig. • Folgende Beziehungs-Charakteristiken spielen eine Rolle: – – – – Unidirektional, bidirektional One to One, One to Many, Many to One, Many to Many Vererbung Aggregation, Komposition • Der korrekte Unterhalt der Beziehungen ist Sache des Programmes! 40 Der Unterhalt der Beziehungen obliegt dem Programm, respektive dem Entwickler. Wird beispielsweise bei einer bidirektionalen Beziehung eine Message in einer Message Queue eingetragen, muss das Programm selber in der Message die Referenz auf die MessageQueue nachführen. Das Persistenz-Framework tut das nicht. Die Deklaration der Beziehungen ist eine Mapping-Hilfe für das Framework und bringt keine zusätzliche Programmautomation mit sich! Arno Schmidhauser November 2006 Seite 40 JPA – Java Persistence API One To One, Unidirektional «Entity» PMQ «Entity» Owner owner id id 1..1 @Entity public class PMQ { @OneToOne( optional=false ) @JoinColumn( name="owner_id" ) protected Owner owner; // ... 41 Die Beziehung geht rein von der linken Seite aus. Es ist an sich nicht spezifiziert, ob ein Owner mehrere PMQ's oder nur eine PMQ haben kann. Die Owner-Klasse beinhaltet keine Angaben zu dieser Beziehung. Die Abbildung wird mit einem Fremdschlüssel in der PMQ-Tabelle realisert. Mit @OneToOne( optional=false) wird präzisiert, dass immer ein Owner vorhanden sein muss. Die Angabe bezieht sich also auf das Zielobjekt. In der Tabelle sollte sich dies darin wiederspiegeln, dass der Fremdschlüssel nicht null sein darf. create table "PMQ" ( id bigint not null identity primary key, qname nvarchar(64) not null unique, owner_id bigint not null ); Die obige Tabelle darf durchaus eine foreign key Klausel enthalten. Sie wurde hier nur aus Übersichtgründen weggelassen. Arno Schmidhauser November 2006 Seite 41 JPA – Java Persistence API One To One, Bidirektional «Entity» PMQ «Entity» Owner owner id id 0..1 1..1 @Entity public class PMQ { @OneToOne( optional=false ) @JoinColumn( name="owner_id" ) protected Owner owner; // ... @Entity public class Owner { @OneToOne( mappedBy="owner", optional=true ) protected PMQ pmq; //... 42 Die Klasse PMQ bleibt gleich wie in der unidirektionalen Variante. Auch die Datenbanktabelle für PMQ bleibt sich gleich, und die Owner-Tabelle muss nicht angepasst werden. Die Angabe mappedBy="owner" bezieht sich auf das Feld owner in der Klasse PMQ, und damit ist indirekt gesagt, dass der Fremdschlüssel sich auf der PMQ-Tabelle befindet. Mit optional=true kann das Owner-Objekt bestehen bleiben, wenn die PMQ gelöscht wird. Programmatisch wird nicht kontrolliert, ob eventuell dasselbe Owner-Objekt für zwei PMQ-Objekte verwendet wird. Dies ist nur zu kontrollieren, indem ein uniqueConstraint auf den owner_id Fremdschlüssel gesetzt wird. Damit können nicht zwei PMQ-Einträge in der Datenbank mit demselben owner_id existieren. Bei bidirektionalen Beziehungen spricht man oft vom Besitzer. Der Besitzer ist in diesem Beispiel die PMQ-Klasse. Der Besitzer ist die Klasse, welche nicht die mappedBy-Angabe enthält. Arno Schmidhauser November 2006 Seite 42 JPA – Java Persistence API One To Many, Unidirektional «Entity» PMQ «Entity» Message messages id id 0..* @Entity public class PMQ { @OneToMany @JoinTable( name="PMQ_Message", joinColumns={ @JoinColumn( name="PMQ_id" ) }, inverseJoinColumns={ @JoinColumn(name="messages_id") } ) protected List<Message> messages = new Vector<Message>(); // ... 43 Eine unidirektionale OneToMany-Beziehung wird mit einer Assoziationstabelle abgebildet, nicht mit einer direkten Primärschlüssel-/ Fremdschlüssel-beziehung. Begründung an diesem Beispiel: Eine Message kann nicht nur zu einer PMQ, sondern vielleicht noch zu anderen Klassen gehören. Wenn die Zugehörigkeit in eine (oder mehrere) Assoziationstabelle ausgelagert ist, braucht es nicht für jede Zugehörigkeit in der Message-Tabelle ein Fremdschlüssel-Attribut, von denen dann viele nicht belegt sind. Obige Angaben in der @JoinTable-Annotation entsprechen dem Default-Verhalten: Der Name der Assoziationstabelle setzt sich aus den Namen der beiden EntityKlassen zusammen. Die Attributnamen der Assoziations-Tabelle ergeben sich wie im folgenden Beispiel für die Tabellendefinition gezeigt, aus einer Kombination von Tabellenname links + id-Feld links, Beziehungsname links + id-Feld rechts. create table "Message" ( id bigint not null identity primary key, -- ... ); create table "PMQ" ( id bigint not null primary key, -- ... ); create table "PMQ_Message" ( PMQ_id bigint not null, messages_id bigint not null unique ); Arno Schmidhauser November 2006 Seite 43 JPA – Java Persistence API One To Many, Bidirektional «Entity» PMQ id messages «Entity» Message 0..1 0..* id pmq @Entity public class PMQ { @OneToMany( mappedBy = "pmq" ) protected List<Message> messages = new Vector<Message>(); //... @Entity public class Message { @ManyToOne( optional=true ) @JoinColumn( name="PMQ_id" ) protected PMQ pmq; //... 44 Die bidirektionale Beziehung wird als enger wie die unidirektionale angesehen und deshalb per Default als direkte Primärschlüssel-/Fremdschlüssel Verknüpfung realisiert. Die Annotation @JoinTable entfällt. Hingegen wird in der Klasse PMQ angegeben, welches Feld (nicht Tabellenattribut!) in der Message-Klasse für die Beziehung verantwortlich ist. Diese Angabe wird durch mappedBy="pmq" in der OneToMany-Annotation vorgenommen. In der Message-Klasse ist die Rückbeziehung durch die @ManyToOne-Annotation deklariert. Die @JoinColumnAnnotation deklariert den Namen des Fremdschlüssels. Achtung: Mit nur dem Java-Code wäre nicht zweifelsfrei festgehalten, dass es sich um eine echt bidirektionale Beziehung handelt. Das Feld Message.pmq könnte ja auf eine andere PMQ zeigen als diejenige von der es referenziert wird, und eine andere Bedeutung haben. Zu beachten ist auch der Unterschied zwischen einer ManyToOne 1..1 oder 0..1 Beziehung: Es wird entweder @ManyToOne( optional=false ) oder @ManyToOne( optional=true ) angegeben. Ausserdem ist die Tabellendefinition für den Fremdschlüssel mit null respektive not null zu deklarieren. create table "Message" ( id bigint not null identity primary key, PMQ_id bigint null -- foreign key to PMQ table -- ... ); Für die @OneToMany-Annotation gibt es keine optional=false respektive optional=true Angabe. Auf der Many-Seite gibt es also nur die Multiplizität 0..n. Andere Multiplizitäten, insbesondere 1..n, müssen programmatisch und allenfalls über Triggers in der Datenbank realisiert werden (check-Constraints sind nicht ausreichend, da der delete-Befehl keine Prüfung von check-Constraints auslöst). Arno Schmidhauser November 2006 Seite 44 JPA – Java Persistence API Many To One, Unidirektional «Entity» Address «Entity» Owner id 1..1 0..* id owner @Entity public class Address { @ManyToOne( optional=false ) @JoinColumn( name="Address_id", referencedColumnName = "id" ) protected Owner owner; // ... 45 Die unidirektionale ManyToOne-Beziehung dürfte eher selten sein. Der Default-Name für das Fremdschlüssel-Attribut ist der Name der Ursprungsklasse + "_" + Name des Primärschlüsselfeldes in der Zielklasse. Das obige Beispiel gibt diese Default-Benennung wieder. Arno Schmidhauser November 2006 Seite 45 JPA – Java Persistence API Many To Many, Bidirektional «Entity» PMQ «Entity» Message @Entity id id messages pmqs public class PMQ { 0..* 0..* @ManyToMany @JoinTable( name="PMQ_Message", joinColumns={@JoinColumn(name="pmqs_id" ) }, inverseJoinColumns={@JoinColumn(name="messages_id") } ) protected List<Message> messages = new Vector<Message>(); // ... @Entity public class Message { @ManyToMany( mappedBy = "messages" ) protected List<PMQ> pmqs = new Vector<PMQ>(); // ... 46 Abbildung mit Assoziationstabelle. create table "PMQ_Message" ( pmqs_id bigint not null, messages_id bigint not null ); Die unidirektionale ManyToMany-Beziehung entspricht aus Sicht Java Code der unidirektionalen OneToMany-Beziehung. Auf obiges Beispiel bezogen kann eine Message also zu mehreren Queues gehören, die Message kennt diese jedoch nicht. Nur die PMQ's kennen ihre Messages. Die Message-Klasse enthält damit das Feld pmqs gar nicht. Seitens Datenbank ist die Situation ebenfalls identisch zur OneToMany Beziehung, ausser dass der zweite Fremdschlüssel nicht unique ist: create table "PMQ_Message" ( PMQ_id bigint not null, messages_id bigint not null unique ); Für die @ManyToMany-Annotation gibt es keine optional=false respektive optional=true Angabe. Es existiert also nur die Multiplizität 0..n. Andere Multiplizitäten, insbesondere 1..n, müssen programmatisch und allenfalls über Triggers in der Datenbank realisiert werden (check-Constraints sind nicht ausreichend, da der delete-Befehl keine Prüfung von check-Constraints auslöst). Arno Schmidhauser November 2006 Seite 46 JPA – Java Persistence API Komposition / Aggregation «Entity» PMQ «Entity» Message id messages id 0..* @OneToMany( cascade={CascadeType.PERSIST, CascadeType.REMOVE}) protected List<Message> messages; «Entity» PMQ «Entity» Message id messages id 0..* @OneToMany( cascade={ CascadeType.PERSIST} ) protected List<Message> messages; 47 Die Komposition ist in der Regel ergänzt sein um eine 1..1 Beziehung von rechts nach links, im obigen Fall zum Beispiel: @ManyToOne( optional=false ) protected PMQ pmq; Achtung: Ohne Angabe der kaskadierten Löschung kann es auch mit einer deklarierten 1..1 Beziehung von rechts nach links zu Inkonsistenzen auf der Datenbank kommen. Der Befehl em.remove( pmq ) würde nämlich klaglos ablaufen, weil innerhalb des Programmes die beteiligten Objekte noch korrekt verknüpft sind, in der Datenbank jedoch nur die PMQ, nicht die Message gelöscht wird. Datenbankseitig könnte man dies durch einen not null constraint auf dem Fremdschlüssel in der Message-Tabelle verhindern. Die Aggregation kann ergänzt sein um eine 0..1 Beziehung von rechts nach links, im obigen Fall zum Beispiel: @ManyToOne( optional=true ) protected PMQ pmq; Arno Schmidhauser November 2006 Seite 47 JPA – Java Persistence API Vererbung, Grundsätzliches • Vererbungshierarchien können problemlos verwendet und abgebildet werden. • Klassen können abstrakt oder konkret sein. • Alle Klassen in der Vererbungshierachie müssen den Primärschlüssel der Basisklasse verwenden (erben). • Es gibt vier Mappingstrategien auf die Datenbank: – – – – Eine einzige Tabelle für die gesamte Verbungshierarchie Eine Tabelle für jede konkrete Klasse Eine Tabelle für jede Klasse Mapped Superclass 48 Zu den Strategien Die Strategie wird in der Basisklasse angegeben mit: @Entitiy @Inheritance(strategy=SINGLE_TABLE) @Entitiy @Inheritance(strategy=TABLE_PER_CLASS) @Entitiy @Inheritance(strategy=JOINED) @MappedSuperclass Arno Schmidhauser November 2006 Seite 48 JPA – Java Persistence API SINGLE_TABLE Strategie Die Attribute der Basisklasse gelten für jeden Datensatz. Attribute von abgeleiteten Klassen sind nur belegt, wenn der Datensatz ein Objekt dieser Klasse repräsentiert. Es ist ein Typ-Attribut erforderlich, dass für jeden Datensatz angibt, zu welcher Klasse das repräsentierte Objekt gehört. Vorteile: Kompakt, Abfragen über alle Objektarten sind einfach zu handhaben. Nachteile: Viele Attribute ohne Belegung mit Werten. Attributzuordnung zu Klasse nicht ersichtlich. Fehlverhalten möglich, indem Attributen Werte zugeordnet werden, die gar keine haben dürften → Integritätsbedingungen erforderlich. Anwendung: Hauptinformation in der Basisklasse, abgeleitete Klassen nicht zu umfangreich, oder insgesamt wenig Attribute zu implementieren. TABLE_PER_CLASS Strategie Es wird nur pro konkrete Klasse eine Tabelle erstellt. Die Attribute einer abstrakten Basisklasse werden in jeder Tabelle der abgeleiteteten Klasse erstellt. Für die Basisklasse selbst wird keine Tabelle erstellt. Vorteile: Jede Tabelle entspricht genau einer Klasse. Es sind keine nicht-belegten Attribute vorhanden. Nachteil: Abfragen über mehrere Klassen hinweg erfordern union-Abfragen (kann teilweise Probleme mit SQL geben). Änderungen an der Basisklasse erfordern Änderungen in allen Tabellen der abgeleiteten Klassen. Anwendung: Basisklasse nur schwach belegt und abstrakt. Meistens werden nur konkrete und einzelne, abgeleitete Klassen in einer Applikation benötigt. JOINED Strategie Jede Tabelle enthält nur die Attribute ihrer Klasse. Der Primärschlüssel der Basistabelle wird in allen abgeleiteten Klassen ebenfalls als Primärschlüssel verwendet und gleichzeitig als Fremdschlüssel auf die Basistabelle. Vorteile: Konzeptionelles Modell im Tabellenmodell noch ersichtlich. Punktuelle Änderungen am Konzept haben nur punktuelle Änderungen in den Tabellen zur Folge. Mit dieser Variante ist es datenbankseitig möglich, dass ein Objekt in mehreren Klassen vorkommt! Beispielsweise kann in einem CRM-System eine Person gleichzeitig Kunde und Lieferant sein. Nachteile: Gewisse Arten von Abfragen sind umständlich, beispielsweise wenn man wissen will, zu welcher Klasse ein Datensatz in der Basistabelle gehört. Es muss dann in jeder abgeleiteten Tabellen nach einem entsprechenden Datensatz gesucht werden. Anwendung: Viele Attribute, sowohl in der Basisklasse, wie in den abgeleiteten Klassen vorhanden. Mehrstufige Vererbungshierarchie. Flexible Datenstruktur für unterschiedlichste Bedürfnisse und Abfrage-Arten erforderlich. Häufige Änderung des Datenmodells wahrscheinlich. Mapped Superclass Strategie Die Basisklasse ist selbst keine Entity, stellt aber Attribute zur Verfügung für die abgeleiteten Klassen, welche als Entities deklariert sind. Die abgeleiteten EntityKlassen übernehmen die Felder von der Basisklasse, welche nicht transient deklariert sind. Die Felder werden in den Tabellen der abgeleiteten Entity-Klasse abgelegt. Kann angewendet werden, wenn die Basisklasse nicht als eigene Tabelle und nicht als Entity in Erscheinung treten soll (Praktische Beispiele?) @MappedSuperclass public class MeineBasisklasse{} @Entity public class MeineAbgeleiteteKlasse extends MeineBasisklasse{} Arno Schmidhauser November 2006 Seite 49 JPA – Java Persistence API Vererbung, Beispiel «Entity» Message id «Entity» SimpleMessage content «Entity» ImageMessage image contentType «Entity» XMLMessage xmlContent 50 Arno Schmidhauser November 2006 Seite 50 JPA – Java Persistence API Vererbung, SINGLE_TABLE-Mapping @Entity @Inheritance(strategy=InheritanceType.SINGLE_TABLE) @DiscriminatorColumn( name="messageType" ) public abstract class Message { @Id @GeneratedValue protected long id; // ... @Entity @DiscriminatorValue("SM") public class SimpleMessage extends Message {} @Entity @DiscriminatorValue("IM") public class ImageMessage extends Message {} … 51 Falls kein Discriminatorattribut angegeben wird, ist "DTYPE" der Default. Falls kein Discriminatorwert angegeben wird, ist der Klassenname der Default. Alle Tabellenattribute, welche nicht zur Basisklasse gehören, dürfen null-Werte enthalten. create table "Message" ( id bigint not null default primary key, messageType varchar( 64 ) not null, sender nvarchar( 64 ) not null, receiver nvarchar( 64 ) not null, priority int not null, date datetime not null, content nvarchar( max ) null, image varbinary( max ) null, contentType nvarchar( 64 ) null, description nvarchar( 255 ) null, xmlContent xml null ); Arno Schmidhauser November 2006 Seite 51 JPA – Java Persistence API Vererbung, JOINED-Mapping @Entity @Inheritance(strategy=InheritanceType.JOINED) public abstract class Message { @Id @GeneratedValue protected long id; // ... @Entity public class SimpleMessage extends Message {} @Entity public class ImageMessage extends Message {} … 52 create table "Message" ( id bigint sender nvarchar( 64 ) receiver nvarchar( 64 ) priority int date datetime ); not not not not not null identity primary key, null, null, null, null, create table "SimpleMessage" ( id bigint not null primary key, content nvarchar( max ) not null, foreign key ( id ) references Message( id ) ); create table "ImageMessage" ( id bigint not null primary key, image varbinary( max ) not null, contentType nvarchar( 64 ) not null, foreign key ( id ) references Message( id ) ); create table "XMLMessage" ( id bigint not null primary key, xmlContent xml not null foreign key ( id ) references Message( id ) ); Das TABLE_PER_CLASS Mapping funktioniert sinngemäss. Arno Schmidhauser November 2006 Seite 52 JPA – Java Persistence API Aufgaben • Implementieren Sie die verschiedenen Beziehungstypen am PMQ-Beispiel: – – – – OneToOne, unidirektional, Aggregation OneToMany, unidirektional, Komposition ManyToMany, bidirektional, Aggregation Vererbung 53 Arno Schmidhauser November 2006 Seite 53 JPA – Java Persistence API IV Spezielles 54 Arno Schmidhauser November 2006 Seite 54 JPA – Java Persistence API Listen • Listen haben eine implizite Ordnung. • Datenbanktabellen haben keine Ordnung. • Die Abbildung von Listen in Tabellen ist im JPA nicht vollständig transparent, sondern muss durch Annotations beschrieben werden. JPA Lösung Hibernate Lösung @OneToMany @OrderBy( "date ASC" ) List<Message> messages; @OneToMany @IndexColumn(name="position" ) List<Message> messages; 55 Für die @IndexColumn-Annotation ist noch die Hibernate-spezifische Annotation zu importieren: import org.hibernate.annotations.IndexColumn; Wird kein @OrderBy angegeben, findet die Sortierung zum Ladezeitpunkt nach dem Primärschlüssel statt. Die Sortierung wird nicht automatisch aufrecht erhalten zur Laufzeit. Bei Hibernate ist @OrderBy nur erlaubt, wenn das Mapping nicht mit einer Assoziationstabelle, sondern direkt mit Fremdschlüssel/Primärschlüssel erstellt wird. Es wird daher das Arbeiten mit der Hibernate-Annotation @IndexColumn empfohlen. Dies hat den Vorteil, dass die Positionierung nicht in der Entity-Tabelle (Message), sondern der Assoziationstabelle abgelegt ist. Damit kann die Positionierung auch im Fall einer ManyToMany-Beziehung abgebildet werden, wo eine Message in verschiedenen PMQ's verschiedene Positionen haben kann. @OrderBy erfordert ausserdem ein geeignetes Attribut für die Sortierung (das vom Programm unterhalten wird). Mit IndexColumn kann ein spezielles, generisches Attribut definiert werden, das vom Framework gepflegt wird. Arno Schmidhauser November 2006 Seite 55 JPA – Java Persistence API Die Abbildung von Listen tendiert zu Performance-Problemen. Wird beispielsweise keine @IndexColumn definiert in Hibernate, werden alle Beziehungsdatensätze beim Zurückschreiben in die Datenbank gelöscht und neu eingefügt. Ist die @IndexColumn definiert, wird beim Anfügen eines neuen Elementes auch nur der neue Datensatz in der Tabelle eingefügt. Das Einfügen eines Elementes zwischendurch hat jedoch einen Update auf allen Einträgen der Assoziationstabelle zur Folge: Für jede Position wird der Fremdschlüssel des Elementes an dieser Position neu gesetzt. Hibernate generiert für eine OneToMany Beziehung folgende Beziehungstabelle für die Liste: create table PMQ_Message ( PMQ_id bigint not null, messages_id bigint not null, position bigint not null, primary key ( PMQ_id, position ), unique ( messages_id ), foreign key ( PMQ_id ) references PMQ( id ) on delete restrict foreign key ( messages_id ) references Message( id ) on delete restrict ) Arno Schmidhauser November 2006 Seite 56 JPA – Java Persistence API Neben Listen werden auch Maps unterstützt. Der Nutzen von Maps für Entities ist allerdings fraglich. Maps ersetzen lediglich das Suchen von Objekten via Primär- oder Fremdschlüssel. Beispiel: import org.hibernate.annotations.IndexColumn; // … @Entity public class PMQ { @JoinTable( name="PMQ_Message", joinColumns = { @JoinColumn( name="pmqs_id" ) }, inverseJoinColumns = { @JoinColumn( name="messages_id" ) } ) @MapKey( name="position" ) protected Map<Long, Message> messages; // … Beispiel für das Arbeiten mit Arrays in Hibernate: import org.hibernate.annotations.IndexColumn; // … @OneToMany @JoinTable( name="EventDate", joinColumns = @JoinColumn(name="id", referencedColumnName="id") ) @Column( name="eventDate" ) @IndexColumn( name="position" ) protected Calendar eventDates[] = new Calendar[10]; // … create table "EventDate" ( id bigint not null, eventDate datetime not null, position smallint not null ); Arno Schmidhauser November 2006 Seite 57 JPA – Java Persistence API Persistente Enumerations • Enumerations können persistent sein. In der Datenbank wird entweder der Ordinalwert (Position) oder der Stringwert (Name der Konstante) abgelegt. @Entity public class MutableMessage extends SimpleMessage { @Enumerated(EnumType.ORDINAL) protected MessageStatus status; oder // ... @Enumerated(EnumType.STRING) 58 Default für die Datenbankablage ist ORDINAL. Der Datentyp in der Datenbank kann varchar oder int sein. varchar ist sowohl für den Stringwert wie für die Zahl geeignet. Anmerkung: Hibernate hat Schwierigkeiten mit der Datenbank, wenn bei bestehenden Daten der Ablagetyp geändert wird Æ Tabellenwerte gemäss aktueller Einstellung der @Enumerated-Annotation anpassen. Arno Schmidhauser November 2006 Seite 58 JPA – Java Persistence API Embedded Classes • Komposition: Mutterobjekt mit eingebetteten Objekten. • Eingebettete Objekte haben keine eigene Identität. • Mutterobjekt und eingebettete sind in derselben Tabelle abgelegt. @Entity public class PMQ { @Id @GeneratedValue private long id; @Embedded protected Owner owner; // ... } @Embeddable public class Owner { protected String name; //... } 59 Klassen, respektive deren Objekte, können in andere Klassen eingebettet sein: Ihre Felder werden in der Datenbank in derselben Tabelle abgelegt wie das Mutterobjekt. Die eingebetteten Objekte haben keine Identität und sind nicht shareable. Die Einbettung von Collections ist nicht unterstützt. Die Einbettung hat (im Gegensatz zu JDO) nichts mit Serialisierung zu tun. Die Felder der eingebetten Klassen kommen explizit in den SQL-Tabellen vor. Embedded Klassen müssen als solche markiert sein. Selbstdefinierte Klassen, welche Serializable und nicht als Entities gekennzeichnet sind, werden automatisch in die Datenbank serialisiert (Binärer Datentyp in der Datenbank). Solche Felder sind jedoch nicht abfragbar mit JPA Query Language. Embedded Classes sind eine echte Komposition im UML-Sinn. Leider nicht für OneToMany Beziehungen. create table "PMQ" ( id bigint not null identity primary key, -- ... name varchar( 32 ) not null ); Arno Schmidhauser November 2006 Seite 59 JPA – Java Persistence API Sekundär-Tabellen für Entities • Gelegentlich wird eine Entity in der Datenbank in mehrere Tabelle hineinmodelliert, oder muss umgekehrt aus mehreren Tabellen zusammengesetzt werden. • Eine Entity kann beliebig viele Sekundärtabellen haben: @Entity @Table( name = "Message" ) @SecondaryTable( name = "MessageExt", pkJoinColumns = { @PrimaryKeyJoinColumn(name="idRef", referencedColumnName = "id") } ) public class Message { // ... 60 Sollen mehrere Sekundärtabellen verwendet werden, muss die Annotation @SecondaryTables( {@SecondaryTable(...), @SecondaryTable(...), ... } ) verwendet werden. Für jedes Feld in der Klasse muss die Herkunft spezifiziert werden, beispielsweise: public class Message { @Column( table="Message", name="sender" ) protected String sender; @Column( table="MessageExt", name="receiver" ) protected String receiver; //... create table "Message" id bigint ( not null identity primary key, -- ... ); create table "MessageExt" idRef bigint ( not null primary key, -- ... ); Tests mit einer Vererbungshierarchie und einer Sekundärtabelle für die Basisklasse waren nicht erfolgreich in Hibernate. Ohne Vererbung klappten die Tests jedoch einwandfrei. Arno Schmidhauser November 2006 Seite 60 JPA – Java Persistence API Mapping von SQL-Views • Komplexe Abfragen werden in einer Datenbank gerne als (materialisierte-) View vorgehalten. Um Performance-Vorteile zu nutzen, kann eine View wie eine Entity angesprochen werden. Beispiel: @Entity public class MessageStatistics { @Id protected long id; @Column( insertable=false, updatable=false) protected int numQueues; // ... create view MessageStatistics ( id, numQueues ) as select m.id, count(*) from Message m left outer join PMQ_Message pm on (…) group by m.id 61 Obiges Beispiel berechnet, in wievielen PMQ's eine Message vorkommt. Die Tabelle PMQ_Message ist die Assoziationstabelle zwischen Message und PMQ. Die Spalten der Klasse sind read only gehalten, indem sowohl das Einfügen, wie der Update verboten ist. Java-technisch müsste man das Erzeugen neuer Statistikobjekte noch durch einen entsprechend privaten Konstruktor verbieten. Arno Schmidhauser November 2006 Seite 61 JPA – Java Persistence API Callbacks • Callbacks sind eine gängige Methode, um Einfluss auf den Lade- oder Speichervorgang von Objekten zu nehmen. • Callbacks werden vom Entwickler definiert, aber vom JPA Framework aufgerufen. Beispiel: @Entity public class ImageMessage extends Message { @PrePersist @PreUpdate protected void compress(){ ... } @PostLoad @PostUpdate protected void uncompress() { ... } 62 Folgende Callback-Annotations sind spezifiziert: • • • • • • • @PrePersist: Vor der entsprechenden Datenbank-Operation, nicht zwingend synchron zur persist()-Methode. Unmittelbar vor flush(). @PostPersist: Nach der entsprechenden Datenbank-Operation. Unmittelbar nach flush(). Ein generierter Primärschlüssel ist in dieser Methode verfügbar. @PreRemove: Vor der entsprechenden Datenbank-Operation, nicht zwingend synchron zur remove()-Methode. Unmittelbar vor flush(). @PostRemove: Nach der entsprechenden Datenbank-Operation. Unmittelbar nach flush(). @PreUpdate: Vor der entsprechenden Datenbank-Operation. Unmittelbar vor flush(). @PostUpdate: Nach der entsprechenden Datenbank-Operation. Unmittelbar nach flush(). @PostLoad: Nach implizitem Refresh, oder aufruf der refresh()-Methode. Die Callbacks werden auch für Objekte, die via kaskadierten Aufruf von persist(), remove() und refresh() betroffen sind, ausgeführt. Callbacks sind void und haben keinen Übergabeparameter. Sie können public, protected oder private sein. Arno Schmidhauser November 2006 Seite 62 JPA – Java Persistence API Foreign Key Constraints • Änderungsoperation von einem Peristenzframework erzeugen oft Konflikte mit Fremdschlüsselbedingungen, wenn Datensätze in der falschen Reihenfolge gelöscht werden. • Hibernate arbeitet mit einer sogenannten "Write Behind Queue" : SQL-Befehle werden so geordnet, dass die Löschung der abhängigen Datensätze zuerst erfolgt. 63 Arno Schmidhauser November 2006 Seite 63 JPA – Java Persistence API DDL Angaben in Annotations • Die Erzeugung von DDL kann, aber muss nicht durch eine Implementation der JPA Spez. angeboten werden. • In der JPA Spez. sind verschiedene Angaben (Annotations und Attribute davon) vorgesehen, welche die Erzeugung von DDL-Befehlen ermöglichen. Beispiel: public class Message { @Column (unique = false, nullable=false, columnDefinition="varchar", lenghth=64 ) public BigDecimal sender; // ... 64 In Hibernate kann man via folgende Einstellungen in persistence.xml die Erstellung von DDL-Code beeinflussen. DDL-Code soll von Hibernate jedesmal neu ausgeführt werden in der Datenbank: <property name="hibernate.hbm2ddl.auto" value="create"/> DDL-Code soll bei Änderungen von Hibernate ausgeführt werden: <property name="hibernate.hbm2ddl.auto" value="update"/> Arno Schmidhauser November 2006 Seite 64 JPA – Java Persistence API Aufgaben • Erstellen Sie ein Listenmapping für die Messages einer PMQ und untersuchen Sie, welche SQL-Befehle erzeugt werden, wenn einer Liste eine neue Message angefügt wird. Zum Tracen von SQL-Befehlen das File /etc/log4j.properties aus dem Hibernate Home in das Applikationsverzeichnis kopieren und anpassen: log4j.logger.org.hibernate.SQL=trace log4j.logger.org.hibernate.type=trace • Erstellen Sie eine eingebettete Klasse, zum Beispiel für den Owner einer PMQ. 65 Arno Schmidhauser November 2006 Seite 65 JPA – Java Persistence API V Objektverwaltung und Transaktionsmanagement 66 Arno Schmidhauser November 2006 Seite 66 JPA – Java Persistence API Grundsätze Der Transfer von Objekten von und zur Datenbank erfolgt automatisch: so spät wie möglich Æ Lazy Access. Der Transfer von Objekten von und zur Datenbank kann manuell erzwungen werden Æ synchron zum Aufruf. Selbstverständlich gilt ein Transaktionsmodell: Der Zugriff auf Objekte erfolgt ab Beginn der Transaktion, die Synchronisation mit der Datenbank wird spätestens beim Commit abgeschlossen und unterliegt der ACID-Regel. Auf Objekte kann auch ausserhalb von Transaktionen zugegriffen werden, jedoch ohne Konsistenz- und Synchronisationsgarantie. 67 Arno Schmidhauser November 2006 Seite 67 JPA – Java Persistence API Objektzustand • Objekte haben vier mögliche Zustände: – new Objekt ist neu erzeugt, hat noch keinen Zusammenhang mit der Datenbank und noch keine gültige ID. – managed Das Objekt hat eine Entsprechung in der Datenbank. Änderungen werden vom Entity Manager automatisch getracked und mit der DB abgeglichen. – detached Das Objekt hat eine Entsprechung in der Datenbank, wurde aber abgekoppelt. Der Zustand wird nicht mehr automatisch abgeglichen mit der Datenbank. – removed Das Objekt existiert noch, ist aber zum Löschen markiert. 68 Ein Objekt ist new wenn es java-mässig instantiiert, aber noch nicht mit persist() bearbeitet wurde. Nach dem persist() ist es managed. Der Aufruf von remove() hat für ein new-Objekt keine Auswirkungen, für ein detached-Objekt wird eine IllegalArgumentException geworfen, für ein managedObjekt wird effektiv der Zustand auf remove gesetzt. Ein Objekt wird detached nach einem Rollback, durch Serialisierung, durch Schliessen des Entity Managers, oder durch Aufruf der clear() Methode des Entity Managers. Eine Methode zum Feststellen des Objektzustandes exisitiert nicht. Arno Schmidhauser November 2006 Seite 68 JPA – Java Persistence API Fragen zur Objektverwaltung 1. Ab wann hat ein neues Objekt eine gültige ID (Wenn die ID via Datenbank-Resourcen vergeben wird und nicht manuell)? 2. Zu welchen Zeitpunkten wird der Objektzustand von der Datenbank eingelesen? 3. Zu welchen Zeitpunkten wird ein geändertes Objekt in die Datenbank zurückgeschrieben? 4. In welchem Zustand ist ein Objekt nach dem Commit? Kann es nach der Transaktion weiterverwendet werden? 5. Was passiert mit einem Objekt beim Rollback? 6. Können persistente Objekte serialisiert und von einer Applikation in eine anderen transportiert werden? 69 Siehe auch Kapitel 3.2 JPA Spezifikation. Arno Schmidhauser November 2006 Seite 69 JPA – Java Persistence API Objekt ID • Ein neues Objekt bekommt erst eine ID, wenn es das erste Mal physisch in die Datenbank transportiert wird. Message m = new Message(...) pmq.append( m ); System.out.println( m.getId() ) Æ Java Default 0 ! em.flush() System.out.println( m.getId() ) Æ gültige ID, z.B. 1 • Die persist()-Methode ist eine logische Operation. 70 @Entity public class PMQ implements Serializable { @Id protected long id; protected String qname; @OneToMany( cascade={ CascadeType.PERSIST } ) protected List<Message> messages = new Vector<Message>(); @Entity public class Message { @Id @GeneratedValue long id; // ... public long getId() { return id; } // ... } Arno Schmidhauser November 2006 Seite 70 JPA – Java Persistence API Einlesen • Der Objektzustand wird beim ersten Zugriff auf das Objekt eingelesen. • Wenn FetchType.EAGER gesetzt ist, werden referenzierte Objekte ebenfalls mitgeladen. • Wenn FetchType.LAZY gesetzt ist, werden referenzierte Objekte beim ersten Gebrauch eingelesen. • Der Objektzustand wird nie automatisch aufgefrischt, nur via die EntityManager.refresh()-Methode. • Eine neue Transaktion führt nicht automatisch zum erneuten Einlesen bestehender Objekte. 71 Der FetchType wird bei den Annotationen @Basic, @OneToOne, OneToMany etc. als Attribut angegeben, zum Beispiel: @Entity public class PMQ { @ManyToMany( fetch = FetchType.EAGER ) protected List<Message> messages = new Vector<Message>(); @OneToOne( fetch = FetchType.EAGER ) protected Owner owner; } @Entity public class Message { @ManyToMany( mappedBy = "messages", fetch = FetchType.EAGER ) protected List<PMQ> pmqs = new Vector<PMQ>(); } Defaultwerte für FetchType: OneToOne EAGER OneToMany LAZY ManyToOne EAGER ManyToMany LAZY Der Gebrauch von refresh() bezüglich Isolationsgrad in der Datenbank muss kritisch betrachtet werden: Bei Isolationsgrad READ_COMMITTED bringt die Methode keine absolute Sicherheit bezüglich Aktualität und Synchronisation mit der Datenbank, bei Isolationsgrad REPEATABLE_READ ist refresh() überflüssig. Für Applikationen, die nie terminieren und die periodisch Daten lesen und anzeigen/weitergeben kann die Funktion jedoch nützlich sein. Arno Schmidhauser November 2006 Seite 71 JPA – Java Persistence API Zurückschreiben • Das Zurückschreiben findet zum Commit-Zeitpunkt oder explizit mit der flush() Methode statt. • Das Zurückschreiben beinhaltet kein Refresh allfälliger Änderungen in der Datenbank in der Zwischenzeit. • Das Zurückschreiben betrifft nur Änderungen, nicht ungeänderte Daten. Applikation m.setContent("A") tx.commit() m.setContent("C") em.flush() Zustand von m in Appl in DB A A A B C C SQL-Client update Message set content = 'B'; commit 72 Bei Hibernate findet der Update so fein wie möglich statt, bei der Änderung eines Feldes eines Objektes, taucht nur dieses Feld im SQL-Update-Befehl auf. Arno Schmidhauser November 2006 Seite 72 JPA – Java Persistence API Objektzustand nach dem Commit, Weiterverwendung des Objektes • Wenn der Persistence Context EXTENDED ist, bleibt ein Objekt im Zustand managed nach dem Commit. – Änderungen nach dem Commit werden aufbewahrt und im Rahmen der nächsten Transaktion in die Datenbank übernommen. • Wenn der Persistence Context TRANSACTION ist, geht ein Objekt in den Zustand detached über nach dem Commit. – Änderungen müssen mit EntityManager.merge() innerhalb der nächsten Transaktion wieder eingekoppelt werden. 73 Der Persistence Context TRANSACTION ist der Normalfall bei Container Managed Entities. Der Persistence Context EXTENDED ist der Normalfall bei Java Standard Applications. Ein Objekt im Zustand detached ist vom EntityManager abgekoppelt. Es kann als eine Art Transfer-Objekt angesehen werden. Es kann serialisiert von einer Applikation in eine andere transportiert werden. Die EntityManager.merge()-Operation erlaubt das Einkoppeln des Objektes in eine Datenbank beim Zielsystem. Das Objekt wird nach folgenden Regeln eingekoppelt: • existiert ein Objekt mit derselben ID wird der Zustand des detached Objekt übernommen. • exisitiert die ID nicht, wird ein neues Objekt mit einer neuen ID angelegt und der Zustand des detached Objektes übernommen. • Weitere Fälle siehe Kap. 3.2.4.1 Wurde das Objekt in der Datenbank zwischen dem Detach und dem Merge geändert sind zwei Resultate möglich beim Commit der Transaktion: • Das Objekt enthält eine Versionsnummer (@Version-Annotation) zur Versionenkontrolle. In diesem Fall wird ein Fehler ausgegeben. • Das Objekt enthält keine Versionsnummer, dann wird der Zustand des gemergeden Objektes in die DB zurückgeschrieben. Es entsteht somit ein LostUpdate. Arno Schmidhauser November 2006 Seite 73 JPA – Java Persistence API Objektzustand nach dem Rollback • Nach einem Rollback ist jedes noch vorhandene Objekt im Zustand detached. Die Belegung der Felder wird durch den Rollback nicht geändert, jedoch der Zustand in der Datenbank. Æ Mögliche Inkonsistenzen. Æ Objekte via Methoden des Entity Manager neu laden: find(), getReference(), createQuery() usw. 74 Arno Schmidhauser November 2006 Seite 74 JPA – Java Persistence API Serialisierung und Transport • Objekte können zwecks Weitergabe in andere Persistenzkontexte oder Applikationen serialisert werden. FileOutputStream fos = new FileOutputStream( "myfile.ser" ); ObjectOutputStream os = new ObjectOutputStream( fos ); os.writeObject( pmq ); os.close(); 75 Für den Serialisierungvorgang wird ein Objekt nicht von der Datenbank geladen. Die Serialisierung funktioniert als nur, wenn die zu serialisierenden Objekte bereits im Cache (Persistence Context) sind. Auf logischer Ebene sichergestellt ist dies gemäss EJB Spezifikation (Kap 3.2.4) nur, wenn alle Beziehungen mit fetch=FetchType.EAGER deklariert sind. Arno Schmidhauser November 2006 Seite 75 JPA – Java Persistence API Serialisierung eines Objektes: FileOutputStream fos = new FileOutputStream( "myfile.ser" ); ObjectOutputStream os = new ObjectOutputStream( fos ); os.writeObject( pmq ); os.close(); Die zu serialisierenden Objekte müssen im Cache vorhanden sein. Ein geeigneter FetchType ist zu wählen: @Entity public class PMQ implements Serializable { @ManyToMany( fetch=FetchType.EAGER ) protected List<Message> messages = new Vector<Message>(); @OneToOne( fetch=FetchType.EAGER ) protected Owner owner; … @Entity public abstract class Message implements Serializable { @ManyToMany( mappedBy = "messages", fetch=FetchType.EAGER ) protected List<PMQ> pmqs = new Vector<PMQ>(); … Die serialisierten Objekte sind unabhängig vom verwendeten Persistenz-Framework (Hibernate), und können beispielsweise wie folgt benützt werden: //deserialize pmq and related ojects FileInputStream fis = new FileInputStream( args[0] + ".ser" ); ObjectInputStream is = new ObjectInputStream( fis ); PMQ pmq = (PMQ) is.readObject(); is.close(); Iterator<Message> it = pmq.getAll().iterator(); while( it.hasNext() ) { Message m = it.next(); System.out.println( m.toString() ); } Die serialisierten Objekte können auch wieder in die Datenbank eingekoppelt (abgeglichen oder neu erzeugt) werden, mit Hilfe der merge() Operation. emf = Persistence.createEntityManagerFactory("PMQ"); em = emf.createEntityManager(); tx = em.getTransaction(); tx.begin(); FileInputStream fis = new FileInputStream( args[0] + ".ser" ); ObjectInputStream is = new ObjectInputStream( fis ); PMQ pmq = (PMQ) is.readObject(); em.merge( pmq ); is.close(); tx.commit(); Damit der Merge kaskadiert arbeitet, muss der entsprechende CascadeType angegeben werden: @Entity public class PMQ implements Serializable { @ManyToMany( cascade={ CascadeType.MERGE } ) protected List<Message> messages = new Vector<Message>(); … Arno Schmidhauser November 2006 Seite 76 JPA – Java Persistence API Fragen zum Transaktionsmanagement 1. Ist der Objektzustand nach dem Lesen in die Applikation in der Datenbank eingefroren, währenddem die Transaktion läuft (Mit welchem Isolationsgrad wird gearbeitet)? 2. Ein Objekt wird geändert: Zu welchem Zeitpunkt wird es in der Datenbank gesperrt? 3. Kann entdeckt werden, ob Daten beim Commit in der Zwischenzeit von anderen Prozessen geändert wurden auf der DB? 77 Siehe auch Kapitel 3.4 JPA Spezifikation. Arno Schmidhauser November 2006 Seite 77 JPA – Java Persistence API Isolationsgrad • JPA arbeitet standardmässig im Isolationsgrad READ COMMITTED. Æ Gelesene Daten sind während der Transaktion nicht eingefroren. Æ Die Konsistenzprobleme Lost Update und Phantom können auftreten. 78 Die Spezifikation geht von einem Isolation Level 1 (READ COMMITTED) aus, respektive einem anderen Isolationsgrad, der keine langen Lesesperren hält. Das Setzen von strengeren Isolationsgraden über die EJB-Spez. ist nicht möglich. Hibernate erlaubt jedoch den Durchgriff auf die JDBC-Ebene und damit, abgesehen vom Abholen der JDBC-Connection, einen im Java-Umfeld standardisierten Zugriff auf Transaktionsbefehle. In Hibernate kann der Isolationsgrad entweder entweder via persistence.xml eingestellt werden: <property name="hibernate.connection.isolation" value="8" /> oder via Anzapfen der JDBC-Session: import java.sql.Connection; import org.hibernate.Session; import org.hibernate.ejb.EntityManagerImpl; // Create EntityManagerFactory for a persistence unit called pmq. emf = Persistence.createEntityManagerFactory("PMQ"); // create EntityManager em = emf.createEntityManager(); tx.begin() Connection con = ((EntityManagerImpl)em).getSession().connection(); con.setTransactionIsolation( Connection.TRANSACTION_SERIALIZABLE ); //… tx.commit() Arno Schmidhauser November 2006 Seite 78 JPA – Java Persistence API Sperrzeitpunkt • Daten werden in der Datenbank im Rahmen von SQLBefehlen gesperrt. • Da Änderungen vorerst lokal durchgeführt und erst zum Commitzeitpunkt in die DB propagiert werden, finden das Sperren erst beim Commit statt. Æ Allfällige Wartesituationen, Deadlocks, Integritätsverletzungen treten effektiv erst zum Commitzeitpunkt auf. 79 Arno Schmidhauser November 2006 Seite 79 JPA – Java Persistence API Explizites Locking • Der Entity Manager stellt einen expliziten lock() Befehl zur Verfügung. • Das Verhalten ist stark implementationsabhängig. • Beispiel: em.lock( pmq, LockModeType.WRITE ) 80 Explizites Locking ist mit EntityManager.lock( ) möglich, ist jedoch sehr unverbindlich definiert. Die Spezifikation verlangt lediglich, dass mit LockModeType.READ ein Lost Update für das gelockte Objekt verhindert wird. Die Implementation muss nicht unbedingt synchron mit dem Aufruf von lock() einen Lock setzen, sie kann grundsätzlich einfach mit Versionierung arbeiten und die Konfliktprüfung damit auf den commit-Zeitpunkt verschieben. Für LockModeType.WRITE ist von der Spezifikation lediglich verlangt, dass ein Lost Update für das gelockte Objekt verhindert wird, sowie, wenn mit Versionierung gearbeitet wird, der Versionenzähler erhöht wird. Für nicht versionierte Objekte besteht von der Spezifikation her kein Unterschied zwischen LockModeType.READ und LockModeType.WRITE. Die Arbeitsweise des lock()-Befehles ist sehr schwammig definiert in der JPA Spez. In Hibernate ist der lock()-Befehl sehr nützlich, weil unmittelbar gelockt wird. Damit ist ein pessimistisches Verfahren synchron zum Applikationsablauf möglich. Daten können lesend gesperrt werden, danach in der Applikation ausführlich bearbeitet werden, ohne dass die Gefahr zwischenzeitlicher Änderungen durch andere Prozesse besteht. Arno Schmidhauser November 2006 Seite 80 JPA – Java Persistence API Beispiel, wie Hibernate LockModeType.READ in einen SQL-Befehl umsetzt: select id from PMQ with (updlock, rowlock) where id =? and version =? Der SQL-Befehl verwendet den Primärschlüssel und das Versionenattribut (dieses ist deshalb notwendig bei Hibernate, ansonsten arbeitet das Locking nicht). Der gelesene Primärschlüssel wird nicht wirklich verwendet, sondern es geht nur darum, mit dem select-Befehl die Sperre auszulösen. Der Befehl wurde von Hibernate spezifisch für SQL-Server 2005 generiert. Da die Klausel with (updlock, rowlock) produktspezifisch ist, muss sie von Hibernate jeweils für einen bestimmten SQLDialekt generiert werden. Hibernate kennt den SQL-Dialekt aus dem Konfigurationsfile persistence.xml. Mit obigem SQL-Befehl sind auch Deadlocks ausgeschlossen, weil nicht nur ein Read-Lock sondern einen Update-Lock (updlock) gesetzt wird. Der SQLBefehl zur Realisierung von LockModeType.WRITE läuft ebenfalls über den Primärschlüssel und das Versionenattribut: update PMQ set version=? where id=? and version=? Bei diesem Update-Befehl wird gleichzeitig die Versionsnummer um 1 erhöht. Arno Schmidhauser November 2006 Seite 81 JPA – Java Persistence API JPA Versionierung • Die Versionierung im Rahmen von JPA ist als applikatorisches Locking zu verstehen. Mit der Versionierung kann ein Concurrency Control über Transaktion hinweg realisiert werden. • Mit einer Versionsnummer können Lost Updates detektiert und vermieden werden: @Entity public class PMQ { @Version protected long version; // ... create table "PMQ" ( version bigint not null, // ... 82 Die Versionennummer wird beim Lesen von Objekten mitgenommen. Beim Zurückschreiben wird der Wert im Objekt mit dem Wert in der Datenbank verglichen. Sind die beiden nicht gleich, wird die Transaktion zurückgesetzt und eine OptimisticLockException ausgeworfen. Das Versionierungsattribut darf nicht durch die Applikation geändert werden. Als Datentyp für die Versionierung kommen int, Integer, long, Long, short, Short und java.sql.Timestamp in Frage. Die Zahlentypen sind vorzuziehen, da nur diese garantiert monoton aufsteigend sind. Es können gleichzeitig versionierte und nicht-versionierte Entities existieren. Mit der @Version-Annotation wird für diese Entity das Versioning automatisch eingeschaltet. Der Begriff "Version" ist leicht missverständlich, weil nicht mehrere Zustandsversionen, sondern nur die Nummer einer einzigen, aktuellen Version verwaltet wird. Dies im Gegensatz zu einigen Datenbanksystemen, welche mit mehreren, vollständigen Versionen desselben Datensatzes arbeiten können. Der Versionierungsmechanismus und die Versionsprüfung tritt automatisch in Kraft, wenn ein Versionenfeld definiert ist. Die Versionierung ist ein sehr einfaches applikatorisches Verfahren, das mögliche Konflikte beim Zurückschreiben detektiert. Ein anderes applikatorisches Verfahren, dass Konflikte verhindert, ist das Checkout/Checkin: Liest eine Applikation einen Datensatz, setzt sie gleichzeitig für eine Marke, dass der Datensatz in Gebrauch ist. Ist diese Marke gesetzt, verpflichten sich andere Applikationen, den Datensatz nur lesend oder gar nicht in Gebrauch zu nehmen. Das Verfahren ist sehr flexibel, die Marke kann um Informationen über den Benützer und den Zeitpunkt des Checkout ergänzt werden. Nachteile: Jedes Lesen erfordert auch einen Schreibvorgang (Setzen der Marke). Stürzt die besitzende Applikation ab, kommt es nie zu einem Checkin. Nach dem Checkout muss sofort ein Commit durchgeführt werden, sonst sind Lesevorgänge technisch blockiert. Arno Schmidhauser November 2006 Seite 82 JPA – Java Persistence API Aufgaben • Entwickeln Sie die Producer und Consumer-Klasse für eine Message Queue so, dass Producer und mehrere Consumer gleichzeitig laufen können. Es muss ein korrekter Ablauf im Sinne der Queue Semantik garantiert sein: Jede Message wird nur einmal gelesen und angezeigt. 83 Arno Schmidhauser November 2006 Seite 83 JPA – Java Persistence API VI JPA Query Language 84 Der offizielle Name ist Java Persistence API Query Language. Der Name EJB QL ist für EJB Version 2 reserviert. Arno Schmidhauser November 2006 Seite 84 JPA – Java Persistence API Konzept 1. Auf das EJB-Modell ausgerichtete Abfragesprache zum Suchen und Bearbeiten von Entities, "SQL-Mapper". 2. Abfragen operieren auf dem Klassenmodell aller Entitäten, nicht dem SQL-Datenmodell. 3. Gegenüber EJB QL 2.1 wesentliche Erweiterungen: – direkte Update- und Delete-Operationen – OUTER |INNER JOIN, GROUP BY, HAVING Klauseln – Projektionen (Abfrage einzelner Attribute resp. Ausdrücke in der Select-Klausel) 85 Zu Punkt 2: Im Objektmodell gibt es keine Fremdschlüssel (in der DB schon, aber diese sind in der Query Language nicht verwendbar). Es können also nur Felder/Beziehungsattribute verwendet werden, die im Klassenmodell vorhanden sind. Will man beispielsweise alle Messages finden, die zu keiner PMQ gehören, so sind die Abfragen unterschiedlich, je nachdem ob zwischen PMQ und Message eine unidirektionale oder eine bidirektionale Beziehung definiert wurde (das relationale Modell ist für beide Fälle dasselbe). Abfrage bei unidirektionaler Beziehung von PMQ zu Message: select m from Message m where m not in ( select m2 from PMQ q JOIN q.messages m2 ) Abfrage bei bidirektionaler Beziehung zwischen PMQ und Message: select m from Message m where m.pmq is null Arno Schmidhauser November 2006 Seite 85 JPA – Java Persistence API Query erzeugen • Ausgangspunkt für Abfragen ist der Entity Manager mit den Funktionen: Query createQuery( String query ) Query createNamedQuery( String qryname ) Query createNativeQuery( String sql, String mapname ) 86 Die erste Funktion ist sicherlich die häufigste Form. Sie übernimmt einen Query Text in der JPA Query Language (JPA QL). In der zweiten Form kann im Java Source Code in Form einer Annotation ein Named Query definiert werden, beispielsweise: @NamedQuery( name="orphans", query=" select m from Message m where m.pmqs is empty " ) Auf dieses Query kann mit createNamedQuery( "orphans" ) Bezug genommen werden. Anwendung: Wenn dasselbe Query häufig gebraucht wird. Die dritte Form ist für die Ausführung von direktem SQL, mit definiertem Mapping auf Felder einer Klasse: createNativeQuery( "select id, sender, receiver from Message", "myMapping" ) @SqlResultSetMapping( name="myMapping", entities={ @EntityResult( entityClass=pmq.Message.class, fields={ @FieldResult( field="id", column="id" ), @FieldResult( field="sender", column="sender" ), @FieldResult( field="receiver", column="receiver" ) } )} ) Arno Schmidhauser November 2006 Seite 86 JPA – Java Persistence API Query, Beispiele • Unparametrisiertes Query Query q = em.createQuery( "select m from Message m where m.sender = 'Rolf'") • Parametrisierte Queries Query q = em.createQuery( "select m from Message m where m.sender = :sender") Query q = em.createQuery( "select m from Message m where m.sender = ?1") 87 Arno Schmidhauser November 2006 Seite 87 JPA – Java Persistence API Query, Parameterübergabe • Übergabe eines Namensparameters Query.setParameter( string name, Object value ) Beispiel q.setParameter( "sender", "Rolf" ); • Übergabe eines Positionsparameters Query.setParameter( int pos, Object value ) Beispiel q.setParameter( 1, "Rolf" ); 88 Für die Übergabe von Datums- und Zeitwerten existieren noch vier weitere Methoden, um festzulegen, welche Teile eines Datums-/Zeitwertes effektiv im Query verwendet werden sollen. setParameter(int pos, Calendar value, TemporalType temporalType) setParameter(int pos, Date value, TemporalType temporalType) setParameter(String name, Calendar value, TemporalType temporalType) setParameter(String name, Date value, TemporalType temporalType) Auch ganze Objekte dürfen Parameter sein. Arno Schmidhauser November 2006 Seite 88 JPA – Java Persistence API Query, Ausführung • Einschränken der Resultatmenge: setFirstResult( int pos ) setMaxResults( int max ) • Abholen des Resultates List getResultList() Object getSingleResult() int executeUpdate() • Synchronisation setFlushModeType( FlushModeType type ) 89 Die Methoden zum Einschränken der Resultatmenge können unter Umständen performance-kritisch sein. Es ist abzuklären, ob die zugrundeliegende Datenbank entsprechende SQL-Ergänzungen (beispielsweise select top ... ) vertsteht, welche bereits datenbankseitig zum Eliminieren der nicht gewünschten Objekte führen. Im Regelfall werden die Objekte mit getResultList als java.util.List abgeholt. Eine Liste ist für sortierte Abfragen der einzig korrekte Datentyp. Für nicht sortierte Abfragen ist eine Liste nicht störend. Einfachheitshalber wurde deshalb auf eine allgemeine Collection als Rückgabetyp verzichtet. Enthält die Abfrage atomare Datentypen, beispielsweise in "select m.priority from Message m" werden die atomaren Typen in die entsprechenden Objekttypen umgewandelt. Enthält die select-Klausel mehrere Werte oder Objekte, wird eine Liste von Arrays zurückgegeben. Mit getSingeResult() wird ein Abfrageresultat abgeholt, dass nur ein einziges Resultat liefert. Erzeugt eine Exception, wenn kein oder mehr als ein Resultat entsteht. Mit executeUpdate() können modifizierende Abfragen (update, delete) durchgeführt werden. Der FlushModeType ist wichtig, wenn vor der Query-Ausführung neue persistente Objekte erzeugt, gelöscht oder modifiziert wurden. Wenn der FlushModeType auf AUTO gesetzt ist, findet vor der Ausführung eine Synchronisation (flush()) zwischen dem Cache und der Datenbank statt, weil das Query gegen die Datenbank abgesetzt wird. Wenn der FlushModeType auf COMMIT gesetzt ist, findet keine Synchronisation statt, und das Query wird gegen den momentan in der Datenbank existierenden Zustand ausgeführt, ohne Berücksichtigung noch pendenter Änderungen der laufenden Transaktion. Arno Schmidhauser November 2006 Seite 89 JPA – Java Persistence API Query, Resultate abholen Query qry = em.createQuery( qrystring ); List result = qry.getResultList(); Iterator itm = result.iterator(); while( itm.hasNext() ) { Object o = itm.next(); if ( o instanceof Object[] ) { for( Object x : (Object[])o ) System.out.println( x.toString() ); } else{ System.out.println( o.toString() ); } } 90 Mit obigem Code-Stück kann jede Art von Query-Resultat abgeholt werden. Arno Schmidhauser November 2006 Seite 90 JPA – Java Persistence API Abfragen, Aufbau • Abfragen in JPA QL folgen demselben Befehlsaufbau wie SQL: select selectklausel from fromklausel [ group by groupklausel ] [ having havingklausel ] [ where whereklausel ] [ order by orderklausel ] 91 In der Selectklausel kann eine Objektreferenz stehen (die in der Fromklausel definiert wird), ein Pfadausdruck, ein Aggregatausdruck oder ein Wertausdruck oder eine Kombination der drei letzteren. Die Fromklausel enthält die Grundmengen an Objekten mit einem Referenznamen (der in JPA QL obligatorisch ist) . Beispiel: from PMQ q . Werden mehrere Grundmengen angegeben ohne Join-Bedingung, wird das Kreuzprodukt gebildet wie in SQL. Die Groupklausel, die Havingklausel und die Orderklausel arbeiten sinngemäss wie in SQL. Insbesondere sind auch Pfadausdrücke erlaubt und die Orderklausel erlaubt die Zusätze ASC und DESC für die Sortierung. Zu erwähnen ist, dass keine der Klauseln Methodenaufrufe auf den Objekten erlaubt. Andere OO-Abfragensprachen, z.B. JDO Query Language erlauben dies. Die Whereklausel kennt die üblichen Vergleiche und Operatoren: =, >, >=, <, <=, <> (not equal), [NOT] BETWEEN, [NOT] LIKE, [NOT] IN, IS [NOT] NULL, AND, OR, NOT, (), [NOT] EXISTS. In der Whereklausel sind die speziellen Prädikate IS [NOT] EMPTY und [NOT] MEMBER OF zu erwähnen. Beide beziehen sich auf Pfadausdrücke, die in Collections enden. Arno Schmidhauser November 2006 Seite 91 JPA – Java Persistence API Pfadausdrücke 1. Ein Pfadausdruck ermöglicht die direkte Navigation von einem äusseren zu inneren, referenzierten Objekten: select q.owner from PMQ q select q.owner.name from PMQ q 2. Ein Pfadausdruck kann in einer Collection enden: select q.messages from PMQ q 3. Ein Pfadausdruck kann nicht über eine Collection hinweg navigieren: select q.messages.sender from PMQ q 92 Zu 1: der Ausdruck nimmt keine Rücksicht auf die Kapselung der inneren Objekte. Auch wenn das Ojekt owner privat ist, kann darauf navigiert werden. Ist ein Objekt im Pfad null, so fällt die entsprechende Zeile aus dem Resultat weg. zu 2: Ein Pfadausdruck, der in einer Collection endet und in einer select-Klausel steht, ist von der JPA Spez. eigentlich nicht erlaubt. Er soll aber hier als Illustration dienen, insbesondere, weil er beispielsweise in Hibernate zugelassen ist. Ein Pfadausdruck, der in einer Collection endet, ist vorallem für die Vewendung in der from- oder der where-Klausel vorgesehen. zu 3: Der Versuch, solche Ausdrücke zu verwenden, ist ein häufiger Fehler. Bei genauerer Überlegung ist jedoch klar, dass das Konstrukt falsch ist. Wenn es zugelassen wäre, ist die Frage, ob sender ein Feld der Collection an sich, oder ein Feld der darin eingebetten Elemente ist. Es würde also eine Mehrdeutigkeit auftreten. Im Weiteren wäre zu bedenken, dass sender wieder eine Collection sein könnte. Damit wiederum kommen Probleme mit im Kreis herum referenzierten Objekten ins Spiel. Aus diesen Gründen wird auf dieses Konstrukt verzichtet in objektorientierten Abfragesprachen (Nicht hingegen im XML-Umfeld: XPath und XQuery bauen genau auf diesen Konstrukten auf). Arno Schmidhauser November 2006 Seite 92 JPA – Java Persistence API Join-Abfragen • Ein Join von zwei Grundmengen bedingt eine definierte Beziehung in den beiden Klassen. Beispiele: Alle Messages der PMQ q1 select m from PMQ q join q.messages m where q.qname = 'q1' Alle PMQs mit Messages der Priorität 1 select distinct q from PMQ q join q.messages m where m.priority = 1 Name jeder PMQ mit Absender aller Messages select q.qname, m.sender from PMQ q left join q.messages m 93 Im ersten Beispiel sind Message-Objekte gesucht. Der rechte Teil der Join-Operation entspricht einer SQL-Join-Bedingung zwischen PMQ und Message. Im zweiten Beispiel sind PMQ-Objekte gesucht. Zu beachten das Schlüsselwort distinct, damit jede PMQ nur einmal in das Resultat kommt. Im drittten Beispiel ist die Möglichkeit eines Outer Joins illustriert. Arno Schmidhauser November 2006 Seite 93 JPA – Java Persistence API in und exists • Die Operatoren IN und EXISTS sind ähnlich wie in SQL, in der Unterabfrage können auch Pfadausdrücke verwendet werden: • Suche alle Queues mit Meldungen der Priorität 1 select q from PMQ q where exists ( select m from q.messages m where m.priority = 1 ) • Suche alle Messages, die zu einer PMQ von Rolf gehören select m from Message m where m in ( select m from PMQ q JOIN q.messages where q.owner.name = 'Rolf' ) 94 Arno Schmidhauser November 2006 Seite 94 JPA – Java Persistence API is empty, member of • is empty und member of ergeben gut verständliche Abfragen auf Pfadausdrücken, die in Collections enden: 1. Suche leere PMQs select q from PMQ q WHERE q.messages IS EMPTY 2. Suche nicht-leere PMQs select q from PMQ q WHERE q.messages IS NOT EMPTY 3. Suche PMQ's in denen die Message m vorkommt Message m = ... ; // irgendwoher Query qry = em.createQuery( "select q from PMQ q where ?1 member of q.messages" ); qry.setParameter( 1, m ); List result = qry.getResultList(); 95 Grundsätzlich sind die beiden Operatoren nicht zwingend notwendig: Beispiel 1 könnte man ersetzen durch select q from PMQ q where not exists ( select m from q.messages ) Beispiel 2 könnte man ersetzen durch select distinct q from PMQ q join q.messages m oder select q from PMQ q where exists ( select m from q.messages ) Beispiel 3 könnte man ersetzen durch select distinct q from PMQ q join q.messages Arno Schmidhauser November 2006 m where m = ?1 Seite 95 JPA – Java Persistence API Abfragen in Vererbungshierarchien • Alle Objekte einer Klasse und ihrer Unterklassen: select m from Message m • Alle XMLMessages in einer PMQ: select xm from XmlMessage xm where xm in ( select m from PMQ q JOIN q.messages m where q.qname = 'q1' ) • Alle Messages, die konkret SimpleMessages sind: select m from Message m where m in ( select mm from SimpleMessage mm ) 96 Ein instanceof Operator wäre nützlich, existiert aber nicht. Folgende Abfrage für das Auffinden von Attributen aus SimpleMessage ist möglich: select m.sender, m.receiver, s.content from Message m, SimpleMessage s where s.id = m.id Arno Schmidhauser November 2006 Seite 96 JPA – Java Persistence API Erstaunliches ... • Die folgenden Abfragen liefern unter Hibernate (JBoss) ein gültiges Resultat: select distinct q from PMQ q where q.messages.sender = 'Bob' and q.messages.sender = 'Alice' select distinct q from PMQ q where q.messages.sender = 'Bob' or q.messages.sender = 'Alice' 97 Erstaunlich ist an den beiden Abfragen, dass eine Navigation über Collections hinweg (q.messages.sender) möglich ist. Dadurch entsteht eine nicht ganz klare Semantik: Bedeutet der Vergleich q.messages.sender = 'bob', dass alle bob heissen müssen, oder nur einer? Im vorliegenden Fall wird der Gleichheitsoperator offenbar als 'mindestens ein' interpretiert. Die Navigation über Collections hinweg ist vom Standard nicht vorgesehen. Die zweite der obigen Abfragen wird vom Framework in folgenden Join umgewandelt: select distinct pmq.id, from PMQ pmq, PMQ_Message pm1, Message m1, PMQ_Message pm2, Message m2 where pmq.id=pm2.PMQ_id and pm2.messages_id=m3.id and pmq.id=pm1.PMQ_id and pm1.messages_id=m2.id and ( m1.sender='Alice' or m2.sender='Bob' ) Arno Schmidhauser November 2006 Seite 97 JPA – Java Persistence API Fetch Join • Der Fetch Join erlaubt das Abfragen von Objekten, mit gleichzeitigem Laden assoziierter Objekte. • Der Fetch Join ist Teil der JPA Spez. • Der Fetch-Join ist ein reines Optimierungs-Hilfsmittel. • Der rechte Operand des Fetch Join hat keine Identifikationsvariable und darf in der Abfrage nicht weiter verwendet werden. select q from PMQ q left outer join fetch q.messages where q.qname = 'q1' 98 Der Fetch Join kann sowohl inner wie outer sein. Zu beachten: Die Join-Semantik hat natürlich zur Folge, dass die Abfrage eventuell mehrfach dasselbe Objekt zurückliefert. Ist dies nicht erwünscht, kann mit select distinct gearbeitet werden. Arno Schmidhauser November 2006 Seite 98 JPA – Java Persistence API Update und Delete • Gemäss Spezifikation können auch Update und DeleteOperationen als "Abfragen" durchgeführt werden. Der Hintergrund dürften Performance-Überlegungen sein. • Beispiel: delete from PMQ q where q.qname = 'q1' • die Löschung beinhaltet kaskadierte Löschungen, die im Rahmen von Beziehungen definiert sind. • Update- und Delete Befehle führen nicht automatisch eine Synchronisation zwischen Datenbank und Applikationscontext (Cache) aus. 99 Arno Schmidhauser November 2006 Seite 99 JPA – Java Persistence API Aufgaben • Erstellen Sie ein Abfrage-Applikation, um die Möglichkeiten von JPA Query Language auszuloten. Testen Sie mit: • Abfragen von Objekten mit Bedingungen / Sortierung • Abfragen in der Vererbungshierarchie • Gibt es Abfragen, die Schwierigkeiten bereiten, aber in SQL grundsätzlich möglich wären? 100 Arno Schmidhauser November 2006 Seite 100