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.