Westfälische Wilhelms-Universität Münster Ausarbeitung Persistenzframeworks im Rahmen des Software Engineering-Seminars Georg Senft Themensteller: Prof. Dr. Herbert Kuchen Betreuer: Dipl.-Wirt.-Inf. Christian Hermanns Institut für Wirtschaftsinformatik Praktische Informatik in der Wirtschaft Inhaltsverzeichnis 1 Einleitung ................................................................................................................... 1 2 Grundlagen Persistenzframeworks ............................................................................ 2 3 4 5 2.1 Definition und Einordnung ................................................................................ 2 2.2 Objektrelationales Mapping ............................................................................... 3 2.3 Besondere Aspekte für Persistenzframeworks ................................................... 7 LINQ .......................................................................................................................... 9 3.1 Überblick und Architektur ................................................................................. 9 3.2 Spracherweiterungen für LINQ........................................................................ 10 3.3 Objektrelationales Mapping und Abfragen mit LINQ to SQL ........................ 12 3.4 LINQ to Entities und ADO.NET 3.0 ............................................................... 17 3.5 LINQ to XML .................................................................................................. 18 Hibernate und weitere Alternativen ......................................................................... 19 4.1 Hibernate und JPA ........................................................................................... 19 4.2 NHibernate für .NET und Quarae für Java ...................................................... 21 Fazit und Ausblick ................................................................................................... 22 A Quellcode zu den Abfragen im LINQ to SQL-Beispiel .......................................... 23 Literaturverzeichnis ........................................................................................................ 25 II Kapitel 1: Einleitung 1 Einleitung Der Umgang mit Datenbeständen spielt beim Großteil aller Anwendungssysteme eine wesentliche Rolle. Seien es in einem großen ERP-System die Ergebnisse einzelner Verarbeitungsschritte abgebildeter Geschäftsprozesse oder der Abruf von Adressinformationen aus einer simplen Anwendung zur Kontaktverwaltung; das Erzeugen, Einlesen, Verarbeiten und Speichern von Datenstrukturen stellt einen zentralen Vorgang dar, auf dessen Grundlage die meisten Anwendungsfälle erst ermöglicht werden. Infolgedessen nehmen bei der Entwicklung von Anwendungen die Entwurfsentscheidungen, die sich auf den Umgang mit Datenbeständen beziehen, ebenfalls einen bedeutsamem Stellenwert ein. Zudem bilden bei Entwicklungsprojekten die Implementierungsschritte zu diesem Teilaspekt häufig eine zeitraubende, da oft auch fehleranfällige Aktivität. Daher sind diesbezügliche Erleichterungen gefragt, auch um eine Fokussierung der Entwicklungsaktivitäten auf die Anwendungslogik zu bewirken. Der Einsatz von Persistenzframeworks zielt auf eine derartige Erleichterung ab. Diese Ausarbeitung befasst sich mit der Untersuchung ausgewählter Frameworks, um einen Eindruck zu vermitteln, mit welchen Mechanismen und welchem Grad der ProblemAbdeckung Erleichterungspotenziale durch sie bereitgestellt werden. Das Augenmerk liegt dabei auf den Lösungen zum Zusammenspiel mit relationalen Datenbanken aus der Perspektive der objektorientierten Programmiersprachen C# und Java. Hervorgehoben wird überdies die Untersuchung von Persistenzlösungen im Bereich der Microsoft LINQ-Technologie, weil es sich hierbei um einen vielversprechenden, da sprachintegrierten und zugleich flexiblen Ansatz handelt. Der Gang der Untersuchung beginnt mit der Erklärung und Einordnung des Begriffs des Persistenzframeworks und behandelt das Problemfeld des objektrelationalen Mappings, typischer Lösungsansätze sowie weiterer Aspekte, für die ein Erleichterungspotenzial für die datenbezogene Anwendungsentwicklung wünschenswert ist. Dem schließt sich im Hauptteil die ausführliche Betrachtung von LINQ an. Sowohl die dabei zu Grunde gelegten Spracherweiterungen als auch die Persistenzlösung für Microsoft SQLDatenbanken stehen dabei im Vordergrund. Zum Vergleich erfolgt eine Untersuchung des für Java entwickelten Persistenzframeworks Hibernate, dessen Ablegers für .NET NHibernate und eine kurze Betrachtung des Java-Projektes Quarae. 1 Kapitel 2: Grundlagen Persistenzframeworks 2 Grundlagen Persistenzframeworks 2.1 Definition und Einordnung Der Begriff Framework steht im Allgemeinen für eine Menge von Objekten, die eine generische Lösung für eine Reihe verwandter Probleme implementieren [BBE95]. Auf den Bereich der Softwaretechnik bezogen liefert ein Framework eine wiederverwendbare, für gewöhnlich nicht domänen- sondern konzeptspezifische [SGM02] Menge an Klassen mit Bezug auf eine bestimmte Aufgabe, deren Rollen und Zusammenspiel geregelt sind, und legt auf diese Weise Entwurfsentscheidungen fest. Im Vergleich zu einer Klassenbibliothek werden Stellen zur Erweiterung und Anpassung der Funktionalität der Klassen bereitgehalten und spezifiziert. Frameworks für die objektorientierte Software-Entwicklung bedienen sich dabei insbesondere dem Prinzip der Vererbung, wobei der Entwickler eine abstrakte Framework-Klasse für einen konkreten Anwendungsfall erweitert, sie folglich mit spezifisch implementierten Methoden ausstattet. Hierbei ist die Kenntnis des Zusammenspiels der FrameworkElemente und somit der Einblick in dessen Funktionsweise erforderlich, weswegen dieser Ansatz als White-Box-Wiederverwendung bezeichnet wird und sich von der Black-Box-Wiederverwendung abgrenzt, bei der direkt instanziierbare, uneinsehbare Klassen als Wiederverwendungs-Komponenten zum Einsatz kommen. Häufig werden auch Mischformen beider Verwendungsansätze verfolgt. Das Ausmaß, in dem ein Framework Lösungen für Probleme eines Anwendungsfalls abdeckt, richtet sich nach dem Grad der Standardisierbarkeit der Probleme und dem Umfang, in dem implementierte Lösungen vorliegen [BBE95]. Persistenz bezeichnet die Fähigkeit einer Anwendung, Daten im Sinne von Objekten, deren Zustände und Verbindungen aus dem aktuellen Programmablauf in einen nichtflüchtigen Speicher zu bewegen, mit dem Ziel, sie dort langfristig zu sichern und zu einem späteren Zeitpunkt eine adäquate Wiederherstellung vorzunehmen [BaHel99, Kap. 12]. Im Wesentlichen kommen zur langfristigen Datenhaltung zwei Verfahren infrage: Die Speicherung der Anwendungsdaten in Dateien oder in Datenbanken. Dateien liegen im Dateisystem des Betriebssystems und beinhalten eine Repräsentation der Daten in einem anwendungsspezifischen oder allgemeinen Dateiformat, beispielsweise in Form einer Bytefolge als Ergebnis einer Objektserialisierung oder als XML-Daten. Die Speicherung der Anwendungsdaten in einer Datenbank ist als 2 Kapitel 2: Grundlagen Persistenzframeworks Übergabe an ein zumeist relationales Datenbank-Management-System aufzufassen, das die Datenhaltung übernimmt und der Anwendung unter anderem Schnittstellen zum Lesen und Schreiben der Daten anbietet. Beide Verfahren erfordern jeweils spezielle Mechanismen, die Dateien im entsprechenden Format zu schreiben und zu lesen beziehungsweise die Interaktion mit dem Datenbank-System abzuwickeln, welche von der Anwendung realisiert werden müssen. Ein Persistenzframework stellt dem Anwendungsentwickler derartige Mechanismen bereit, wobei dies durch eine einheitliche Schnittstelle erfolgen kann und auf diese Weise eine Kapselung mit der damit verbundenen Entkoppelung der Anwendung vom spezifischen DatenhaltungsVerfahren angestrebt werden kann. Für die Entwicklung von Geschäftsanwendungen wird in der Praxis häufig der Ansatz einer Drei-Schichten-Architektur verfolgt [DH03, S. 17 ff.], bei der drei SoftwareSchichten nach technischen und logischen Aufgabenbereichen getrennt hierarchisch angeordnet sind [BaHei99, Kap. 10]: Die GUI-Schicht mit der Aufgabe, die Benutzerinteraktion und die Präsentation von Daten zu realisieren, die FachkonzeptSchicht, welche die Abbildung von Geschäftsobjekten sowie -prozessen und damit den funktionalen Kern der Anwendung beherbergt, und die Datenhaltungs-Schicht zur Realisierung der Persistenz. Dabei werden für die Datenhaltung von Geschäftsanwendungen derzeit zumeist relationale Datenbank-Management-Systeme eingesetzt, die durch ihre Eignung für verteilt und parallel zugreifende Anwendungen, Mehrbenutzerumgebungen und den effizienten Umgang mit großen Datenmengen den dateibasierten Methoden weitaus überlegen sind. In dem beschriebenen Szenario kann ein Persistenzframework bei der Implementierung eingesetzt werden, um die Aufgaben der Datenhaltungs-Schicht sowohl durch die Bereitstellung von Funktionalität als auch durch festgelegte Entwurfsmuster zu unterstützen und im Idealfall gar weitestgehend zu übernehmen. 2.2 Objektrelationales Mapping Relationale Datenbanken, basierend auf dem Modell von [Co70], speichern Daten in zweidimensionalen Tabellen, auch Relationen genannt. Die Spalten jener Tabellen werden als Attribute bezeichnet, die Zeilen als Datensatz oder auch Tupel. Attribute liegen dabei typisiert vor und können die Funktion eines Primärschlüssels zur eindeutigen Identifizierung eines Datensatzes innerhalb der Tabelle erfüllen oder auch 3 Kapitel 2: Grundlagen Persistenzframeworks durch Aufnahme des Primärschlüsselwertes eines Datensatzes einer anderen oder derselben Tabelle eine Fremdschlüssel-Beziehung abbilden. Jede Tabelle muss mit mindestens einer Primärschlüssel-Spalte ausgestattet sein. Der Entwurf der Tabellenstruktur folgt dabei den Regeln der Normalisierung, um wohlgeformte Relationen zu entwickeln und Dateninkonsistenzen und -redundanzen zu vermeiden [Ke83]. Als Datenbanksprache hat sich SQL, die Structured Query Language, derzeit in Version 2006, durchgesetzt, die Kommandos zur Datenmanipulation und -abfrage wie SELECT, INSERT, UPDATE und DELETE sowie Befehle zur Definition der Tabellenstrukturen und zur Zugriffskontrolle vorsieht. Indes wird in objektorientierten Programmiersprachen mit vererbbaren Klassen, Methoden und Attributen sowie daraus instanziierten Objekten und Verweisen gearbeitet; einem Paradigma, das sich vehement von dem der normalisierten Relationen unterscheidet und, allein auf die Struktur und die Verbindung von Objekten bezogen, einen deutlich größeren Spielraum bietet. So sind beispielsweise die Prinzipien der Vererbung und Polymorphie im relationalen Modell nicht vorgesehen. Um Objektgraphen datenbankbasiert zu persistieren, bieten sich verschiedene Möglichkeiten an: Neben objektorientierten Datenbanken, die in der Praxis nur spärlich zum Einsatz kommen [Le00], kann man objektrelationale Datenbanken, deren relationaler Ansatz um benutzerdefinierte Datentypen erweitert wurde, oder XML-Datenbanken, die XMLDaten als Attributtyp vorsehen, verwenden. Unter der Prämisse, ein rein relationales Datenbanksystem zu verwenden, bestehen jedoch nur die zwei Vorgehensweisen, entweder Objektserialisierungs-Ergebnisse als einzelnen Attributwert abzulegen oder ein speziell für die Aufnahme der Objektklassen modelliertes Datenbankschema, das Klassen, Attribute und Verweise adäquat abbildet, zu verwenden. Nur die letzte Methode, die auch als objektrelationales Mapping bezeichnet wird, nutzt effektiv die Fähigkeiten des relationalen Datenbankmodells und ermöglicht beispielsweise die effiziente, wertbasierte Suche. Beim objektrelationalen Mapping gilt es, den Impedanz-Unterschied [Am03, Kap. 7] zu überwinden, der sich aus der Verschiedenheit des objektorientierten und des relationalen Modells ergibt. Zwar lassen sich für objektorientiert organisierte Strukturen zum Zwecke der Persistenz relationale Datenstrukturen erstellen, die eine vollständige Abbildung der Daten aufnehmen können, jedoch sind hierfür vielfältige Mechanismen und Konstrukte notwendig, da eine direkte Abbildung in dem Sinne, dass beispielsweise 4 Kapitel 2: Grundlagen Persistenzframeworks eine Klasse ihr vollständiges Pendant in einer Tabelle findet, nur in sehr einfachen Fällen möglich ist. Im Übrigen gelten vergleichbare Probleme auch für die Abbildung von objektorientierten Datenstrukturen in hierarchische XML-Dokumente, deren semistrukturierter Aufbau zwar dem von Objekten näher kommt, jedoch ebenso Mechanismen zur Abbildung in Elemente, Attribute und Dokumente erfordert. Im Folgenden werden die Lösungsansätze des objektrelationalen Mappings anhand der Ausführungen von [BHRS07], [Me08] und [Ke97] beleuchtet: Im Kern folgt das objektrelationale Mapping der Strategie, dass eine Klasse als eine Tabelle modelliert wird. Die zu speichernden Attribute bzw. Eigenschaften der Klasse, die in Form von primitiven Datentypen vorliegen, finden sich als Attribute der Tabelle, also Spalten, wieder. Eine gespeicherte Objektinstanz entspricht dabei einem Datensatz. Attribut-Werte in primitiven Datentypen müssen in entsprechende Datentypen des konkret eingesetzten, relationalen Datenbankmanagementsystems überführt werden, da es sowohl bezüglich der Typensysteme von Java oder .NET/CLR im Vergleich zu SQL als auch beim Vergleich der SQL-Dialekte verschiedener Datenbank-Hersteller keine vollständige Übereinstimmung gibt. Liegt ein zu speicherndes Attribut nicht in Form eines primitiven Datentypes, also als Verweis auf die Objekt-Instanz einer anderen oder derselben Klasse, vor, so hat für diese Klasse, falls noch nicht geschehen, ebenfalls eine Modellierung als Tabelle zu erfolgen. Über eine Fremdschlüsselrelation wird der Bezug hergestellt. Sowohl zu diesem Zweck, als auch zur Beschreibung der eindeutigen Identität von ObjektDatensätzen bei Lese- und Schreiboperationen müssen die Tabellen mit einem Primärschlüssel ausgestattet sein, der wiederum in die abgebildete Klasse als zusätzliches Attribut aufgenommen werden muss. Per se ist dies bei objektorientierten Programmen nicht notwendig, bei relationalen Datenbanken jedoch Pflicht. Weiterhin stellt sich die Frage, welchen Wert ein Primärschlüssel für ein abgebildetes Objekt annimmt, damit unter Einhaltung der referenziellen Integrität eine Eindeutigkeit in der Tabelle und über alle Objekte der Klasse gewährleistet ist. Zwei Ansätze stehen zur Wahl des Identitätswertes zur Verfügung: Der erste Ansatz sieht die Generierung durch das Datenbanksystem vor, wobei dies dort beispielsweise unter Verwendung einer fortlaufenden Nummerierung zuverlässig erfolgt, aber nach Erzeugung des Datensatzes eine zusätzliche Leseoperation zur Ermittlung dieses Wertes vonseiten der Anwendung stattfinden muss. Der zweite Ansatz sieht die Generierung eines pseudo-eindeutigen 5 Kapitel 2: Grundlagen Persistenzframeworks Wertes durch die Laufzeitumgebung der Anwendung vor, denkbar in Java 5 als UUID und in .NET als Guid oder andere Verfahren zur Gewinnung von Zufallszahlen, wobei Kollisionen jedoch nur äußerst unwahrscheinlich sind. Auf die beschriebene Weise lassen sich 1:1- und n:1-Beziehungen realisieren. Über die genannten Fälle der Objekt-Attribute in Form von primitiven oder komplexen Datentypen hinaus verwendet man in objektorientierten Sprachen mengenwertige Attribute wie Listen oder Kollektionen. Stellt deren Verwendung eine 1:n-Beziehung dar, so erfolgt die Abbildung in der Datenbank in umgekehrter Richtung der Abhängigkeitsbeziehung. Insofern nimmt die Tabelle der verwiesenen Klasse ein Fremdschlüssel-Attribut auf, welcher verweisenden Objekte ihre Objekte zugeordnet werden. Auch dies wirkt sich wiederum auf die verwiesene Klasse auf, die dieses zusätzliche Attribut führen muss, was einen aus der objektorientierten Perspektive unnötigen, zyklischen Verweis ergibt. Durch die Umkehrung der Verweisrichtung ist es erforderlich, beim Laden der verweisenden Klasse außerdem passende Einträge aus der Tabelle der Verwiesenen abzufragen. Darüber hinaus ist es bei der Verwendung mengenwertiger Attribute ohne Weiteres denkbar, dass ein Objekt der verwiesenen Klasse in mehreren Instanzen des mehrwertigen Attributes der verweisenden Klasse auftauchen kann. In diesem Fall handelt es sich um eine n:m-Beziehung, deren relationale Abbildung nicht ohne eine Zwischentabelle mit Fremdschlüsseln auf verweisende und verwiesene Datensätze erfolgen kann, mit dem Resultat aufwendiger, mehrstufiger Abfragen und Mechanismen zur Sicherstellung der referenziellen Integrität. Da sich im objektorientierten Quelltext die 1:n-Verwendung eines mehrwertigen Attributs nicht von der n:m-Verwendung unterscheiden lässt, kann eine Fallunterscheidung nicht ohne Zusatzinformationen erfolgen. Ferner ist das Prinzip der Vererbung der Attribute bzw. Eigenschaften und Methoden von Oberklassen an Unterklassen in objektorientierten Sprachen von zentraler Bedeutung – Mehrfachvererbung sei hier nicht betrachtet. In relationalen Datenbanken existiert dieses Prinzip nicht; es ist keine Struktur-Ebene zur derartigen Verknüpfung von Tabellen vorhanden. Zur Abbildung von Klassenhierarchien beim objektrelationalen Mapping gibt es drei Ansätze: Beim ersten Ansatz wird je Klasse eine Tabelle verwendet, wobei diese die jeweiligen, individuellen Attribute der Ober- und Unterklassen aufnehmen. Die Tabellen der erweiternden Unterklassen nehmen dabei nur die zusätzlichen Attribute auf. Jede Tabelle verfügt ferner über einen 6 Kapitel 2: Grundlagen Persistenzframeworks Primärschlüssel für den eindeutigen Identitätswert eines Objektes. Für ein Unterobjekt taucht dieser Wert somit in jeder Tabelle entlang seines Vererbungspfades auf. Als Resultat werden Unterklassen bei diesem Ansatz auf mehrere Tabellen verteilt, was Zugriffsgeschwindigkeit und Speicher kostet. Beim zweiten Ansatz wird je konkreter Unterklasse eine eigene, vollständige Tabelle verwendet, die alle Ober- und Unterklassen-Attribute zugleich aufnimmt. Insofern wird die Vererbungsrelation der Klassen im Datenbankschema vollständig ignoriert. Ein konkretes Objekt befindet sich stets in einer einzigen Tabelle, was die Zugriffsleistung bei Objekten, solange deren Klasse bekannt ist, im Vergleich zum ersten Ansatz verbessert, jedoch bei polymorphen Abfragen die Untersuchung sämtlicher, infrage kommender Klassen-Tabellen erfordert. Der dritte Ansatz verwendet zur Abbildung der gesamten Klassenhierarchie eine einzige Tabelle und vereint darin unter Ergänzung von Typ-Informationen alle Ober- und Unterklassen mit allen Attributen. Als Resultat kann eine Tabelle mit vielen ungenutzten Feldern vorliegen. Ferner ist die Eindeutigkeit der Attributnamen in Verbindung mit Attributtypen über alle Unterklassen zu gewährleisten. Das objektrelationale Mapping kann manuell durch Entwurf und Implementierung der zu speichernden Objektklassen, des Datenbankschemas und der Transfer-Mechanismen erfolgen, jedoch soll dies durch den Einsatz eines Persistenzframeworks erleichtert oder gar automatisiert werden. Es handelt sich hierbei um die Kernaufgabe eines Persistenzframeworks für den Umgang mit relationalen Datenbanken. 2.3 Besondere Aspekte für Persistenzframeworks Neben den genannten Aspekten des objektrelationalen Mappings, wie mit Primärschlüsseln, mehrwertigen Attributen oder Vererbungshierarchien umgegangen wird,oder wie flexibel und komfortabel der Einsatz erfolgen kann, sind bei der Untersuchung von Persistenzframeworks weitere Aspekte einzubeziehen: Im Falle neuer Anwendungsentwicklungen interessiert die Frage, ob eine Unterstützung des Entwicklers seitens des Frameworks geboten wird, aus einer vorgegebenen Klassenstruktur entsprechende Tabellenstrukturen (Top-Down-Perspektive des ORM) automatisch in einer ursprünglich leeren Datenbank zu erzeugen. Für bestehende Projekte stellen sich die Fragen, wie das objektrelationale Mapping für bereits vorliegende Datenbankschemata eingesetzt werden kann, ob also aus bestehenden Strukturen gemappte Klassen erzeugbar oder zumindest verwendbar sind (Bottom-Up7 Kapitel 2: Grundlagen Persistenzframeworks Perspektive), wie mit bestehenden Daten umgegangen wird, wie sich Änderungen an abgebildeten Klassen darauf auswirken und ob bestehende Datenstrukturen, Abfragen und SQL-Funktionen weiterhin nutzbar bleiben. Darüber hinaus ist zu untersuchen, welche Möglichkeiten und Einschränkungen im Umgang mit Datenbank-Transaktionen bereitstehen, die bei verteilten Geschäftsanwendungen von hoher Bedeutung sind, damit die Konsistenz innerhalb eines relationalen Datenbankmanagementsystems gewährleistet bleibt, und ob zur Sicherung der referenziellen Integrität Gebrauch von Triggern und Löschweitergaben gemacht wird. Ferner sind Performance-Aspekte zu eruieren, ob ein Connection-Pooling oder zumindest ein zwischenzeitiges Aufrechterhalten von Verbindungen zum Datenbanksystem stattfindet, da ein ständiges Neuaufbauen von Verbindungen die Leistung einer Anwendung massiv negativ beeinträchtigen kann. Dazu zählen auch die Aspekte, ob und wie CachingMechanismen für schnelle Wertänderungen an gemappten Objekten vorgesehen und umgesetzt sind sowie zu welcher Tiefe und zu welchem Zeitpunkt Objektgraphen beim Laden aufgelöst und vorgehalten werden, das sogenannte Fetching [BHRS07]. 8 Kapitel 3: LINQ 3 LINQ 3.1 Überblick und Architektur Das im Folgenden untersuchte Persistenzframework, das auch gleichzeitig den Fokus dieser Ausarbeitung setzt, stellt die Datenzugriffsmöglichkeiten dar, die im Rahmen der Language Integrated Query-Technologie von Microsoft bereitgestellt werden. LINQ steht dabei für eine Methode zur Vereinfachung und Vereinheitlichung der Implementierung jedweder Art des Zugriffs auf Datenmengen [PR07], also nicht nur zum Zweck der Persistenz, wobei diese Perspektive vordergründig betrachtet werden soll. Die ursprüngliche Motivation Microsofts zur Entwicklung von LINQ war die Überwindung von Problemen beim Datenzugriff auf relationale Datenbanken in den bisherigen .NET-Versionen und die Schaffung einer Lösung zum objektrelationalen Mapping [MEW08], die es vor .NET 3.5 von Microsoft nicht als fertiges Produkt gegeben hat. Als zentraler Aspekt von LINQ gilt der Ansatz, für beliebige Objekttypen aus beliebigen Datenquellen eine vereinheitlichte Syntax und ein gemeinsames Konzept zum Umgang mit Datenmengen und deren Elementen zu liefern. Dies erfolgt durch die Integration von Abfragen auf Mengen als natives Konstrukt in den .NET/CLRProgrammiersprachen, unter anderem C# 3.0 und VB.NET 9.0. Dabei wird intensiv von den Erweiterungen, die jene Sprachen im Vergleich zu ihren Vorgängerversionen erhalten haben, Gebrauch gemacht. Als Resultat der Sprachintegration ist bei Abfrageausdrücken in LINQ die Typsicherheit gewährleistet; ferner erfolgt eine Unterstützung des Entwicklers durch die Verfügbarkeit der Abfrageelemente bei der Codevervollständigung im Entwicklungs- und Debugmodus von Visual Studio 2008 sowie die Überprüfung der Ausdrücke zur Kompilierung, nicht erst zur Laufzeit. Darüber hinaus werden beim .NET-Framework 3.5 LINQ-Provider mitgeliefert, einheitliche Komponenten, die Schnittstellen zu spezifischen Datenquellen implementieren, wobei auch Möglichkeiten zu deren Erweiterung vorgesehen sind. Zu nennen sind: LINQ to Objetcs zum Zugriff auf nicht-persistierte Kollektionen, LINQ to SQL für die relationalen Datenbanken Microsoft SQL Server und Compact Edition, LINQ to XML und LINQ to Entities für den Datenzugriff per ADO.NET. Zu beachten ist, dass die Provider eine Top-Down-Perspektive verfolgen, den Zugriff auf 9 Kapitel 3: LINQ bestehende Strukturen zu ermöglichen, sodass für die Provider zu SQL und Entities zwar ein umfangreiches objektrelationales Mapping vorgesehen ist, aber keine Funktionalität zur automatisierten Erstellung neuer Datenbankschemata auf Grundlage objektorientierter Klassen vorliegt. Dies hat im Datenbankmanagementsystem stets selbst zu erfolgen. Programmiersprachen C# LINQ‐Bausteine Expression‐Bäume LINQ‐Provider LINQ to Objects Visual Basic Abfrage‐Ausdrücke Abfrage‐Operationen LINQ to SQL LINQ to XML SQL Server XML LINQ to Entities KlasseA KlasseB int : Zahl KlasseB : myB Datenquellen foo() bar() int : ZahlB bool : fertig flexnet() indapta() Objekte Entity Framework Abbildung 1: Architektur der LINQ-Technologien, nach [MEW08, S. 30]. 3.2 Spracherweiterungen für LINQ Die neuen Sprachkonstrukte C# 3.0 und VB.NET 9.0, von denen LINQ intensiv Gebrauch macht und dadurch erst ermöglicht wird, werden im Folgenden genauer für C# betrachtet. Es handelt sich um: Objektinitialisierer, Erweiterungsmethoden, implizit typisierte Variablen, anonyme Typen und Lambda-Ausdrücke. Objektinitialisierer dienen als verkürzte Schreibweise für die gemeinsame Initialisierung eines Objektes und das Setzen dessen öffentlicher Attribute. Anstelle Cl o = new Cl(4); o.Attr1 = 1; o.Attr2 = 2; kann man dies durch einen Objektinitialisierer mit einer Anweisung bewirken: Cl o = new Cl(4) { Attr1 = 1, Attr2 = 2 }; Der Compiler setzt den Objektinitialisierer in dieselben Kommandos um, wie es nach traditioneller Schreibweise erfolgt wäre. Erweiterungsmethoden ermöglichen es dem Entwickler, bestehende Klassen mit zusätzlichen Methoden auszustatten, ohne Vererbung zu verwenden. Insofern ist dies auch für sealed-Klassen oder primitive Typen möglich. internal static class ExtendInteger{ internal static void add(this int a, int b){a += b;}} 10 Kapitel 3: LINQ Das Beispiel stattet den primitiven Datentyp int mit der Methode add aus, die sich wie folgt verwenden lässt: int c=20; c.add(22); //c == 42 Erweiterungsmethoden, die den erweiterten Typen über ihren ersten Parameter annehmen, was durch das Schlüsselwort this gekennzeichnet wird, haben in einer statischen Klasse deklariert zu werden und sind selbst ebenfalls static. Implizit typisierte Variablen stehen dem Entwickler nur innerhalb eines Methodenkörpers zur Verfügung. var fourtytwo = 42; Über das Schlüsselwort var wird eine derartige Variable deklariert, wobei dies nur bei unmittelbarer Initialisierung anwendbar ist und der Typ der Variable durch den Initialisierungswert bestimmt wird. Anders als beim variant-Typ in Visual Basic ist eine derartige Variable streng typisiert, was auch durch Visual Studio und den Compiler gewährleistet und geprüft wird. Anonyme Typen kombinieren implizit typisierte Variablen mit Objektinitialisierern und ermöglichen es, innerhalb einer Methode komplexe Objektinstanzen zu verwenden, ohne dass dafür eine Klasse zu erstellen ist: var blauesAuto = new {Marke="BMW", hatWinterreifen=true}; var grauesAuto = new {Marke="Audi", hatWinterreifen=false}; var rotesMoped = new {Marke="Puch"}; Für die Objekte blauesAuto und grauesAuto wird implizit durch den Compiler eine Klasse mit den Attributen string Marke und bool hatWinterreifen erstellt und verwendet. Dabei richtet sich der Compiler nach den hier stets implizit typisierten Objektinitialisierer-Parametern und auch deren Reihenfolge. Für das Objekt rotesMoped wird insofern ein anderer anonymer Typ angelegt und verwendet. Zur Veranschaulichung des Sprachkonstrukts der Lambda-Ausdrücke sei zunächst das Prinzip der Methodenzeiger, der delegates, gezeigt, die bereits in früheren Sprachversionen zum Einsatz gekommen sind: static bool istKlein(int i) { return (i < 42); } void test(){int[] Zahlenliste = { 32, 73, 99, 79, 17, 13, 18 }; int[] kleineZahlen = Array.FindAll<int>( Zahlenliste, new Predicate<int>(istKlein));} Die Methode Array.FindAll, die vom Framework bereitgestellt wird, durchläuft alle Elemente der Zahlenliste, wendet auf sie istKlein an und nimmt daraufhin die jeweils wahren Ergebnisse in die Liste kleineZahlen auf. Der Delegat-Typ 11 Kapitel 3: LINQ Predicate, dessen Instanz hier auf die Methode istKlein zeigt, teilt FindAll mit, welche Methode für die Elemente jeweils aufzurufen ist. Durch die Verwendung anonymer Methoden kann die explizite Methode istKlein entfallen und der Methodenkörper direkt in den Aufruf von FindAll aufgenommen werden: …Array.FindAll<int>(Zahlenliste,delegate(int i){return(i<42);}); Durch die Verwendung eines neuartigen Lambda-Ausdruckes kann man dies noch weiter verkürzen; mit einem Statement-Body: …Array.FindAll<int>(Zahlenliste, i => { return (i < 42); }); oder analog mit einem Expression-Body: …Array.FindAll<int>(Zahlenliste, i => (i < 42)); Das links stehende i steht dabei für den Parameter, der in den rechts vom LambdaOperator => stehenden Ausdruck eingeht. FindAll lässt auch hier jedes int-Element der Zahlenliste als i in den Lambda-Ausdruck eingehen und erwartet ein boolResultat. Dabei ist wiederum vollständige Typsicherheit gegeben. Der Expression-Body wird vom Compiler intern in die folgende Form umgewandelt, einen sog. ExpressionBaum: ParameterExpression i = Expression.Parameter(typeof(int), "i"); Expression<Func<int, bool>> istKlein = Expression.Lambda<Func<int, bool>>( Expression.LessThan( i, Expression.Constant(42, typeof(int)) ), new ParameterExpression[] { i }); In der Expression-Klasse findet man zudem Methoden zu sämtlichen in einem Lambda-Ausdruck verwendbaren Operatoren. Der Expression-Baum lässt sich zu einer Func zusammenführen, die sich wie folgt für FindAll verwenden lässt: Func<int, bool> istKleinC = istKlein.Compile(); …Array.FindAll<int>(Zahlenliste, j => istKleinC.Invoke(j)); 3.3 Objektrelationales Mapping und Abfragen mit LINQ to SQL Im Folgenden wird demonstriert, wie eine Anwendung ausgehend von einer bestehenden Datenbank erstellt wird und unter Einsatz des LINQ to SQL-Providers Lese- und Schreiboperationen mit gemappten Objekten durchgeführt werden, die auf der zu Grunde liegenden Datenquelle umgesetzt werden. Als Anschauungsobjekt dient die Datenbank dbo.Fahrtenbuch mit den Tabellen Automobil, Person und Fahrt 12 Kapitel 3: LINQ inklusive der drei Fremdschlüssel-Relationen Fahrt.FahrerPerson und Automobil.HalterPerson, Fahrt.FahrzeugAutomobil. Die Datenbank liegt auf einem Microsoft SQL Server 2005 vor, wobei der Provider LINQ to SQL zu Datenquellen der Versionen ab 2000 und der SQL Server Compact Edition kompatibel ist. Als Entwicklungsumgebung kommt Microsoft Visual Studio 2008 und als Programmiersprache C# in Verbindung mit .NET 3.5 zum Einsatz. Abbildung 2: Datenbank-Diagramm zu dbo.Fahrtenbuch. Um das objektrelationale Mapping vorzunehmen, wird in einem neuen Projekt eine neue LINQ to SQL-Klasse erstellt. Unter Zuhilfenahme des grafischen DesignWerkzeugs, wie es in einem praktischen Projekt sicherlich zweckmäßig ist, wird nach Festlegung einer Verbindungszeichenfolge und dem Verbindungsaufbau zur Datenquelle deren Schema geladen und importiert. Dies erfolgt durch Drag&Drop der Tabellen aus dem Server Explorer in die entsprechende Design-Ansicht, wobei neben Tabellen auch gespeicherte Prozeduren, Ansichten und Funktionen auswählbar sind. Fremdschlüssel-Relationen zwischen Tabellen werden automatisch übernommen und angezeigt. Die Entwicklungsumgebung generiert im Hintergrund die gemappten, objektorientierten Klassen, stattet sie unter anderem mit allen wesentlichen Attributen aus und annotiert sie. Es sei angemerkt, dass ein erfolgreiches Mapping auch mit einfacheren Mustern und weniger Quelltext erfolgen kann, hier jedoch der ausführliche, automatisch generierte Code aufgrund seiner Vollständigkeit und der Verwendung zweckdienlicher Entwurfsmuster analysiert werden soll. Es werden vier Klassen generiert: Zum einen FahrtenbuchDataContext, die System.Data.Linq.DataContext erweitert und für die drei Tabellen die Klassen 13 Kapitel 3: LINQ Automobil, Person und Fahrt. Die Erstgenannte stellt dabei hauptsächlich einen Ansatzpunkt für benutzerdefinierte Erweiterungen der Funktionalität des DataContext dar, der unter anderem die Aufgabe des Verbindungsmanagements übernimmt und als Kernfunktionalität die Durchführung von Abfragen durch die Umsetzung von LINQAusdrücken in spezifische SQL-Kommandos vornimmt. Sämtliche Konstruktoren sind mit dem Aufruf der Erweiterungsmethode OnCreated() ausgestattet. Insofern kann der Entwickler Programmschritte ergänzen, die beim Verbindungsaufbau, dem Erzeugen des Kontextes, verarbeitet werden sollen. Ferner sind die Erweiterungsmethoden Insert, Update und Delete für die drei Tabellen-Objekte beigefügt, die zu den jeweiligen Ereignissen aufgerufen werden, sowie typisierte Eigenschaften zum komfortablen Holen der Tabellen als EntitySet<T> generiert worden. Annotiert ist der FahrtenbuchDataContext lediglich mit dem DatabaseAttribute(Name="Fahrtenbuch"), was den Namen der Datenbank auf dem Quellserver widerspiegelt. Die drei den Tabellen zugehörigen Klassen werden vom Designer so angelegt, dass sie die Schnittstellen INotifyPropertyChanging und -Changed implementieren, was an sich nicht notwendig ist, jedoch wiederum eine komfortable Erweiterung darstellt, um Änderungsereignisse der Felder zu behandeln. Die dafür notwendigen Erweiterungsmethoden liegen ebenfalls vor. Von Interesse ist die Wiedergabe der gemappten Attribute, die als öffentliche Eigenschaften mit privaten Feldern erstellt wurden. Jedes einfache, in keinem Fremdbezug stehende Attribut ist mit Column annotiert. Dieses beschreibt per Storage den Namen des lokalen Feldes, das den Dateninhalt aufnimmt, gegebenenfalls per Name die Bezeichnung der Tabellenspalte, den Typ des Feldinhaltes z. B. als DbType="NVarChar(50) NOT NULL" und explizit, ob es sich um ein Pflichtfeld handelt CanBeNull=false. Felder, die leer bleiben können, werden als System.Nullable<T> typisiert abgebildet. Primärschlüssel werden als solche durch IsPrimaryKey=true gekennzeichnet, wobei die Generierungsstrategie des Primärschlüssel-Wertes festgelegt wird, hier als Vergabe durch das Datenbanksystem IsDbGenerated=true in Verbindung mit der Angabe, dass dieser Wert nach einem Einfügevorgang aus der Datenbank einzulesen ist, AutoSync=AutoSync.OnInsert. Fremdschlüsselattribute werden wie gewöhnliche Attribute abgebildet, zusätzlich wird jedoch in die verweisende Klasse ein Attribut vom Typ der verwiesenen Klasse ergänzt, das die Relation zum Ausdruck bringt und 14 Kapitel 3: LINQ verwiesene Objekte typisiert und transparent erreichbar macht. So steht beispielsweise in der Klasse Automobil neben Nullable<int> Halter auch die entsprechende Person zur Verfügung. Annotiert wird das Zusatzattribut als Association unter Angabe, dass es Teil einer Fremdschlüssel-Relation ist, IsForeignKey=true, und der daran beteiligten, hiesigen ThisKey="Halter" und verwiesenen Spalten OtherKey="PersonID". Ferner stattet der Codegenerator die set-Methode des Assoziations-Attributs mit Operationen zur Aktualisierung aller an der Relation beteiligten Objekte und Fremdschlüssel-Attributen aus. Auch in der Gegenrichtung wird die verwiesene Klasse, im Beispiel Person, um eine typisierte Kollektion EntitySet<Automobil> mit Association annotiert. Dies verdeutlicht, dass dem Entwickler transparente Navigationsmöglichkeiten über beide Richtungen einer Fremdschlüsselrelation geschaffen werden. Nachdem das objektrelationale Mapping vorgenommen ist, können Abfragen per LINQ auf Grundlage der erstellten Klassen formuliert werden. Um in einer Anwendung Zugriff auf die gemappten Datenelemente zu erhalten, ist die Instanziierung eines DataContext, in diesem Fall des FahrtenbuchDataContext, erforderlich, was unter Angabe einer Verbindungszeichenfolge stattfindet, welche die dafür nötigen Informationen zur Datenquelle enthält. Zur Abfrage sämtlicher Automobile lässt man sich die Table<Automobil> vom Kontext-Objekt übergeben und führt folgendes LINQ-Statement damit aus: IQueryable<Automobil> qAutos = from auto in tAutos select auto; Die Resultat-Kollektion wird als IQueryable angegeben, was eine Erweiterung des IEnumerable darstellt und als Automobil typisiert wird, der gemappten Klasse. Eine Implementierung von IQueryable kann dabei über die Methode zur Auswertung des Abfrageausdruckes entscheiden und kann Mengenoperationen wie Auswahl und Filterung damit direkt in der Datenquelle selbst stattfinden lassen. Auf diese Weise wird im Falle von LINQ to SQL von den leistungsfähigen Abfragemechanismen einer SQL Server-Datenquelle Gebrauch gemacht. Der gezeigte Abfrageausdruck besteht aus zwei Teilen: dem from/in-Abschnitt, der die Variable auto für die Elemente der Sequenz tAutos vom Typ Table<Automobil> durchläuft, und dem select Abschnitt, der die Rückgabe der Elemente auto bewirkt. Die LINQ-Syntax ist keinesfalls mit SQL zu verwechseln, obwohl sie bezüglich der verwendeten 15 Kapitel 3: LINQ Schlüsselwörter sehr ähnlich erscheint. Um bei einem Abfrageausdruck eine Filterung anzuwenden, genügt es einen where-Abschnitt anzufügen: IQueryable<Automobil> qAutos = from auto in tAutos where (auto.Typ.StartsWith("BMW")) select auto; Hervorzuheben ist die Verwendung der String-Methode StartsWith direkt auf dem gemappten Attribut string Typ, die einem wie alle weiteren String- Memberfunktionen nativ zur Verfügung stehen. Tiefer in die Tabellenstruktur vordringende Abfragen, die bei SQL die Verwendung von geschachtelten Abfragen oder JOIN-Verbunde erfordern, lassen sich auf ähnliche Weise vornehmen: IQueryable<Automobil> qAutos = from auto in tAutos where (auto.Person.Vorname.Equals("Georg")) select auto; Die missverständliche Bezeichnung Person steht dabei für das durch die Fremdschlüsselbeziehung Halter verwiesene Person-Objekt und wurde vom Codegenerator so gewählt. Im grafischen Designer lässt sie sich jedoch ohne Mühen beispielsweise in HalterPerson ändern, wobei die Übernahme der Änderung in alle relevanten Codestellen automatisch erfolgt. Das Einfügen von Objekten sei an folgendem Auszug demonstriert: Person georg = (from person in tPersonen where person.Vorname.Equals("Georg") select person).First(); Automobil georgsAuto = (from auto in tAutos where (auto.Person == georg) select auto).First(); Fahrt neueFahrt = new Fahrt { Automobil = georgsAuto, Person = georg, geschaeftlich = false, Zeit = DateTime.Now, Zweck = "Winterreifen" }; tFahrten.InsertOnSubmit(neueFahrt); ctx.SubmitChanges(); Das neue Fahrt-Objekt wird instanziiert und seine Attribute gesetzt. Das Fahrzeug als Automobil und der Fahrer als Person werden hier im Datenbestand gesucht, wobei IQueryable.First() zum Einsatz kommt, um das erste Element der Kollektion zu erhalten. Die neueFahrt wird zur zeitlich versetzten Speicherung an die Tabelle tFahrten per InsertOnSubmit() übergeben. Die Übertragung an das Datenbanksystem erfolgt erst durch den Aufruf von SubmitChanges() des Datenkontextes. Löschen erfolgt über die Tabellenfunktion DeleteOnSubmit(). Die Modellierung von Vererbungsstrukturen kann bei LINQ to SQL ebenfalls über die grafische Design-Ansicht erfolgen. Ausgehend von einer bestehenden Tabellen-Struktur in der Datenbank, die bereits eine Vererbungshierarchie abbildet, lassen sich 16 Kapitel 3: LINQ entsprechende Klassen modellieren. Dabei kann zur Unterscheidung verschiedener Unterklassen eine Diskriminator-Spalte mit Unterklassen-individuellen Ausprägungen festgelegt werden. LINQ to SQL unterstützt zwar die Verwendung von Datenbanktransaktionen, jedoch nicht komfortabler als dies bereits in .NET 2.0 der Fall war. Um beispielsweise das gezeigte Speichern einer Fahrt innerhalb einer Transaktion stattfinden zu lassen, was in Anbetracht der datenbankseitigen Primärschlüsselvergabe ratsam erscheint, muss im FahrtenbuchDataContext über dessen Connection manuell eine Transaktion gestartet und zugewiesen werden: DbTransaction tr = ctx.Connection.BeginTransaction(IsolationLevel.ReadCommitted); ctx.Transaction = tr; try {ctx.SubmitChanges(); tr.Commit();} catch {tr.Rollback();} Eine integrierte Methode der Transaktionsverwaltung innerhalb der DatenkontextKlasse, beispielsweise durch Setzen eines IsolationLevel für alle schreibenden Übertragungsvorgänge, wäre förderlich gewesen, um diesbezügliche Fehler auszuschließen. Der FahrtenbuchDataContext bietet indes genügend Ansatzpunkte, um eine derartige Funktionalität nachzurüsten. Das Laden von gemappten Objekten findet im Normalfall zu dem Zeitpunkt statt, zu dem sie tatsächlich von der Anwendung benötigt werden [Ki09]. Dieses Verhalten wird als Lazy Fetching bezeichnet. Für Situationen, in denen dieses Verhalten nicht erwünscht ist, lassen sich dem DataContext sogenannte DataLoadOptions mitteilen. Das Beispiel zeigt die Anweisung, beim Laden der Automobil-Tabelle die Person-Tabelle ebenfalls zu übertragen: DataLoadOptions dlo = new DataLoadOptions(); dlo.LoadWith<Automobil>(auto => auto.Person); ctx.LoadOptions = dlo; 3.4 LINQ to Entities und ADO.NET 3.0 Mit Veröffentlichung des Servicepack 1 für das .NET-Framework 3.5 und dem Servicepack 1 für das Visual Studio 2008 hat Microsoft im August dieses Jahres die Datenzugriffskomponenten ADO.NET 3.0 und den LINQ to Entities-Provider nachgeliefert. Den zentralen Unterschied zu LINQ to SQL stellt die Verwendung des ADO.NET Entity Frameworks dar, das die Verwendung eines sogenannten Entity17 Kapitel 3: LINQ Modells ermöglicht [MEW08]. Hierbei handelt es sich um ein konzeptionelles Schema, das der Entwickler mithilfe eines grafischen Modellierungswerkzeuges auf Grundlage des internen Schemas einer Datenquelle erstellen kann. Es ist dabei möglich, eigene Entities zu modellieren und diese mit einer Zuordnung auf verschiedene TabellenElemente auch unter Einbezug logischer Bedingungen zu belegen. Vergleichbar mit einer Datenbank-Ansicht lassen sich somit Entities erzeugen, die anhand von Relationen auf zusammengeführten Daten arbeiten. Der LINQ to Entities-Provider übernimmt im Umgang mit zusammengeführten Entities deren Auflösung, um beispielsweise Aktualisierungsoperationen auf den zu Grunde gelegten, internen Tabellen durchzuführen [Ki09]. Durch die Trennungsschicht wird eine weitere Abstraktion, nun auch auf Strukturebene der gemappten Klassen, möglich, welche die Entkopplung vom konkreten, internen Datenbankschema fördert. 3.5 LINQ to XML Anstelle einer relationalen Datenbank kann zum Speichern von Daten auch eine XMLDatei verwendet werden. Durch die Dokumentenorientierung eignet sich das Dateiformat darüber hinaus als Transferdatei zwischen zwei Systemen und spielt beispielsweise im Bereich der Webservices eine große Rolle. Der LINQ to XMLProvider liefert als zentrale Klasse das XElement. XElement wurzel = new XElement("wurzelknoten", new XElement("unterknoten0", null), new XElement("unterknoten1", null) ); Unter Verwendung funktionaler Konstruktoren kann der Entwickler auf einfache Weise XML-Bäume erzeugen, wobei für den Inhalt eine beliebige Zahl weiterer XElemente angegeben werden kann. Darüber hinaus stellt die Klasse die Methoden Load() und Save() bereit und kann somit auch ein vollständiges XML-Dokument repräsentieren [Ra07]. Ein Mapping vergleichbar mit dem objektrelationalen bei LINQ to SQL oder der JavaTechnologie JAXB von XML-Elementen auf Klassen existiert im Rahmen von LINQ to XML nicht. Elementbezeichnungen werden konsequent vom Typ String gesetzt. 18 Kapitel 4: Hibernate und weitere Alternativen 4 Hibernate und weitere Alternativen 4.1 Hibernate und JPA Das Open Source Persistenzframework Hibernate für JDBC-kompatible, relationale DBMS liegt derzeit in der dritten Generation vor. Seit Version 3.2 ist es kompatibel zum Java Persistence API-Standard (JPA), der seit der Java Platform Enterprise Edition 5.0 Schnittstellen zu Persistenzframeworks spezifiziert. Hibernate und JPA sind sowohl für den Einsatz in einem Enterprise Java Beans-Container als auch in der Java Platform Standard Edition geeignet [MW08]. Die Betrachtungen im Folgenden beziehen sich auf die charakteristischen, JPA-konformen Funktionalitäten von Hibernate. Die Spezifikation des objektrelationalen Mappings kann bei Hibernate über XMLDateien oder aussagekräftiger über Annotationen in den gemappten Klassen erfolgen. Eine derartige Klasse ist mit der Annotation @Entity zu versehen und hat einen Primärschlüssel zu besitzen, der mit @Id markiert ist. Die Strategie für dessen Wertzuweisung legt @GeneratedValue(Strategy=GenerationType.IDENTITY) fest. Neben IDENTITY zur Generierung des Wertes über die Datenbank stehen laut JPA TABLE, wobei der letzte Wert in einer gesonderten Tabelle abgespeichert wird, und SEQUENCE, das nicht für alle DBMS geeignet ist, zur Verfügung. Hibernate beherrscht zudem weitere Strategien, wie uuid.hex oder guid. Attribute, die als Spalten gemappt werden sollen, werden mit @Column annotiert. Zu beachten ist, entweder alle Annotationen inkl. @Id vor die Klassenattribute selbst oder konsequent vor deren Getter-Funktionen zu positionieren. Pflichtfelder erhalten dabei nullable=false und Felder, für die eine Eindeutigkeit gewährleistet werden soll, unique=true. Neben weiteren @Column-Attributen ist vor allem für das Mappen von String-Feldern legth von Interesse. Das Mapping primitiver Datentypen in korrespondierende DatenbankFeldtypen wird implizit vorgenommen. Weitere spaltenbezogene Annotationen sind z. B. @Temporal für Zeitwerte und @Basic(fetch=FetchType.LAZY), das laut JPA ein spätes, bedarfsweises Laden des annotierten Attributes bewirken soll, jedoch von Hibernate nicht umgesetzt wird. Zur Abbildung von 1:1-Assoziationen steht die Annotation @OneToOne zur Verfügung, für 1:n-Beziehungen @OneToMany. Letzteres sollte zu einem mengenwertigen Attribut stehen, wobei dafür eine Set-Kollektion wie HashSet empfehlenswert ist [He07, S. 191]. Alternativ kann man indizierte Map- 19 Kapitel 4: Hibernate und weitere Alternativen Kollektionen, wie HashMaps nutzen, deren Schlüsselspalte über die zusätzliche Annotation @MapKey(name="…") anzugeben ist. Darüber hinaus ist in der @OneToMany-Annotation per mappedBy die Angabe des Fremdschlüssel-Attributes der Zielklasse und dort die Annotation @ManyToOne erforderlich. Erfolgt dies nicht, so wird für das Mapping eine Zwischentabelle verwendet. Ferner wird @JoinColumn zur Kennzeichnung von Fremdschlüsseln angeboten. @ManyToMany kennzeichnet analog n:m-Beziehungen, deren beiderseitige Aktualität von einer Seite durch Listenoperationen auf der anderen Seite zu gewährleisten ist. Bei den AssoziationsAnnotationen kann ferner über fetch=fetchType.LAZY ein bedarfsweises Laden erwirkt werden, das von Hibernate in diesem Fall umgesetzt wird, sowie die Weitergabe von u. a. Speicher- oder Lösch-Anforderungen durch cascade=CascadeType.ALL aktiviert werden, was jedoch nicht mit Weitergaben oder Triggern auf Datenbank-Ebene gleichzusetzen ist. Das Mapping einer Vererbungshierarchie aus @Entity-Klassen kann über die Klassen-Annotation @InheritanceStrategy(strategy=…) gesteuert werden. Hier stehen TABLE_PER_CLASS mit geerbten Attributen in allen Untertabellen, JOINED mit vererbten Attributen in der Oberklassen-Tabelle und SINGLE_TABLE als Vorgabe zur Verfügung [He07]. Beim letztgenannten ist zur Unterscheidung der Klassen die Angabe einer @DiscriminatorColumn möglich. In Hibernate und JPA ist die Bereitstellung eines EntityManagers vorgesehen, der u. a. für gemappte Objekte die Methoden persist() zum Speichern, remove() zum Löschen und find() zum Suchen anhand des Primärschlüssels bietet. Weiterführende Abfragen lassen sich in JPA nur per createNativeQuery() oder createQuery() erzeugen, wobei ersteres als String übergebene Anweisungen an das DBMS weiterleitet und das zweite Anweisungen in der JPA Query Language, einem vereinfachten SQL-Dialekt, erwartet. Die Abfragen sind parametrisierbar. Zusätzlich bietet Hibernate die Klassen Criteria und Example zur strukturierten oder typsicheren Modellierung von Abfragekriterien. Zur Transaktionssteuerung liefert der EntityManager Instanzen der Klasse EntityTransaction, die per begin() einzuleiten und per commit() abzuschließen sind. Ferner ist Connection Pooling über Drittanbieter-Bibliotheken wie C3PO möglich [BHRS07, S. 96]. Auf Grundlage eines vorliegenden Mappings kann durch hb2ddl.SchemaExport mit den Methoden create() und drop() das vollständige, zugehörige Datenbankschema im DBMS angelegt oder entfernt werden. 20 Kapitel 4: Hibernate und weitere Alternativen 4.2 NHibernate für .NET und Quarae für Java Zum vorgestellten Hibernate für Java existiert mit NHibernate ein ebenfalls als Open Source veröffentlichtes Pendant für .NET. Es liegt derzeit mit dem Versionsstand 2 vor und unterstützt offiziell die .NET-Versionen 1.1 und 2.0, lässt sich jedoch auch unter .NET 3.5 einsetzen. NHibernate ist aus einer Portierung von Hibernate 2.1 entstanden und wird seitdem eigenständig weiterentwickelt. Es verwendet zum Datenzugriff ADO.NET und kann insofern mit diversen DBMS eingesetzt werden. Auch liegen Werkzeuge zur Generierung von .NET-Klassen aus XML-Mapping-Dateien vor. Die Funktionalität entspricht in weiten Teilen der von Hibernate, mit einer deutlichen Einschränkung: Die Spezifikation des objektrelationalen Mappings kann ausschließlich über XML-Dateien erfolgen, nicht über Annotationen; obwohl Annotationen in .NET seit 1.0 als sogenannte Attribute möglich sind, wird davon kein Gebrauch gemacht. Zur Verwendung mit LINQ liegt ein entsprechender LINQ to NHibernate-Provider vor, der den Zugriff auf gemappte Klassen über IQueryable ermöglicht. Die wesentlichen LINQ-Kommandos für das Abfragen von Elementen sind dabei implementiert und werden über Criteria umgesetzt. Bei Quarae handelt es sich um kein Persistenzframework, sondern um ein junges JavaProjekt mit dem Ziel, an LINQ angelehnte Ausdrücke auf Listen, Iterable- oder org.quaere.Queryable-Datenstrukturen anzuwenden. Aufgrund der mangelnden Sprach-Integration weicht die Schreibweise freilich von der LINQs ab: Iterable<Integer> kleineZahlen = from("n").in(Zahlenliste).where(lt("n", 42)).select("n"); Der dargestellte Ausdruck bildet eine Baumstruktur, die per Visitor-Entwurfsmuster ausgewertet wird. Darüber hinaus kann Quarae über die genannte Queryable-Struktur, die den JPA-EntityManager einbezieht, Anfragen an ein Persistenzframework richten. 21 Kapitel 5: Fazit und Ausblick 5 Fazit und Ausblick Den Problemfeldern, die beim Mapping objektorientierter Klassenstrukturen mit relationalen Datenbank-Schemata durch die Verschiedenheit der zu Grunde liegenden Paradigmen als objektrelationaler Impedanz-Unterschied auftauchen, können vielfältige Lösungsansätze entgegengebracht werden. So lässt sich die Abbildung der Struktur objektorientierter Klassen durch den Einsatz einer oder mehrerer relationaler Tabellen realisieren, wobei auch komplexe und mengenwertige Attribute sowie VererbungsHierarchien abgebildet werden können. Insofern ist die Modellierung relationaler Schemata zur adäquaten Aufnahme der Daten und Beziehungen komplexer ObjektGraphen unter Zuhilfenahme spezifischer Mechanismen vollständig möglich. Die im Rahmen der LINQ-Technologie vorliegenden Methoden liefern dem Entwickler umfassende und flexible Ansätze bei der Erstellung von Persistenzlösungen in .NET. Insbesondere der LINQ to SQL-Provider sorgt durch effektives objektrelationales Mapping für eine umfangreiche Unterstützung der Verwendung relationaler Daten als Objektstrukturen. Die dabei verwendeten Muster gewährleisten die konsistente Kapselung und effiziente Abfrage von Objekten aus der Datenbank. Mit den LINQAusdrücken wird zudem eine prägnante und typsichere Methode für Operationen auf transienten oder persistenten Kollektionen bereitgestellt. Integrierte Ansätze zur automatisierten Erstellung eines Datenbank-Schemas aus gemappten Objekten sowie Verbesserungen im Umgang mit Transaktionen fehlen bislang. Das ebenfalls vorgestellte Persistenz-Framework Hibernate für Java, das sich ausschließlich für die Verwendung mit relationalen, JDBC-kompatiblen Datenquellen eignet, bietet einen vergleichbaren Grad der Problem-Abdeckung. Es ist kompatibel zum allgemeinen Java Persistence API und liefert darüber hinaus ein breites Repertoire ausgereifter Lösungen zum objektrelationalen Mapping. Als Pendant für .NET und ADO.NET-kompatible Datenquellen ist NHibernate als gleichwertig zu betrachten, mit der Einschränkung, dass die Spezifikation des Mappings allein über XML-Dateien erfolgen kann. Aufgrund der Sprachintegration von Abfragen durch LINQ erscheint diese Lösung in komplexen Szenarien gegenüber den traditionellen Methoden in Java und Hibernate überlegen. Es bleibt abzuwarten, wann derartige Mechanismen, die über Demonstrationen wie Quarae hinaus gehen, in Java zur Verfügung stehen. 22 Anhang A: Quellcode zu den Klassen im LINQ to SQL-Beispiel A Quellcode zu den Abfragen im LINQ to SQL-Beispiel Öffnen eines DataContext, Laden der Tabellen, Abfragen der Autos: FahrtenbuchDataContext ctx = new FahrtenbuchDataContext(Properties.Settings.Default.ConnectionString); Table<Automobil> tAutos = ctx.Automobils; Table<Person> tPersonen = ctx.Persons; Table<Fahrt> tFahrten = ctx.Fahrts; IQueryable<Automobil> qAutos = from auto in tAutos select auto; Es wird folgender SQL-Ausdruck generiert: SELECT [t0].[Kennzeichen], [t0].[Typ], [t0].[Erstzulassung], [t0].[Halter] FROM [dbo].[Automobil] AS [t0] Abfrage aller Automobile, deren Typ mit "BMW" beginnt: IQueryable<Automobil> qAutos = from auto in tAutos where (auto.Typ.StartsWith("BMW")) select auto; Es wird folgender SQL-Ausdruck generiert: SELECT [t0].[Kennzeichen], [t0].[Typ], [t0].[Erstzulassung], [t0].[Halter] FROM [dbo].[Automobil] AS [t0] WHERE [t0].[Typ] LIKE BMW% Abfrage aller Automobile, deren Halter den Vornamen "Georg" trägt: IQueryable<Automobil> qAutos = from auto in tAutos where (auto.Person.Vorname.Equals("Georg")) select auto; Es wird folgender SQL-Ausdruck mit JOIN generiert: SELECT [t0].[Kennzeichen], [t0].[Typ], [t0].[Erstzulassung], [t0].[Halter] FROM [dbo].[Automobil] AS [t0] LEFT OUTER JOIN [dbo].[Person] AS [t1] ON [t1].[PersonID] = [t0].[Halter] WHERE [t1].[Vorname] = 'Georg' Abfrage einer Person mit dem Vornamen "Georg", dessen Automobils, Erstellung einer neuen Fahrt, Speichern der neuen Fahrt: Person georg = (from person in tPersonen where person.Vorname.Equals("Georg") select person).First(); Automobil georgsAuto = (from auto in tAutos where (auto.Person == georg select auto).First(); Fahrt neueFahrt = new Fahrt { Automobil = georgsAuto, Person = georg, geschaeftlich = false, Zeit = DateTime.Now, Zweck = "Winterreifen" }; tFahrten.InsertOnSubmit(neueFahrt); ctx.SubmitChanges(); 23 Anhang A: Quellcode zu den Klassen im LINQ to SQL-Beispiel Auf dem SQL-Server wird folgender Befehl beim Übermitteln der Änderung ausgeführt: INSERT INTO [dbo].[Fahrt]([Zeit], [Fahrzeug], [Fahrer], [Entfernung], [geschaeftlich], [Zweck], [exportiert]) VALUES (@p0, @p1, @p2, @p3, @p4, @p5, @p6) SELECT CONVERT(Int,SCOPE_IDENTITY()) AS [value] Mit den Parameter-Typen: @p0 datetime,@p1 nchar(12), @p2 int,@p3 float,@p4 bit,@p5 text,@p6 bit Und den Parameter-Werten: @p0='2008-12-08 18:40:03:783',@p1=N'WAF IT 246 @p2=3,@p3=0,@p4=0,@p5='Winterreifen',@p6=0 ', 24 Literaturverzeichnis [Am03] Scott Ambler: Agile Database Techniques, John Wiley & Sons, 2003. [BaHei99] Heide Balzert: Lehrbuch der Objektmodellierung, Spektrum Akademischer Verlag, 1999. [BaHel99] Helmut Balzert: Lehrbuch Grundlagen der Informatik, Spektrum Akademischer Verlag, 1999. [BHRS07] Robert F. Beeger, Arno Haase, Stefan Roock, Sebastian Sanitz: Hibernate – Persistenz in Java-Systemen mit Hibernate und der Java Persistence API, 2. Aufl., dpunkt.verlag, 2007. [BBE95] Andi Birrer, Walter R. Bischofberger, Thomas Eggenschwiler: Wiederverwendung durch Frameworktechnik – vom Mythos zur Realität, in: Objektspektrum (1995) 5, S. 18-26. [Co70] Edgar Frank Codd: A Relational Model of Data for Large Shared Data Banks, in: Communications of the ACM, 13 (1970) 6, S. 377–387. [DH03] Jürgen Dunkel, Andreas Holitschke: Softwarearchitektur für die Praxis, Springer, 2003. [He07] Sebastian Hennebrüder: Hibernate – Das Praxisbuch für Entwickler, Galileo Press, 2007. [Ke97] Wolfgang Keller: Mapping Objects to Tables – A Pattern Language. 1997. [Ke83] William Kent: A Simple Guide to Five Normal Forms in Relational Database Theory, in: Communications of the ACM, 26 (1983) 2, S. 120– 125. [Ki09] Paul Kimmel: LINQ Unleashed for C#, Pearson Education, 2009. [Le00] Neal Leavitt: Whatever Happened to Object-Oriented Databases?, in: Computer, 33 (2000) 8, S. 16–19. [MEW08] Fabrice Marguerie, Steve Eichert, Jim Wooley: LINQ im Einsatz, Carl Hanser Verlag, 2008. [Me08] Vijay P. Mehta: Pro LINQ Object Relational Mapping with C# 2008, Apress, 2008. [MW08] Bernd Müller, Harald Wehr: Java-Persistence-API mit Hibernate, Standardisierte Persistenz, Addison-Wesley, 2008. [PR07] Paolo Pialorsi, Marco Russo: Introducing Microsoft LINQ, Microsoft Press, 2007. [Ra07] Joseph C. Rattz: Pro LINQ: Language Integrated Query in C# 2008, Apress, 2007. [SGM02] Clemens Szyperski, Dominik Gruntz, Stephan Murer: Component Software – Beyond Object-oriented Programming, Pearson Education, 2002.