LINQ – Verstehen und Einsetzen - Software and Systems Engineering

Transcrição

LINQ – Verstehen und Einsetzen - Software and Systems Engineering
Objektorientiertes Programmieren mit .NET und C#
Proseminar im Wintersemester 2010/2011
LINQ – Verstehen und Einsetzen
Georg Wagner
Technische Universität München
Abstract: LINQ ist eine Sammlung von Spracherweiterungen und Klassen
und wurde mit dem .NET Framework 3.5 eingeführt. Mit LINQ wird der
Zugriff auf Datenquellen wie Datenbanken, XML-Dateien oder Datasets vereinheitlicht und vereinfacht. Daraus resultiert, dass das Abfragen und Ändern
von Daten weniger Code benötigt und somit die Produktivität und die Qualität des Codes zunimmt.
Diese Ausarbeitung stellt eine Einführung in LINQ dar. Ausgehend von einem groben Überblick der Bestandteile von LINQ und seinen Entwurfszielen,
werden die zum Verständnis von LINQ benötigten C# Spracherweiterungen
erläutert. Ferner wird auf die wesentliche Konzepte von LINQ eingegangen
und deren Anwendung beispielhaft bei der Arbeit mit einer Datenbank in
Microsoft-SQL-Server demonstriert.
1
Einleitung
LINQ bietet eine Methodologie, die die Implementierung verschiedener Arten von
Zugriffen auf Datenquellen vereinfacht und vereinheitlicht. Es kann als eine Sammlung von Spracherweiterungen angesehen werden, die im November 2007 mit der
Veröffentlichung des .NET Frameworks 3.5 miteingingen.
1.1
Entwurfsziele von LINQ
Beim Schreiben von .NET-Anwendungen kommt oft vor, dass an bestimmten Stellen mit persistenten Daten gearbeitet werden muss. Diese Daten können aus vielerlei Quellen bezogen werden, hierzu gehören Datenbanken, XML-Dateien oder
Datasets, um nur einige zu nennen.
Das Problem mit der Arbeit mit solchen Datenquellen ist, dass Sie den Entwickler dazu zwingen mit Abstraktionsmodellen zu arbeiten, die für objektorientierte
Sprachen wie C# oder VB.NET mit Anpassungsaufwand auszuwerten sind. Exemplarisch dafür sind relationale Datenbanken, da sie den Entwickler dazu zwin-
gen mit Tabellen und Spalten zu arbeiten. Möchte man einige Daten aus solchen
Tabellen extrahieren, im Code weiterverarbeiten und wieder persistieren, ist viel
Anpassungscode nötig. Selbiges gilt auch für andere Datenquellen. Dieses Unvermögen eines Systems, den Output eines anderen Systems zu empfangen, wird auch
als Impedanzunterschied oder Fehlanpassung genannt. LINQ wurde von Microsoft
vor allem dazu eingeführt, diese Fehlanpassung zu reduzieren [Hej06].
Tatsächlich wird man mit LINQ befähigt mit gleich bleibenden Sprachfeatures
auf Datenquellen der unterschiedlichsten Paradigmen zuzugreifen. Die einheitliche
Abfragesyntax zur Datenabfrage und Datenmanipulation wird dabei direkt in die
Programmiersprache integriert. Mithilfe vorausgehendem Mapping (siehe Kapitel
5.1) kann dann auf Daten direkt zur Entwurfszeit typsicher zugegriffen werden, was
die Anzahl möglicher Fehler zur Laufzeit reduziert. Dadurch ließen sich Intellisense
und Debugging auch für LINQ-Anfragen verfügbar machen.
1.2
Bestandteile von LINQ
Zum formulieren von LINQ-Anfragen können Abfrageausdrücke, Abfrageoperatoren und Ausdrucksbäume verwendet werden. Abfrageoperatoren stellen lediglich
eine Untermenge von Erweiterungsmethoden (siehe Kapitel 2.4) dar. Abfrageausdrücke wiederum erweitern die Syntax von C# und VB.NET um SQL-ähnliche
Schlüsselwörter, die im Grunde in Aufrufe von Abfrageoperatoren übersetzt werden. Wenn Abfrageausdrücke bzw. Abfrageoperatoren in andere Abfragesprachen
wie SQL übersetzt werden müssen, liegen die Abfragen als Ausdrucksbaum vor. Zu
diesen sei in dieser Arbeit nur soviel gesagt, das sie für Übersetzungen in andere
Abfragesprachen effizienter analysiert werden können als Zeichenfolgen.
Um mit den von LINQ unterstützten Datenquellen arbeiten zu können, werden
die Abfrageoperatoren spezifisch für die Datenquelle implementiert und spezielle
Klassen bereitgestellt. Abbildung 1 zeigt die in .NET Framework 4.0 unterstützte
LINQ-Implementierungen:
In dieser Arbeit werden sowohl LINQ to Objects als auch LINQ to SQL behandelt. Mit LINQ to Objects kann man Abfragen auf Auflistungen von Objekten
ausführen. So lassen sich Daten, die temporär in Auflistungen gespeichert werden,
bequem über Abfragen filtern und sortieren. Mittels LINQ to SQL kann man mit
den gleichen Abfragekonzepten ebenso Abfragen auf Datenbanken ausführen. Allerdings werden nur Microsoft-SQL-Server-Datenbanken unterstützt. Dieses Problem
wird durch LINQ to Entities umgangen. Es unterscheidet sich darin, dass es mit
einer Abstraktionsschicht arbeitet und damit mit jedem Datenbanksystem arbeiten kann, das einen Entity-Framework-Provider anbietet. Die verbliebene LINQImplementierungen ermöglichen gemäß ihres Namens XML-Dokumente oder Da-
Abbildung 1: Wichtige Bestandteile von LINQ (Quelle: In Anlehnung an [MEW08, 30])
taSet-Objekte abzufragen.
LINQ lässt sich auch um weitere Implementierungen erweitern. Zahlreiche Implementierungen lassen sich bereits über das Internet von Drittanbietern runterladen.
So wird zum Beispiel von Amazon LINQ to Amazon angeboten, welche LINQAbfragen in die von Amazon unterstützten REST-URLs konvertiert. Die Antwort
wird anschließend in XML-Daten zurückgegeben, die über LINQ to XML in .NETObjekte umgewandelt werden können.
2
Spracherweiterungen
Um die Konzepte von LINQ zu verstehen, müssen vorerst die mit C# 3.0 eingeführten Spracherweiterungen erläutert werden, die aufeinander aufbauend im Folgenden
erklärt werden.
2.1
Implizit typisierte lokale Variable
Lokale Variablen können unter Verwendung des var-Schlüsselwortes ohne Angabe
eines Typen deklariert werden. Dadurch weist man den Compiler an, den Variablentyp vom Ausdruck abzuleiten, der zum Initialisieren der Variable genutzt wird
[Mic10b].
Listing 1: Implizit typisierte lokale Variablen
1
var i = 8 ; // i wird zum Typ int
2
3
4
var d = 1 . 2 ; // d wird zum Typ double
//wird zum Typ Dictionary<String , DateTime>
var b i r t h d a y s = new D i c t i o n a r y <s t r i n g , DateTime > ( ) ;
Der Compiler leitet also den Variablentypen sowohl von Wert- als auch Referenztypen ab.
2.2
Objekt-Initialisierer
Objekt-Intialisierer ermöglichen bei der Instanziierung einer Klasse oder Struktur
nach dem Aufruf des Konstruktors in geschweiften Klammern eine Initialisierung
für die öffentlichen Felder bzw. öffentlichen Eigenschaften anzugeben [MEW08, 72].
var prod = new Product ( ) {name = " Notebook " , p r i c e = 5 9 9 . 9 9 }
2.3
Anonyme Typen
Beim Arbeiten mit LINQ kommt oft zur Anwendung, dass beim Abfragen von Daten selbige zur bequemen Bearbeitung zu Objekten gruppiert werden. Anstatt eine
Klasse für die Gruppierung der Daten zu definieren, wird kurzerhand ein abstrakter
Typ definiert:
Listing 2: Definition eines anonymen Typen
var s p r i n t e r = new { P l a t z = r a n g l i s t e . P l a t z ,
Vorname = r a n g l i s t e . Vorname , Nachname = r a n g l i s t e . Nachname}
Intern übersetzt der Compiler den anonymen Typen in eine Klasse und deklariert
die angegebenen Eigenschaften. Die für die Deklaration nötige Typangaben müssen
nicht angegeben werden. Diese kann der Compiler von dem Ausdruck ableiten, der
der Eigenschaft zugewiesen wird. Ist dieser Ausdruck eine Eigenschaft kann bei der
Deklaration ebenso der Eigenschaftsname ausgelassen werden. Das heißt, dass in
Listing 2 der Teilausdruck “Platz =” ausgelassen werden kann. Damit wird die
Eigenschaft nach dem Namen der zuweisenden Eigenschaft benannt.
Auf den Namen des anonymen Typen kann im Code nicht zugegriffen werden, da
der Name intern automatisch generiert wird und nach Außen nicht bekannt ist.
Von einem Weg über Reflection wird an dieser Stelle aufgrund der Performance
abgesehen [MEW08, 96].
Wenn nun der vom Compiler übersetzte Code ausgeführt wird, wird ein Objekt der
anonymen Klasse erstellt, wobei sprinter die Referenz zu diesem Objekt bekommt.
Ähnlich wie bei Objekt-Initialisierer werden dann die Eigenschaften des Objekts
gesetzt.
2.4
Erweiterungsmethoden
Mit Erweiterungsmethoden kann man bereits definierten Typen nachträglich Methoden hinzufügen. Diese Methoden müssen als ersten Parameter das Schlüsselwort
this vorangestellt haben.
Listing 3: Erweiterungsmethoden
1
2
3
4
s t a t i c c l a s s AStaticClass {
p u b l i c s t a t i c IEnumerable<TSource> Where<TSource >(
t h i s IEnumerable<TSource> s o u r c e ,
Func<TSource , bool > p r e d i c a t e ) { . . .
Damit wurde dem Compiler mitgeteilt, dass Objekte aus Klassen, welche die IEnumerable<TSource> Schnittstelle implementieren, die Erweiterungsmethode Where
besitzen. Diese Erweiterungsmethode lässt sich so aufrufen, als ob sie eine Instanzenmethode des implementierenden Objektes wäre. Beim Aufruf wird dann für den
1. Parameter kein Argument übergeben:
Listing 4: Erweiterungsmethoden
1
2
L i s t <Student> s t u d e n t s = GetListFromAnywhere ( ) ;
var f i l t e r e d S t u d e n t s = s t u d e n t s . Where ( Method ) ;
Wie hierbei zu sehen ist, steht Method für eine Methode, welche auch ein LambdaAusdruck sein kann (siehe Kapitel 2.5).
Hinsichtlich Instanzenmethoden haben Erweiterungsmethoden Einschränkungen,
da sie zum Beispiel nicht auf öffentliche Member zugreifen können. Zudem sind
Erweiterungsmethoden weniger „sichtbar“. Verwendet eine Instanzenmethode den
Namen einer Erweiterungsmethode, wird bei einem Methodenaufruf die Instanzenmethode aufgerufen.
2.5
Lambda-Ausdrücke
Die Lambda-Ausdrücke sind die letzte Spracherweiterung die wir zum wesentlichen
Verständnis von LINQ benötigen. Sie entspringen dem sog. Lambda-Kalkül aus
dem Gebiet der mathematischen Logik, das von Alonzo Church in den 1930er
Jahren eingeführt wurde [BB00, 5, 18].
Mit Lambda-Ausdrücken wird das Einfügen von Code an Stellen erlaubt, wo man
eigentlich eine Methoden-Übergabe an ein Delegate erwartet hätte:
Listing 5: Ein Lambda-Ausdruck
1
2
3
4
5
s t a t i c v o i d Main ( s t r i n g [ ] a r g s ) {
L i s t <Student> s t u d e n t s = GetListFromAnywhere ( ) ;
var f i l t e r e d S t u d e n t s = s t u d e n t s . Where (
s t u d e n t => s t u d e n t . name == " Otto " ) ;
...
Intern wandelt der Compiler den Lambda-Ausdruck in eine Methode um. Die
Eingabe-Parameter (siehe Abbildung 2) des Lambda-Ausdrucks definieren die Parameterliste dieser Methode. Diese Parameter müssen mit der Parameterliste des
Delegates übereinstimmen, welchem die Methode übergeben wird. In unserem Beispiel erwartet das Delegate mit der Signatur Func<Student, bool> eine Methode
mit einem Studenten-Objekt als Parameter. Mit dem Eingabe-Parameter student
wird ein solches Objekt deklariert. Der Compiler leitet den Typ dabei automatisch
aus der Delegate-Signatur ab [MEW08, 79,6].
Abbildung 2: Ein Lambda-Ausdruck
Wie in Abbildung 2 zu sehen ist, steht rechts vom Lambda-Operator ein Ausdruck, der den Methodenrumpf für die zu erstellende Methode darstellt. Das fehlende return-Schlüsselwort wird vom Compiler automatisch hinzugefügt. Möchte
man mehrere Anweisungen im Methodenrumpf stehen haben, kann man statt des
Ausdrucks auch einen Anweisungsblock verwenden. Bei diesem müssen allerdings
return und Semikolons verwendet werden. Hier ist ein Beispiel für einen solchen
Lambda-Ausdruck:
// Innerhalb der geschweiften Klammern sind auch mehrere
// Ausdrücke möglich
s t u d e n t => { r e t u r n s t u d e n t . Vorname == " Otto " ; }
3
LINQ-Abfragen mit LINQ to Objects
Nachdem die wichtigsten Spracherweiterungen von C# 3.0 gezeigt wurden, ist man
nun in der Lage zu verstehen, wie man sprachintegrierte Abfragen mit Abfrageausdrücken und Abfrageoperatoren formulieren kann. Solche LINQ-Abfragen haben
gemeinsam, dass sie auf Auflistungen von Objekten zugreifen. Damit sind alle Objekte von Klassen gemeint, die die generische Schnittstelle IEnumerable<T> implementieren. Solche Objekte werden im LINQ-Kontext Sequenzen genannt. In den
folgenden zwei Kapitel wird ausschließlich die LINQ-Implementierung LINQ to
Objects verwendet.
3.1
Abfrageausdrücke
Gegeben sei folgendes Beispiel: Es gibt eine Sequenz students, die nichts anderes
ist als eine Collection List<Student>. Aus dieser Collection sollen nun alle Studenten ausgefiltert werden, die das Proseminar .NET Programmierung machen und
lassen sie dem Nachnamen nach aufsteigend ordnen. Die Anfrage hierzu ist einfach
zu verstehen, da sie syntaktisch sehr ähnlich zu SQL ist:
Listing 6: Ein Abfrageausdruck zum Ausfiltern von Studenten
1
2
3
4
5
L i s t <Student> s t u d e n t s = GetListFromAnywhere ( ) ;
var p a r t i c i p a n t s = from s t u d e n t i n s t u d e n t s
where s t u d e n t . p r o s e m i n a r == " .NET␣ Programmierung "
o r d e r b y s t u d e n t . surname
s e l e c t student ;
Bei Abfrageausdrücken ist charakteristisch, dass sie mit einer from-Klausel beginnen und mit einer select- bzw. group-Klausel enden. Die from-Klausel ist hierbei
ein Generator, der die Variable student einführt, welche alle Studenten-Objekte
der Sequenz students durchläuft. Mit Where werden dann alle Besucher gefiltert
die das Proseminar besuchen und mit orderby ihrem Nachnamen nach aufsteigend
sortiert.
Die letzte select-Klausel spezifiziert die Gestalt der Abfrageausgabe [MEW08,
117]. Hier wird bestimmt, was aus dem Ergebnis der Auswertung der vorangehenden Klauseln auszuwählen ist. In Listing 6 kann man beispielsweise eine einzelne
Eigenschaft von student oder mit der Angabe des Objekts student selbst alle
Eigenschaften auswählen. Möchte man eine beliebige Kombination von Eigenschaften auswählen, kann man diese in der Select-Klausel in einem anonymen Typen
festhalten (siehe Listing 15). Bei Bedarf kann auch direkt eine Klasse instanziiert
werden, um später an anderer Stelle damit weiterarbeiten zu können. Im aktuellen
Beispiel macht es keinen Performance-Unterschied, ob das ganze Objekt oder nur
die nötige Eigenschaften ausgewählt werden, da in der ersten Zeile sowieso die ganze
Sequenz in den Speicher geladen wird. Das Ergebnis der Anfrage ergibt schließlich
eine Sequenz IEnumerable<Student>. Einfachheitshalber wird einer implizit typisierten Variable participants überlassen, den passenden Typ abzuleiten.
Die Namen der Studenten werden wie folgt ausgegeben:
Listing 7: Ausgeben der gefragten Studenten
1
2
3
f o r e a c h ( var p e r s o n i n p a r t i c i p a n t s )
{
C o n s o l e . WriteLine ( p e r s o n . surname ) ;
C o n s o l e . WriteLine ( p e r s o n . name ) ;
}
Für die andere Abfrage-Klauseln „SelectMany, Join, GroupJoin, GroupBy, Let,
Cast, ThenBy sei aus platztechnischen Gründen auf http://msdn.microsoft.
com/de-de/library/bb384065.aspx“ verwiesen.
3.2
Abfrageoperatoren
Als Abfrageoperatoren bezeichnet man lediglich eine Menge von Erweiterungsmethoden, die Operationen im Kontext von LINQ-Abfragen ausführen. Dazu gehören
vor allem die Erweiterungsmethoden, die in IEnumerable<T> deklariert sind. Jede
Klausel eines Abfrageausdrucks wie select oder where ruft den zu ihr passenden Abfrageoperator auf. Der Compiler übersetzt den Abfrageausdruck im letzten
Beipiel in folgende Abfrageoperator-Aufrufe:
Listing 8: Abfrageoperatoren zum Ausfiltern der Studenten
1
2
3
4
5
L i s t <Student> s t u d e n t s = GetListFromAnywhere ( ) ;
var p a r t i c i p a n t s = s t u d e n t s . Where (
s t u d e n t => s t u d e n t . p r o s e m i n a r == " .NET␣ Programmierung " )
. OrderBy ( s t u d e n t => s t u d e n t . surname )
. S e l e c t ( s t u d e n t => s t u d e n t ) ;
Im obigen Listing gibt jede Erweiterungsmethode eine Sequenz zurück – auch hier
vom Typ IEnumerable<Student>. Zugleich nimmt auch jede Erweiterungsmethode
eine derartige Sequenz entgegen. Ohne dieses so genannte Pipeline Pattern müsste
man Methoden ineinander verschachteln (siehe Listing 9), was der Lesbarkeit stark
schadet.
Jede Abfrageausdrucks-Klausel wird bekanntlich vom Compiler in einen Aufruf
eines Abfrageoperators übersetzt. Mit Abfrageoperatoren stehen mehr Möglichkeiten zum Arbeiten mit Sequenzen zur Verfügung, da mehr Abfrageoperatoren1
als Abfrageausdrucks-Klauseln existieren. So lassen sich mit Abfrageausdrücke beispielsweise keine Aggregationen oder Mengenoperationen durchführen. Der Programmierer muss sich aber nicht für eine bestimmte Abfrageform entscheiden, denn
Abfrageausdrücke und -operatoren lassen sich auch kombinieren (siehe Listing 15).
1 Für einen Einblick der zur Verfügung stehenden Abfrageoperatoren kann http://msdn.
microsoft.com/de-de/library/19e6zeyy.aspx konsultiert werden
4
Ausführung von LINQ-Abfragen im Detail
Alle LINQ-Abfragen aller LINQ-Implementierungen haben etwas gemeinsam: Die
Auswertung ihrer Abfragen wird so lange hinausgezögert, bis Abfrage-Ergebnisse
im Code eingelesen werden müssen. Dieses Konzept wird als die Verzögerte Ausführung bezeichnet.
4.1
Verzögerte Ausführung
Zur Illustration wird eine vereinfachte Version unseres vorangegangenen Beispiels
ohne die Sortierung genommen:
Listing 9: Abfrage-Auswertung erfolgt nicht bei Definition
1
2
3
4
5
6
var p a r t i c i p a n t s = s t u d e n t s . Where (
s t u d e n t => s t u d e n t . p r o s e m i n a r == " .NET" )
. S e l e c t ( s t u d e n t => s t u d e n t ) ;
// Quellsequenz students bekommt ein neues Element
s t u d e n t e n . Add( new Student ( ) { surname = " Weber " ,
p r o s e m i n a r = " .NET" } ) ;
7
8
9
f o r e a c h ( var p e r s o n i n p a r t i c i p a n t s ) {
C o n s o l e . WriteLine ( p e r s o n . surname ) ; }
Überraschenderweise wird nun bei der Ausgabe auch der hinzugefügte Student
Weber ausgegeben. Der Grund ist, dass die Abfrage erst bei der Iteration der
Ergebnis-Sequenz participants ausgeführt wird, also beim Durchgehen der Elemente von participants. Diese Iteration erfolgt bekanntlich so, dass das IEnumerable implementierende Objekt participants ein Iterator-Objekt an person
zurückgibt. person geht dann pro foreach-Durchlauf ein Element durch und gibt
ein Studenten-Objekt zurück. Dieses Verfahren hat den Effekt, dass die Daten
zum spätmöglichsten Zeitpunkt geliefert werden, und zwar genau dann, wenn die
Ergebnis-Objekte in der foreach-Schleife benötigt werden. Die Abfrage wird also
verzögert ausgeführt. [Mic10a]
Der Effekt der Verzögerten Ausführung geht aber zunichte, sobald Abfrageoperatoren genutzt werden, die Sortierungen, Gruppierungen oder Aggregationen vornehmen. Wenn beispielsweise Orderby genutzt wird, kann dieser Abfrageoperator
erst dann etwas zurückgeben, wenn er alle Studenten-Objekte besitzt. Andernfalls
wäre die Sortierung nicht möglich.
Fraglich bleibt noch, wie die Erweiterungsmethoden von IEnumerable<T> aufgebaut sind, da sie nicht eine ganze Sequenz zurückgeben, sondern die einzelne Elemente. Um dies auf den Grund zu gehen, muss vorerst gezeigt werden, wie der
Compiler Erweiterungsmethoden auflöst.
4.2
Auflösung von Erweiterungsmethoden
Wie bereits erwähnt wurde, werden Erweiterungsmethoden in statischen Klassen definiert. So sind beispielsweise die IEnumerable<T>-Erweiterungsmethoden
in Enumerable definiert worden. Der Compiler wandelt diese Erweiterungsmethoden allerdings in normale statische Methoden um. Gleichzeitig werden alle Aufrufe
der Erweiterungsmethode passend dazu umgewandelt [PR08, 44]. Konkret sieht
das bei unseren eben genutzten Abfrageoperatoren so aus:
Listing 10: Auflösung der Erweiterungsmethoden
1
2
3
4
var p a r t i c i p a n t s = Enumerable . S e l e c t (
Enumerable . Where ( s t u d e n t s ,
s t u d e n t => s t u d e n t . Proseminar == " .NET" ) ,
s t u d e n t => s t u d e n t ) ;
Jedes mal, wenn participants also iteriert wird, kommt die Kontrolle erst zu Select. Diese Methode erwartet ein Studenten-Objekt als Element, welches von der
Where-Methode zurückgegeben wird. Wenn nun ein solches Element zurückgegeben worden ist, wird zum Schluss dem Iterator person das Element weitergegeben.
Schließlich wird in Listing 9 in der foreach Schleife der Inhalt des Elements ausgegeben. Dieses Prozedere wiederholt sich bis die Sequenz participants durchiteriert
wurde.
Bei der nächsten Iteration ist nun ausschlaggebend, dass der Kontrollfluss der Abfrageoperatoren nicht wieder am Anfang des Methodenrumpfes beginnt. Stattdessen setzt sich die Kontrolle an der Stelle fort, wo für die letzte Iteration ein Element
zurückgegeben wurde. Dieses Verhaltensmerkmal lässt sich mit so genannanten CoRoutinen realisieren [MEW08, 106].
4.3
Die Co-Routinen
Co-Routinen sind zum Beispiel Where und Select. Anhand des Aufbaus von Where
wird ihre Verhaltensweise beleuchtet:
Listing 11: Die Where-Erweiterungsmethode
1
2
3
p u b l i c s t a t i c IEnumerable<TSource> Where<TSource >(
t h i s IEnumerable<TSource> s o u r c e ,
Func<TSource , Boolean> p r e d i c a t e ) {
4
5
f o r e a c h ( TSource e l e m e n t i n s o u r c e ) {
i f ( p r e d i c a t e ( element ) ) {
y i e l d return element ;
} } }
6
7
8
Methoden die yield return nutzen, sind immer Co-Routinen. Dieses Schlüsselwort kann nur von Methoden verwendet werden, die als Rückgabetyp IEnumerableoder IEnumerator definiert haben. Doch wenn man sich die foreach-Schleife anschaut, werden in Wirklichkeit einzelne Elemente zurückgegeben, die von der Sequenz source stammen. Gelangt der Kontrollfluss beim ersten Mal auf yield return, wird die Kontrolle an die aufrufende Methode zurückgegeben. Bei weiteren
Aufrufen wird der Kontrollfluss beim letzten yield return fortgesetzt. Dies so
lange bis der aufrufende Iterator jedes Element source durchiteriert hat.
5
LINQ to SQL
Gegeben sei eine Microsoft-SQL-Datenbank, für welche eine Kunden-, Mitarbeiterund Auftragstabelle angelegt ist. Auf diese Datenbank wird eine SQL-Abfrage2
gestartet:
Listing 12: SQL-Anfrage die zur Datenbank gesendet werden soll
1
2
3
4
5
6
7
8
SELECT DISTINCT c . [ CustomerID ] , c . [ CompanyName ]
FROM [ Northwind ] . [ dbo ] . [ Customers ] c
JOIN [ Northwind ] . [ dbo ] . [ Orders ] o on
c . CustomerID = o . CustomerID
JOIN [ Northwind ] . [ dbo ] . [ Employees ] e on
o . EmployeeID = e . EmployeeID
WHERE e . EmployeeID = 1
ORDER BY c . [ CustomerID ]
Diese Anfrage lässt sich wie gewohnt mit einer einfachen LINQ-Abfrage lösen. Das
wesentliche Problem hierbei ist, dass eine gewisse Instanz benötigt wird, welche die
LINQ-Abfrage in eine SQL-Abfrage umwandelt. Die DataContext-Klasse in System.Data.Linq erfüllt diese Aufgabe. Sie verwaltet auch nebenbei die Verbindung
zur Datenbank. Bevor aber mit bestimmten relationalen Daten gearbeitet werden
kann, muss ein Mapping vorgenommen werden.
5.1
Objektrelationales Mapping
Ganz gleich ob wir bei einem Datenbankzugriff Daten abfragen oder sie verändern,
es ist nötig die betreffenden relationalen Daten in der Datenbank auf Objekte zuzu2 Das Beispiel wurde aus Simon Bastians Arbeit „ADO.NET“ im gleichen Proseminar „.NETProgrammierung“ übernommen.
ordnen, was in der Software-Entwicklung Objektrelationales Mapping genannt wird.
Ohne diese Zuordnung wäre eine Verwendung der LINQ-Syntax nicht möglich, da
unklar ist, welches Objekt für welche Datenbanktabelle steht.
In LINQ gibt es drei wesentliche Mapping-Möglichkeiten. Zum einen kann man
Zuordnungen in den von C# verfügbaren Attributen manuell kennzeichnen. Klassen, die auf Datenbank-Tabellen oder Eigenschaften, die auf Datenbank-Attribute
verweisen, werden jeweils mit einem Attribut gekennzeichnet. Mithilfe von Tools
lässt sich dies automatisiert lösen: Mit dem in Visual Studio integrierten „LINQ to
SQL“-Designer, werden Attribute automatisch anhand eines Modells zugeordnet,
welches man visuell über eine GUI entwerfen kann. Sollen Mappinginformationen
zur Laufzeit veränderbar sein, bietet es sich an XML-Mapping-Dateien zu nutzen,
für die es ein Mapping XML-Schema gibt [Mic10c].
Für eine schnelle grundlegende Lösung verwirklichen wir das Mapping mit Attributen. Wir ordnen der relationalen Tabelle Customers eine Klasse zu und weisen
daraufhin den einzelnen Datenbank-Attributen die korrespondierenden Eigenschaften der Klasse zu:
Listing 13: Mapping für das Ausgangsbeispiel
1
2
3
4
5
6
7
8
9
10
[ Table ]
p u b l i c c l a s s Customer {
[ Column ( IsPrimaryKey = t r u e ) ]
// Guid repräsentiert einen eindeutigen Schlüssel
p u b l i c Guid CustomerID { g e t ; s e t ; }
[ Column ]
p u b l i c S t r i n g FirstName { g e t ; s e t ; }
[ Column ]
p u b l i c S t r i n g CompanyName { g e t ; s e t ; }
... }
Dies geschieht entsprechend auch für die Tabellen Employees und Orders.
5.2
Die Datenanfrage in LINQ
Bevor es zur LINQ-Abfrage geht, wird sich dem peripheren Code gewidmet:
Listing 14: Die Datenanfrage in LINQ
1
2
3
4
5
DataContext dataContext = new DataContext ( c o n n e c t i o n S t r i n g ) ;
Table<Customer> c u s t o m e r s =
dataContext . GetTable<Customer > ( ) ;
Table<Employee> employees =
SdataContext . GetTable<Employee > ( ) ;
6
Table<Order> o r d e r s = dataContext . GetTable<Order > ( ) ;
7
8
var query = . . . //kommt noch
9
10
11
12
f o r e a c h ( var row i n query ) {
C o n s o l e . WriteLine ( row . CustomerID + " ␣ " +
row . CompanyName ) ;
Am Anfang wird ein DataContext instanziiert, der mithilfe eines ConnectionStrings eine Verbindung zur Datenbank aufbaut. Daraufhin werden drei TableKlassen istanziiert. Zum Verständnis ist aber nur relevant, dass die Table-Objekte
nicht bei der Definition sofort mit den Daten der relationalen Tabellen gefüllt
werden. Sie werden erst nach den LINQ-Abfragen befüllt, und zwar genau dann,
wenn auf die Daten zugegriffen werden muss. Im Beispiel also bei der WriteLineMethode. Dieses Verhalten stimmt mit dem Prinzip der Verzögerten Ausführung
überein.
Die LINQ-Abfrage ist nun wie folgt aufgebaut:
Listing 15: Die in SQl zu übersetzende LINQ-Abfrage
1
2
3
4
5
6
var query = ( from c i n c u s t o m e r s
j o i n o i n o r d e r s on c . CustomerID e q u a l s o . CustomerID
j o i n e i n employees on o . EmployeeID e q u a l s e . EmployeeID
where e . EmployeeID == 1
o r d e r b y c . CustomerID
s e l e c t new { c . CustomerID , c . CompanyName } ) . D i s t i n c t ( ) ;
Mit der Anfrage werden nun alle Kunden ermittelt, die Aufträge beim Mitarbeiter mit der MitarbeiterID 1 haben. Anschließend werden sie nach der KundenID
aufsteigend geordnet. Mehrfache Kundennennungen werden mit dem Abfrageoperator Distinct ausgeblendet. Da zur Ausgabe nur die KundenID und der Firmenname des Kunden wichtig ist, wird ein anonymer Typ erstellt, der diese Werte
festhalten soll. Wenn nun der Kontrollfluss bei der ersten Ausführung der WriteLine-Methode angekommen ist, wird tatsächlich erst dann eine in SQL übersetzte
Anfrage losgeschickt. Schaut man sich die Protokollierung der Kommunikation zum
Datenbankserver an, ist folgende Anfrage vorzufinden:
Listing 16: SQL-Anfrage die zum Server geschickt wird
1
2
3
4
5
6
7
SELECT DISTINCT [ t 0 ] . [ CustomerID ] , [ t 0 ] . [ CompanyName ]
FROM [ dbo ] . [ Customers ] AS [ t 0 ]
INNER JOIN [ dbo ] . [ Orders ] AS [ t 1 ] ON
[ t 0 ] . [ CustomerID ] = [ t 1 ] . [ CustomerID ]
INNER JOIN [ dbo ] . [ Employees ] AS [ t 2 ] ON
[ t 1 ] . [ EmployeeID ] = [ t 2 ] . [ EmployeeID ]
WHERE [ t 2 ] . [ EmployeeID ] = 1
Bemerkenswert ist, dass der anonyme Typ und der Abfrageoperator Distinct in
der Übersetzung der LINQ-Abfrage berücksichtigt werden.
5.3
Aktualisierung von relationalen Daten
Wenn nun die Datenbestände der drei verwendeten relationalen Tabellen verändert werden sollen, geschieht dies Dank des Mappings recht intuitiv. Angenommen
es liegt im Interesse die Aufträge mit den IDs von 1 bis 10 zur AuftragID 123
zusammenzuführen, kann diese Veränderung in der Datenbank wie folgt realisiert
werden:
Listing 17: Aktualisierung von Order
1
2
3
var query = from e i n employees
where e . OrderID ?= 10
select e ;
4
5
6
f o r e a c h ( var row i n query ) {
row . OrderID = 1 2 3 ;
}
7
8
dataContext . SubmitChanges ( ) ;
In diesem Fall werden allerdings zwei Anfragen zum Server gesendet. Eine zum Ermitteln, welche Datensätze unseren Kriterien entsprechen und eine andere um die
eigentliche Aktualisierung auszuführen, die schließlich mit dataContext.SubmitChanges
übermittelt wird.
6
Schluss
Wie herausgearbeitet bereichert der Einsatz von LINQ neben der Produktivität
auch die Qualität von Code. Falsche Nutzung von LINQ kann die Codequalität
mangels Performance allerdings auch schmälern. Es müssen Erfahrungen gesammelt werden, wann statt LINQ-Abfragen normale Methodenaufrufe angebracht
sind, da die Ausführungszeit bei LINQ bedeutend höher ausfallen kann. Auf der
anderen Seite hat man mit LINQ große Produktivitätssprünge: Man kann ohne
großen Anpassungscode mit mehreren Datenquellen gleichzeitig arbeiten und gelangt mittels Features wie den typisierten Zugriff, die Debugging-Unterstützung
und den Intellisense-Support mit weniger Aufwand zum funktionierenden Code als
mit den zuvor existierenden APIs für Datenquellen. Abschließend lässt sich also
sagen, dass für zeitkritische Codes ein vernünftiges Maß zwischen Performance auf
der einen Seite und Codetransparenz und Kürze auf der anderen Seite gefunden
werden muss.
Literatur
[BB00]
H. Barendregt und E. Barendsen.
Introduction to Lambda Calculus,
2000. ftp://ftp.cs.kun.nl/pub/CompMath.Found/lambda.pdf. Abgerufen
am 23.12.2010.
[Hej06]
Anders Hejlsberg. LINQ and Functional Programming. Video-Interview, 2006.
http://windowsclient.net/learn/video.aspx?v=6903. Zeitabschnitt 2:44 5:50 – abgerufen am 23.12.2010.
[MEW08] F. Marguerie, S. Eichert und J. Wooley. LINQ Im Einsatz. Hanser Verlag,
2008.
[Mic10a]
Microsoft. MSDN - Einführung in LINQ-Abfragen (C#) -> Abfrageausführung, 2010. http://msdn.microsoft.com/de-de/library/bb397906.aspx.
Abgerufen am 23.12.2010.
[Mic10b] Microsoft. MSDN - Implizit typisierte lokale Variablen, 2010. http://msdn.
microsoft.com/de-de/library/bb384061.aspx. Abgerufen am 23.12.2010.
[Mic10c]
Microsoft. MSDN - Referenz zur externen Zuordnung (LINQ to SQL), 2010.
http://msdn.microsoft.com/de-de/library/bb386907.aspx. Abgerufen am
23.12.2010.
[PR08]
P. Pialorsi und M. Russo. Datenbank-Programmierung mit Microsoft LINQ.
Microsoft Press, 2008.

Documentos relacionados