Persistenzframeworks - Department of Information Systems

Werbung
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.FahrerPerson
und
Automobil.HalterPerson,
Fahrt.FahrzeugAutomobil. 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.
Herunterladen