Veröffentlichen auch Sie Ihre Arbeiten - es ist ganz einfach!
Mehr InfosBachelorarbeit, 2009, 63 Seiten
Bachelorarbeit
Fachhochschule Stralsund (Elektrotechnik, Bachelor Informatik)
1,0
2 Einleitung
3 Wichtige Neuheiten in C# 3.0
3.1 Implizite Typisierung
3.2 Objekt- und Auflistungsinitialisierer
3.3 Anonyme Typen
3.4 Erweiterungsmethoden
3.5 Lambda Ausdrücke
4 LINQ to SQL
4.1 Setup
4.2 LINQ Syntax
4.3 Definition von Entitätsklassen
4.4 Einfache Abfragen
4.5 Where Bedingung
4.6 Verschachtelte Abfragen
4.6.1 Nicht korrelierende Abfragen
4.6.2 Korrelierende Abfragen
4.7 Order By
4.8 Group By
4.9 Joins
4.10 Der IN Operator
4.11 Die EXISTS Bedingung
4.12 Der BETWEEN Operator
4.13 Beziehungen
4.13.1 1:1 Beziehungen
4.13.2 1:n Beziehungen
4.13.3 m:n Beziehungen
4.14 Verzögerte Ausführung
4.15 Gespeicherte Prozeduren
4.16 Datenmanipulation
4.16.1 Insert
4.16.2 Update
4.16.3 Delete
5 Datendefinition
6 Zusammenfassung
7 Literaturverzeichnis
8 Anhang
8.1 Abbildungsverzeichnis
8.2 Listingverzeichnis
8.3 Entitätsklassen
8.3.1 Kunde
8.3.2 Produkt
8.3.3 Verkauf
8.3.4 Position
8.3.5 Kategorie
8.3.6 Adresse
9 Erklärung
Als Programmierer arbeitet man ständig mit einer Menge von Daten und Ob-jekten. Auf externe Daten kann man mit Hilfe entsprechender Abfragesprachen zugreifen, doch für Ansammlungen von Objekten gab es ein derartiges Abfrage-system bisher nicht. Man musste Listen manuell durchlaufen, um die, den Kriterien entsprechenden Objekte zu finden.
Doch mit Version 3.0 des C# Sprachstandards führt Microsoft die „Language Integrated Queries“, kurz: LINQ ein. Mit Hilfe von LINQ ist es möglich, .Net Objekte mit einer SQL ähnlichen Syntax zu durchsuchen. Außerdem ermöglichen zusätzliche Provider auch den Zugriff auf externe Datenquellen, wie Datenbanken oder XML Dateien.
Der erste Teil dieses Dokumentes gibt einen kurzen Überblick über einige wichtige Neuheiten im C# 3.0 Standard, im zweiten Teil wird dann ausführlich auf den LINQ to SQL O/R Mapper eingegangen und dessen Funktionsweise untersucht.
Dieses Kapitel gibt eine kurze Einführung über die mit dem C# 3.0 Standard eingeführten Neuerungen, welche für ein grundsätzliches Verständnis der LINQ Funktionalitäten notwendig sind.
Mit Einführung des C# 3.0 Sprachstandards ist es mit Hilfe des var Schlüssel-wortes möglich, den Typ von Variablen implizit zu spezifizieren. Implizit typi-sierte Variablen sind jedoch weiterhin stark typisiert, so als wären sie explizit definiert worden. Ihr Typ wird allerdings erst vom Compiler endgültig bestimmt.
Abbildung in dieser Leseprobe nicht enthalten
Listing 3.1: explizite und implizite Typisierung
Dass hierbei sowohl a wie auch b vom gleichen Typ sind, lässt sich leicht an der Ausgabe erkennen.
Abbildung in dieser Leseprobe nicht enthalten
Listing 3.2: Ausgabe von Listing 3.1
Verwendung findet die implizite Typisierung vor allem im Zusammenhang mit den ebenfalls neu eingeführten Anonymen Typen (3.3), welche zur Entwicklungszeit noch keinen Typ besitzen und eine explizite Typisierung daher nicht möglich ist.
In den meisten Fällen ist die Verwendung des Schlüsselwortes jedoch optional und dient lediglich der Erleichterung für den Programmierer, da er bei Variablendefinitionen nicht alles doppelt schreiben muss. Dabei ist allerdings darauf zu achten, dass sich dies negativ auf die Lesbarkeit des Codes auswirken kann.
Abbildung in dieser Leseprobe nicht enthalten
Listing 3.3: optionale Verwendung der impliziten Typisierung
Das var Schlüsselwort ist jedoch nur an folgenden Stellen zulässig:
- Für Variablen innerhalb von Methoden,
- In Initialisierungen (),
- In foreach Anweisungen (foreach (var item in list)),
- In using Anweisungen (using (var file = File.Open("test.txt", FileMode.Open))).
Da der Compiler den Typ der Variablen bestimmen können muss, ist es außerdem nicht möglich, eine mit var spezifizierte Variable mit null zu initialisieren.[1]
Auch bei der Variablendefinition mit Hilfe der impliziten Typisierung, bietet Visual Studio 2008 volle IntelliSense Unterstützung (Bild 3.1).
Abbildung in dieser Leseprobe nicht enthalten
Bild 3.1: IntelliSense bei impliziter Typisierung
Objektinitialisierer dienen dazu, den Feldern oder Eigenschaften eines Objektes schon beim Erstellen Werte zuzuweisen, ohne einen passenden Konstruktor zu benötigen.
Abbildung in dieser Leseprobe nicht enthalten
Listing 3.4: Objektinitialisierer
Auch über den Objektinitialisierer können nur Feldern oder Eigenschaften Werte zugewiesen werden, deren Zugriffsbeschränkungen dies zulassen und die nicht schreibgeschützt sind. Außerdem muss ein Standardkonstruktor vorhanden sein.
Die Hauptanwendung von Objektinitialisierern sind die anonymen Typen, da man deren Eigenschaften nur zum Zeitpunkt der Erstellung festlegen kann.
Auflistungsinitialisierer sind Objektinitialisierer speziell für Auflistungen, die das IEnumerable Interface implementieren und eine Add Methode besitzen. Mit ihnen kann man die Auflistungen schon bei der Erstellung mit Daten füllen, ohne jeden Eintrag manuell einfügen zu müssen. [1]
Abbildung in dieser Leseprobe nicht enthalten
Listing 3.5: Auflistungsinitialisierer
Anonyme Typen besitzen eine Reihe, vom Programmierer festgelegte schreib-geschütze Eigenschaften, welche in einem einzelnen Objekt zusammengefasst werden. Der Unterschied zu "normalen" Typen ist, dass anonyme Typen nicht explizit definiert werden müssen.
Abbildung in dieser Leseprobe nicht enthalten
Listing 3.6: Definition eines anonymen Typs
Der in Listing 3.6 erstellte anonyme Typ besitzt außer den schreibgeschützten Eigenschaften Vorname und Nachname keine weiteren Eigenschaften, Methoden oder Ereignisse, lediglich die von der Klasse Object geerbten Klassenmember. Anonyme Typen können außer in die Klasse Object in keine anderen Klassen oder Schnittstellen umgewandelt werden. Beim Kompilieren wählt der Compiler einen zufälligen Namen für den anonymen Typ, welcher zur Entwicklungszeit jedoch nicht bekannt ist und daher auch nicht verwendet werden kann (Listing 3.7).
Abbildung in dieser Leseprobe nicht enthalten
Listing 3.7: Zufällig gewählter Name eines anonymen Typs
Werden anonyme Typen mit den Werten aus anderen Variablen gefüllt, können auch die Namen für die Eigenschaften des anonymen Typs weggelassen werden. In diesem Fall werden die Namen der Variablen oder Eigenschaften verwendet, mit denen sie initialisiert wurden.
Abbildung in dieser Leseprobe nicht enthalten
Listing 3.8: Erstellung eines anonymen Typs ohne direkte Angabe von Eigenschaftsnamen
Stimmen bei mehreren anonymen Typen die Anzahl, der Typ sowie die Reihen-folge der Eigenschaften überein, behandelt der Compiler diese als gleichen Typ (Listing 3.9).
Abbildung in dieser Leseprobe nicht enthalten
Listing 3.9: gleiche anonyme Typen
Haben außerdem alle Eigenschaften zweier Instanzen des gleichen anonymen Typs die gleichen Werte, so werden auch die beiden Instanzen als gleich behandelt (Listing 3.10).
Abbildung in dieser Leseprobe nicht enthalten
Listing 3.10: gleiche anonyme Typen
Da anonyme Typen nur mit Hilfe des var Schlüsselwortes initialisiert werden können, gelten für sie die gleichen Einschränkungen hinsichtlich ihrer Verwen-dung wie bei der impliziten Typisierung (z.B. nur innerhalb von Methoden). Auch bei den anonymen Typen bietet Visual Studio die gewohnten IntelliSense Funktionen (Bild 3.2). [1]
Abbildung in dieser Leseprobe nicht enthalten
Bild 3.2: IntelliSense bei anonymen Typen
Mit Hilfe von Erweiterungsmethoden wird es dem Programmierer ermöglicht, bestehende Klassen um neue Methoden zu erweitern, ohne eine abgeleitete Klasse erstellen oder das Original ändern zu müssen. Einmal definiert, können die Erweiterungsmethoden verwendet werden, als wären es echte Instanzmethoden der erweiterten Klasse. Listing 3.11 zeigt die Verwendung der um die IsNullOrEmpty Methode erweiterte String Klasse.
Abbildung in dieser Leseprobe nicht enthalten
Listing 3.11: Verwendung der Erweiterungsmethode
Erweiterungsmethoden müssen in einer nicht generischen statischen Klasse definiert werden. Sie werden als statische Methoden definiert, später allerdings als Instanzmethoden behandelt.
Abbildung in dieser Leseprobe nicht enthalten
Listing 3.12: Add Erweiterungsmethode für die Int32 Klasse
Der erste Parameter von Erweiterungsmethoden muss mit dem this Modifi-zierer versehen werden und bestimmt den Typ für den die Methode entwickelt wird. Die restlichen Parameter stellen die Parameter der Methode dar. In Listing 3.12 wird die Klasse System.Int32 um die Methode Add erweitert, welche einen Parameter übergeben bekommt. Listing 3.13 zeigt die Verwen-dung der Add Methode.
Abbildung in dieser Leseprobe nicht enthalten
Listing 3.13: Verwendung der Erweiterungsmethode aus Listing 3.12
Um die Erweiterungsmethoden verwenden zu können, muss lediglich der Namespace eingebunden werden, in dem sich die Klasse mit den Methoden befindet.
Auch wenn die Erweiterungsmethoden scheinbar echte Instanzmethoden sind, ändern sie nichts an den Zugriffsbeschränkungen des erweiterten Typs. Auch von den Erweiterungsmethoden aus hat man nur Zugriff auf die öffentlichen Klassenmember.[1]
Erweiterungsmethoden sind leicht durch die IntelliSense Unterstützung zu erkennen, zum einen an dem Zusatz ’Erweiterung’ im IntelliSense Tooltip und zum anderen an dem eigenen Symbol für Erweiterungsmethoden (Bild 3.3).
Abbildung in dieser Leseprobe nicht enthalten
Bild 3.3: IntelliSense bei Erweiterungsmethoden
Erweiterungsmethoden sollten jedoch nur in Ausnahmefällen verwendet werden. Wird eine erweiterte Klasse geändert, jedoch nicht die Erweiterungs-methode, kann dies zu schwer auffindbaren Fehlern führen. Da es sich bei den Erweiterungsmethoden nicht um echte Instanzmethoden handelt, können daraus schwer nachvollziehbare Probleme resultieren, wie am folgenden Beispiel deutlich wird.
Abbildung in dieser Leseprobe nicht enthalten
Listing 3.14: Einfache Erweiterungsmethode
Abbildung in dieser Leseprobe nicht enthalten
Listing 3.15: Verwendung der Erweiterungsmethoden aus Listing 3.14
Der Aufruf der DoSomething Methode in Listing 3.15 kann theoretisch ohne Fehler abgeschlossen werden, obwohl myInstance mit null initialisiert wurde.[1]
Bei Lambda Ausdrücken handelt es sich um eine Erweiterung der mit dem C# 2.0 Standard eingeführten anonymen Methoden, welche zum Erstellen von Delegaten oder Ausdrucksbaumstrukturen verwendet werden. Im Gegensatz zu anonymen Methoden, ermöglichen Lambda Ausdrücke jedoch einen mehr deklarativen Programmierstil.
Abbildung in dieser Leseprobe nicht enthalten
Listing 3.16: Einfacher Lambda Ausdruck
Der Ausdruck aus Listing 3.16 bekommt den Parameter x übergeben und gibt das Ergebnis von x * 2 als Rückgabewert zurück. Dabei befinden sich auf der linken Seite des Lambda Operators (=>) die Eingabeparameter und auf der rechten Seite der eigentliche Ausdruck.
Abbildung in dieser Leseprobe nicht enthalten
Listing 3.17: einfacher Delegat
Jetzt kann man eine Instanz des Delegaten anlegen und dieser einen Lambda Ausdruck zuweisen.
Abbildung in dieser Leseprobe nicht enthalten
Listing 3.18: Erstellung eines Lambda Ausdruckes
Lambda Ausdrücke werden hauptsächlich in den methodenbasierten LINQ Abfragen an allen IEnumerable und IQueryable Klassen verwendet.
Abbildung in dieser Leseprobe nicht enthalten
Listing 3.19: Methodenbasierte LINQ Abfrage unter Verwendung eines Lambda Ausdruckes
Für derartige Abfragen bringt das .Net Framework eine Reihe generischer Delegat Typen mit.
- public delegate TResult(TResult);
- public delegate TResult(T, TResult);
- public delegate TResult(T1, T2, TResult);
- public delegate TResult(T1, T2, T3, TResult);
- public delegate TResult(T1, T2, T3, T4,TResult);
Bei dem Beispiel aus Listing 3.19 handelt es sich um den Delegaten: public delegate bool(int , bool);, was bedeutet, dass der Delegat einen Parameter vom Typ int übergeben bekommt und einen Rückgabewert vom Typ bool hat. Intern wird jetzt für jedes Element x der Liste myList überprüft, ob der Aus-druck x < 5 zutrifft.
Im Vergleich zu früheren C# Versionen entsteht somit ein deutlich deklarativer Stil.
Abbildung in dieser Leseprobe nicht enthalten
Listing 3.20: Version von Listing 3.19 ohne Lambda Ausdruck
Lambda Ausrücke können auch mehr als einen Eingabeparameter besitzen. Ein Ausdruck für den Delegaten public delegate bool(int, int, bool); könnte z.B. so aussehen.
Abbildung in dieser Leseprobe nicht enthalten
Listing 3.21: Lambda Ausdruck mit zwei Eingabeparametern
Neben einfachen Ausdrücken können Lambda Ausdrücke auch Anweisungen enthalten. Diese müssen dann lediglich in geschweiften Klammern geschrieben werden.[1]
Abbildung in dieser Leseprobe nicht enthalten
Listing 3.22: Lambda Ausdruck aus Listing 3.19 als Anweisungs-Lambda
LINQ to SQL ist ein objektrelationaler Mapper, welcher den Zugriff auf Microsoft SQL Datenbanken ermöglicht. Er übersetzt dazu die code-internen LINQ Abfragen in SQL Abfragen und über-führt in der Gegenrichtung die Antwort der Datenbank in .Net Objekte.
Im folgenden Kapitel wird ausführlich über die Funktionsweise und die Möglichkeiten von LINQ to SQL eingegangen.
Für die Untersuchungen wird Microsoft Visual Studio 2008 Professional mit dem zur Installation gehörenden Microsoft SQL Server 2005 Express Edition verwendet. Als eigentliche Datenquelle dient eine SQL Datenbankdatei, welche mit Hilfe des folgenden Verbindungsstrings an die Datenbank angehängt wird.
Abbildung in dieser Leseprobe nicht enthalten
Listing 4.1: SQL Verbindungsstring
Der Verbindungsstring in Listing 4.1 gibt an, dass die Datenquelle der lokale SQL Express Server mit angehängter Datenbankdatei ist. User Instance=True legt fest, dass die Datei nicht an den global konfigu-rierten Server angehängt werden soll, sondern an eine neue Benutzerinstanz. Integrated Security=True sorgt lediglich dafür, dass zur Authentifizierung das aktuelle Windows Benutzerkonto verwendet wird.
Da zur Untersuchung von LINQ to SQL eine einfache Windows Forms Anwen-dung verwendet wird, werden die SQL Abfragen lediglich in eine, auf der Form platzierten, TextBox ausgegeben (Listing 4.4).
Abbildung in dieser Leseprobe nicht enthalten
Listing 4.2: Code der Testform
Die eigentlichen Abfragen befinden sich in der MyDataContext Klasse, welche von DataContext abgeleitet ist und somit den Zugriff auf die Daten-bank realisiert.
Abbildung in dieser Leseprobe nicht enthalten
Listing 4.3: MyDataContext Klasse
Um die SQL Ausgaben zu erhalten, muss der Log Eigenschaft der DataContext Instanz ein TextWriter Objekt zugewiesen werden. Da die Ausgaben ledig-lich in einer TextBox auf der Testform stehen sollen, wird eine entsprechende Ableitung der TextWriter Klasse verwendet.
Abbildung in dieser Leseprobe nicht enthalten
Listing 4.4: SQLLogWriter zur Ausgabe der SQL Abfragen
In Listing 4.4 ist zu erkennen, dass in der WriteLine Methode die eckigen Klammern des übergebenen Strings entfernt werden. Der Grund dafür ist, dass die Log Ausgaben des LINQ to SQL Providers nicht die endgültig abgesetzten SQL Abfragen darstellen, sondern die interne Darstellung, wie sie von der System.Data.SqlClient.SqlCommand Klasse verwendet wird. Dabei werden, wie beim MS SQL Server üblich, die Spalten- und Tabellennamen in eckige Klammern gesetzt und außerdem für die Parameter Platzhalter verwen-det.
Abbildung in dieser Leseprobe nicht enthalten
Listing 4.5: Beispielausgabe des LINQ to SQL Loggers
Listing 4.5 zeigt die Ausgabe des LINQ to SQL Loggers. @p0 ist dabei der Platzhalter für einen Parameter, welcher, mit einigen Zusatzinformationen, als Kommentar unter der eigentlichen Abfrage steht.
Für dieses Dokument werden die ausgegebenen SQL Abfragen jedoch in normale SQL Abfragen umgeformt, um die Lesbarkeit zu erhöhen.
Abbildung in dieser Leseprobe nicht enthalten
Listing 4.6: Umgeformte Abfrage aus Listing 4.5
Als Testdatenbank wird eine einfache Kunden-Verkaufsdatenbank verwendet, die nach folgendem Schema aufgebaut ist.
Die Auskapselung der Adresse eines Kunden dient zur Demonstration von 1:1 Beziehungen.
Abbildung in dieser Leseprobe nicht enthalten
Bild 4.1: Die verwendete Datenbank
LINQ Abfragen werden nach einer SQL ähnlichen Syntax aufgebaut. Das folgende Bild zeigt das Syntaxdiagramm für LINQ Abfragen. [2]
Abbildung in dieser Leseprobe nicht enthalten
Bild 4.2: LINQ Syntax
Bevor man LINQ to SQL jedoch verwenden kann, müssen die zu verwendenden Tabellen als Klassen abgebildet werden. Zur Abbildung der Tabellen werden die im System.Data.Linq.Mapping vorhandenen Attribute verwendet. Um also die Kundentabelle als Klasse darzustellen, wird folgender Code benötigt.
Abbildung in dieser Leseprobe nicht enthalten
Listing 4.7: Abbildung der Kunden Tabelle
Um dem .Net Framework zu sagen, dass es sich bei der Klasse um die Abbil-dung einer Tabelle in der Datenbank handelt, muss die Klasse mit dem Table Attribut versehen werden. Das Attribut hat als einzige Eigenschaft den Namen der Tabelle. Lässt man den Namen jedoch, wird vom LINQ to SQL Provider angenommen, dass die Tabelle den gleichen Namen wie die Klasse hat. In dem Beispiel in Listing 4.7 könnte man den Namen also auch weglassen.
Um die Felder der Klasse an die entsprechenden Spalten in der Tabelle zu binden, wird das Column Attribut verwendet. Über die Name Eigenschaft wird der Name der zugehörigen Spalte angegeben. Wird dieser weggelassen, wird vom LINQ to SQL Provider der Name des Feldes verwendet.
Das Column Attribut hat jedoch noch einige weitere Eigenschaften. Die wichtigsten sind:
- CanBeNull: legt fest, ob eine Spalte null-Werte enthalten darf
- DbType: legt den Typ der Spalte fest
- IsDbGenerated: legt fest, ob der Wert der Spalte von der Datenbank
generiert wird.
- IsDiscriminator: legt fest, ob die Spalte einen Diskriminatorwert enthält
(für Vererbung).
- IsPrimaryKey: legt fest, ob die Spalte den Primärschlüssel enthält.
- IsVersion: legt fest, ob die Spalte Versionsinformationen enthält.
- Name: legt den Namen der Spalte fest.
- Storage: legt das private Feld fest, das den Wert der Spalte enthält.
Anhand der verwendeten Eigenschaften ist also zu erkennen, dass es sich bei der Spalte Id in Listing 4.7 um den Primärschlüssel handelt, welcher außer-dem von der Datenbank generiert wird (Identitätsspalte).
Das Column Attribut kann an allen Feldern oder Eigenschaften verwendet werden, solange diese nicht schreibgeschützt sind. Verwendet man es jedoch an einer schreibgeschützten Eigenschaft, muss über die Storage Eigenschaft ein Feld angegeben werden, in welchem der Wert gespeichert werden soll.
Abbildung in dieser Leseprobe nicht enthalten
Listing 4.8: Verwendung der Storage Eigenschaft
Stellt man in LINQ to SQL Abfragen Bedingungen (z.B. Where) an bestimmte Felder, ist dies nur an denen möglich, welche mit dem Column Attribut ver-sehen worden sind. Da man jedoch nach den objektorientierten Prinzipien Felder nicht als publik deklariert, sondern den Zugriff über Eigenschaften kontrolliert, ist das Vorgehen aus Listing 4.8 die bessere Wahl.
Hat man das Column Attribut also wie in Listing 4.7 an den privaten Feldern, sind Bedingungen an die öffentlichen Eigenschaften dieser Felder später nicht möglich.[3] [4]
Um eine Abfrage an die Datenbank zu senden, muss man als erstes eine Instanz der System.Data.Linq.DataContext Klasse erzeugen und dieser einen Verbindungsstring mit den nötigen Informationen übergeben, um sich mit der Datenbank zu verbinden.
Der bevorzugte Weg für die Verwendung der DataContext Klasse ist eine eigene, von ihr abgeleitete Klasse zu erstellen und diese mit der benötigten Funktionalität zu versehen. Im ersten Beispiel wird sie allerdings aus Gründen der Lesbarkeit noch direkt instanziiert.[4]
Hat man jedoch eine Instanz, kann man sich über die generische Methode GetTable eine Referenz auf die entsprechende Tabelle holen und die Abfrage schreiben.
Abbildung in dieser Leseprobe nicht enthalten
Listing 4.9: Einfache LINQ to SQL Abfrage
In Listing 4.9 wird als erstes die DataContext Instanz erstellt und dann über die GetTable Methode die Referenz auf die Kundentabelle geholt. In der eigentlichen Abfrage ist kunde ein frei wählbarer Bezeichner, welcher ein einzelnes Element aus der Tabelle darstellt. Mit select kunde wird festge-legt, dass das Ergebnis den gesamten Kunden enthalten soll.
Die Abfrage wird jedoch vom Compiler nicht in dieser Form übernommen, son-dern in methodenbasierte Abfragen und Lambda Ausdrücke übersetzt.
Abbildung in dieser Leseprobe nicht enthalten
Listing 4.10: Vom Compiler übersetzte Version von Listing 4.9
Der Lambda Ausdruck kunde => kunde bedeutet, dass der Eingabepara-meter kunde unverändert zurückgegeben werden soll. Der Typ von kunde wird hierbei automatisch aus dem Kontext ermittelt, da es sich bei der kundenTable um eine Instanz der generischen Klasse Table<Kunde> handelt.
Auf die Übersetzung der LINQ Abfragen in die methodenbasierte Syntax wird in diesem Dokument jedoch nicht weiter eingegangen, da dies nach einem fest definierten Schema abläuft.[2]
Da es sich bei der Abfrage aus Listing 4.9 nur um eine sehr einfache Abfrage handelt, ist auch die endgültige SQL Abfrage nicht weiter kompliziert.
Abbildung in dieser Leseprobe nicht enthalten
Listing 4.11: In SQL übersetzte Version der Abfrage aus Listing 4.9
Will man sich nur die Vornamen aller Kunden auflisten lassen, lässt sich dies durch eine geringfügige Änderung der Abfrage erreichen (Listing 4.12).
Abbildung in dieser Leseprobe nicht enthalten
Listing 4.12: Auswahl der Vornamen aller Kunden
Auch die Abfrage aus Listing 4.12 erzeugt die gleiche SQL Abfrage wie schon die Abfrage aus Listing 4.9. Das Auswählen der Vornamen wird also nicht schon durch die Datenbank vorgenommen, sondern erst durch das .Net Frame-work.
Wie man in Listing 4.12 sieht, können schon kleine Änderungen der Abfrage den Typ des Ergebnisses beeinflussen. Daher wird in vielen Dokumenten für LINQ to SQL Abfragen die Implizite Typisierung verwendet, um den Änderungs-aufwand und auch das Schreiben solcher Abfragen zu vereinfachen (Listing 4.13). [3]
Abbildung in dieser Leseprobe nicht enthalten
Listing 4.13: Verwendung der impliziten Typisierung
Auch where Bedingungen lassen sich mit LINQ to SQL ganz einfach formu-lieren.
Abbildung in dieser Leseprobe nicht enthalten
Listing 4.14: Einfache where Bedingung
Anstelle von Vergleichsoperatoren kann man auch Methoden oder Eigenschaf-ten verwenden, welche einen Rückgabewert vom Typ bool haben und für die dem LINQ to SQL Provider eine Übersetzung in SQL bekannt ist. Die Abfrage aus Listing 4.14 könnte man also auch wie folgt schreiben.
Abbildung in dieser Leseprobe nicht enthalten
Listing 4.15: Equals anstelle des Vergleichsoperators
Beide Abfragen werden vom LINQ to SQL Provider in die gleiche SQL Abfrage übersetzt.
Abbildung in dieser Leseprobe nicht enthalten
Listing 4.16: SQL Abfrage von Listing 4.14 und Listing 4.15
Für die meisten Funktionen, die für derartige Vergleiche Verwendung finden, existieren auch SQL Übersetzungen, so auch für StartsWith und EndsWith.
Abbildung in dieser Leseprobe nicht enthalten
Listing 4.17: Verwendung der StartsWith Methode
Die Abfrage aus Listing 4.17 wird unter Verwendung des LIKE Operators in SQL übersetzt.
Abbildung in dieser Leseprobe nicht enthalten
Listing 4.18: In SQL übersetzte Version von Listing 4.17
Verwendet man hingegen EndsWith wird folgende SQL Abfrage generiert.
Abbildung in dieser Leseprobe nicht enthalten
Listing 4.19: SQL Abfrage bei Verwendung von EndsWith
Bei Methoden, für die jedoch keine SQL Übersetzungen existieren, wie z.B. eigene Erweiterungsmethoden, meldet das Framework bei Ausführung einen Fehler. Listing 4.20 verwendet eine selbst erstellte Erweiterungsmethode, welche dem LINQ to SQL Provider daher nicht bekannt sein kann.
Abbildung in dieser Leseprobe nicht enthalten
Listing 4.20: Verwendung einer eigenen Erweiterungsmethode
Führt man die Abfrage aus Listing 4.20 aus, erhält man eine Exception mit folgender Fehlermeldung: „Für die Boolean IsNullOrEmpty(System.String)-Methode gibt es keine unterstützte Übersetzung in SQL.“
Neben solch einfachen können auch komplexere Bedingungen, unter Verwendung der normalen Verknüpfungsoperatoren, gebildet werden.
Abbildung in dieser Leseprobe nicht enthalten
Listing 4.21: Mehrere verknüpfte Bedingungen
Auch die Übersetzung solcher Bedingungen erfolgt problemlos in die entspre-chende SQL Abfrage (Listing 4.22).[3]
Abbildung in dieser Leseprobe nicht enthalten
Listing 4.22: SQL Übersetzung von Listing 4.21
Neben einfachen Abfragen sind mit LINQ auch verschachtelte Abfragen möglich. Diese Anfragen lassen sich im Prinzip genauso bilden, wie man es aus normalem SQL gewohnt ist.
Abbildung in dieser Leseprobe nicht enthalten
Listing 4.23: Einfache verschachtelte Abfrage
In Listing 4.23 werden alle Kunden, deren Alter über dem Durchschnittsalter liegt, zurück gegeben. Um dies zu erreichen, werden in der inneren Abfrage die Geburtsjahre aller Kunden erfragt und der Durchschnitt daraus errechnet. Da in der inneren Abfrage nur das Geburtsjahr aller Kunden abgefragt wird, ist das Ergebnis daraus eine Liste mit Integerwerten. Um jetzt den Durchschnitt der Ergebnisse zu berechnen, wird die Average Erweiterungsmethode, welche an allen Auflistungstypen mit numerischen Inhalten verfügbar ist, verwendet.
Wie man dann an der SQL Übersetzung der Abfrage (Listing 4.24) sieht, greift der LINQ to SQL Provider auf die entsprechende SQL Methode, im aktuellen Fall also auf AVG, zurück.[3]
Abbildung in dieser Leseprobe nicht enthalten
Listing 4.24: SQL Übersetzung von Listing 4.23
Hier sieht man jedoch die Verwendung von Funktionen (Convert), welche nicht zum SQL Standard gehören und die eine Verwendung mit anderen SQL Datenbanksystemen nicht möglich macht.[3]
Nach dem gleichen Prinzip, wie schon die nicht-korrelierenden Abfragen, lassen sich auch korrelierende Abfragen erstellen. Dabei verwendet man einfach die gewünschten Werte aus der äußeren Abfrage auch für die innere.
Abbildung in dieser Leseprobe nicht enthalten
Listing 4.25: Korrelierende Abfrage
In Listing 4.25 werden alle Kunden abgefragt, deren Geburtsdatum mehr als einmal vorkommt. Um die Anzahl zu bestimmen, wird an der inneren Abfrage die Count Erweiterungsmethode verwendet, welche später in die entsprechende SQL Funktion übersetzt wird.
Abbildung in dieser Leseprobe nicht enthalten
Listing 4.26: SQL Abfrage aus Listing 4.25
Die Ergebnisse einer LINQ to SQL Abfrage lassen sich natürlich auch nach Belieben ordnen. Dies erfolgt wie in normalen SQL auch, über die orderby Funktion.
Abbildung in dieser Leseprobe nicht enthalten
Listing 4.27: Geordnete Abfrage
Die orderby Funktion wird dabei direkt in die ORDER BY SQL Funktion übersetzt.
Abbildung in dieser Leseprobe nicht enthalten
Listing 4.28: SQL Übersetzung von Listing 4.27
Natürlich ist auch das Ordnen nach mehr als einer Spalte möglich. Dazu werden einfach die Spalten, nach denen das Ergebnis geordnet werden soll, nacheinander durch Kommata getrennt, aufgelistet.
Abbildung in dieser Leseprobe nicht enthalten
Listing 4.29: Nach mehreren Spalten ordnen
Zusätzlich kann man mit Hilfe von ascending und descending auch die Richtung, nach der sortiert werden soll, angeben. Dies lässt sich sowohl für alle Spalten gemeinsam festlegen wie auch für jede Spalte separat. Wird allerdings auf die Angabe verzichtet, wird das Ergebnis automatisch aufsteigend sortiert.[5]
Abbildung in dieser Leseprobe nicht enthalten
Listing 4.30: Nach allen angegebenen Spalten aufsteigend sortieren
Abbildung in dieser Leseprobe nicht enthalten
Listing 4.31: Unterschiedliche Sortierung der einzelnen Spalten
Auch dies kann direkt in die entsprechende SQL Abfrage überführt werden.
Abbildung in dieser Leseprobe nicht enthalten
Listing 4.32: SQL Übersetzung von Listing 4.31
In Listing 4.32 fällt auf, dass die Angabe der Sortierrichtung bei der Spalte Nachname fehlt. Der Grund dafür ist, dass ohne Angabe der Richtung automa-tisch aufwärts sortiert wird.
Will man die Ergebnisse einer LINQ to SQL Abfrage nach bestimmten Para-metern gruppieren, erfolgt dies über die group by Funktionen. Im Gegensatz zur GROUP BY SQL Funktion werden in LINQ to SQL nicht nur die Gruppen gebildet, sondern auch gleich deren Inhalt erfasst.
Abbildung in dieser Leseprobe nicht enthalten
Listing 4.33: Gruppieren der Kunden nach dem Geburtsjahr
In Listing 4.33 werden die Kunden dem Geburtsjahr nach gruppiert. Will man, wie in Listing 4.33 dargestellt, lediglich die gruppierten Ergebnisse, kann man die select Klausel weglassen, da die Gruppen automatisch zurück gegeben werden. Das Ergebnis einer solchen Gruppierung ist eine Auflistung von IGrouping<TKey, TElement> Objekten. Da IGrouping selbst die IEnumerable Schnittstelle implementiert, erhält man eine Liste von Listen. Dabei ist die äußere Liste eine Auflistung der Gruppen und jede Gruppe ist eine Auflistung der Elemente innerhalb der Gruppe.
Um durch die Gruppen aus Listing 4.33 zu iterieren, wird folgender Code benötigt.
Abbildung in dieser Leseprobe nicht enthalten
Listing 4.34: Durchlaufen der Ergebnisse von Listing 4.33
In Listing 4.34 ist gruppe.Key der Schlüssel, nach dem gruppiert wurde, also das Geburtsjahr der Kunden und gruppe selbst ist die Auflistung der Kunden mit dem entsprechenden Alter.
Da hier bereits die Inhalte der einzelnen Gruppen bekannt sind, kann man davon ausgehen, dass der LINQ to SQL Provider mehr als nur eine Anfrage an die Datenbank gesendet hat.
Abbildung in dieser Leseprobe nicht enthalten
Listing 4.35: SQL Abfragen von Listing 4.33
In Listing 4.35 erkennt man, dass der LINQ to SQL Provider im aktuellen Fall vier SQL Abfragen benötigt, um alle benötigten Daten zu erhalten. In der ersten Abfrage werden die vorhandenen Gruppen bestimmt, um dann in den nächsten drei Abfragen die Kunden jeder Altersgruppe zu bestimmen.
Geht man davon aus, dass jedoch sehr viele Gruppen existieren, könnte dieses Vorgehen eventuell zu Performanceproblemen führen. Die Tatsache, dass jedoch die Inhalte der Gruppen nicht sofort abgefragt werden, sondern erst in dem Moment, wo auf sie zugegriffen wird, könnte diesem entgegenwirken. Da eine Performanceuntersuchung jedoch nicht Bestandteil dieser Arbeit ist, wird darauf nicht weiter eingegangen.
Will man jedoch weitere Operationen mit der Gruppe durchführen oder Bedin-gungen stellen, muss man der Gruppe mit Hilfe des into Schlüsselwortes einen Bezeichner zuweisen.
Abbildung in dieser Leseprobe nicht enthalten
Listing 4.36: Abfrage der Gruppen mit mindestens fünf Kunden
Würde man die Abfrage aus Listing 4.36 in SQL formulieren, könnte man dies mit Hilfe der HAVING Klausel realisieren. Der LINQ to SQL Provider erzeugt jedoch folgende Abfrage.
Abbildung in dieser Leseprobe nicht enthalten
Listing 4.37: SQL Abfrage von Listing 4.36
Listing 4.37 zeigt, dass eine verschachtelte Abfrage verwendet wird, bei der in der inneren Abfrage die Gruppen und die Anzahl der enthaltenen Elemente bestimmt werden und in der äußeren über die where Bedingung diejenigen mit weniger als fünf Elementen herausgefiltert werden.
Bisher wurden als Gruppenschlüssel lediglich Int32 Werte verwendet, es sind jedoch alle beliebigen Typen, so auch komplexe Typen möglich, mit denen man zusammengesetzte Schlüssel erzeugen kann.
Abbildung in dieser Leseprobe nicht enthalten
Listing 4.38: Zusammengesetzter Gruppenschlüssel
Listing 4.38 verwendet als Gruppenschlüssel einen anonymen Typen, daher muss man das Ergebnis der Abfrage in diesem Fall als var deklarieren. Anstelle des anonymen Typen lassen sich jedoch auch alle benannten Typen verwenden, was dann auch eine explizite Typisierung möglich machen würde.
Abbildung in dieser Leseprobe nicht enthalten
Listing 4.39: Durch die Ergebnisse aus Listing 4.38 iterieren
Abbildung in dieser Leseprobe nicht enthalten
Listing 4.40: SQL Abfrage von Listing 4.38
Listing 4.40 zeigt, dass die Bestimmung der Gruppen und die Erfassung deren Elemente analog zu einfachen Gruppenschlüsseln erfolgen. Die komplexe where Bedingung dient lediglich zur korrekten Behandlung von NULL Spalten, im aktuellen Fall also Nachname.[5]
Auch Joins sind direkt in den LINQ Abfragen möglich, auch wenn diese nur selten benötigt werden, da man solche Beziehungen in der Regel fest definie-ren kann und der LINQ to SQL Provider diese dann selbst auflöst.
Abbildung in dieser Leseprobe nicht enthalten
Listing 4.41: Einfacher JOIN
Da das Ergebnis des Joins aus verschiedenen Objekten besteht, werden diese in Listing 4.41 durch einen anonymen Typen zusammengefasst.
Abbildung in dieser Leseprobe nicht enthalten
Listing 4.42: Abfrage aus Listing 4.41
Neben einem einfachen Join lässt sich über einen kleinen Umweg auch ein Left Outer Join nachbilden. Dazu wird die Erweiterungsmethode DefaultIfEmpty verwendet, welche entweder die Elemente der Auflistung zurück gibt oder, sollte die Auflistung leer sein, einen Defaultwert. Der Defaultwert ist null bei Referenztypen und der Standardwert bei Wertetypen.
Abbildung in dieser Leseprobe nicht enthalten
Listing 4.43: Nachbildung eines Left Outer Joins
Bei dem Code in Listing 4.43 wird das Ergebnis des Joins in die Variable tmpProdukte gelegt, aus welcher dann mit Hilfe einer weiteren from Klausel die Einträge gelesen werden. Sollten jedoch keine Einträge vorhanden sein, sorgt die DefaultIfEmpty Methode dafür, dass null zurück gegeben wird. Das Ergebnis daraus ist eine Auflistung aller Kategorien und falls vorhanden, der enthaltenen Produkte.[1]
Dass dies vom LINQ to SQL Provider richtig interpretiert wird, zeigt Listing 4.44.
Abbildung in dieser Leseprobe nicht enthalten
Listing 4.44: Abfrage von Listing 4.43
Der IN Operator lässt sich in LINQ to SQL Abfragen sehr einfach nachbilden. Verwendet man die Contains Erweiterungsmethode einer IEnumerable Instanz um zu prüfen, ob ein Spaltenwert darin enthalten ist, wird diese mit Hilfe des IN Operators übersetzt.
Abbildung in dieser Leseprobe nicht enthalten
Listing 4.45: Nachbildung des IN Operators
Abbildung in dieser Leseprobe nicht enthalten
Listing 4.46: Übersetzung von Listing 4.45
Bei der negierten Version von Listing 4.45 wird auch in der SQL Übersetzung der negierte IN Operator, also NOT IN verwendet.
Abbildung in dieser Leseprobe nicht enthalten
Listing 4.47: Negierte Abfrage aus Listing 4.45
Abbildung in dieser Leseprobe nicht enthalten
Listing 4.48: Übersetzung von Listing 4.45
Verwendet man in Listing 4.45 anstelle des Arrays eine innere Abfrage, wird dies mit Hilfe von EXISTS gelöst.[1]
[...]
Kommentare