Language Integrated Query (LINQ) Einbettung einer deklarativen Abfragesprache in objektorientierte Programmiersprachen Christian Piechnick TU Dresden [email protected] Abstract Der Umgang mit unterschiedlichen Datenquellen, enge Terminpläne und die Thematik der Wartbarkeit und Anpassbarkeit von Softwareprodukten gewinnt immer mehr an Bedeutung. Dabei stellt die Abfrage von Daten aus heterogenen Datenquellen bei der Entwicklung von Geschäftsanwendungen einen zentralen Punkt dar. Die Lücke zwischen mengenorientierten, deklarativen Abfragesprachen wie SQL und imperativen Programmiertechniken wird in herkömmlichen Technologien durch datenquellabhängige Einzellösungen überwunden, die je nach Technologie einen hohen Einarbeitungsaufwand erfordern. Ziel dieses Papers ist es, die von Microsoft gewählten Ansätze vorzustellen, eine deklarative Abfragesprache in die objektorientierten, .NET basierten Sprachen zu integrieren. Weiterhin wird erläutert, durch welche Ansätze LINQ die Abfrage verschiedener Datenquellen in gleicher Syntax ermöglicht. Anhand bekannter und bewährter Technologien werden die wichtigsten konzeptionellen Unterschiede von LINQ erarbeitet. Darauf basierend wird dargestellt, welchen Einfluss diese Ansätze auf die Eigenschaften der integrierten Abfragesprache haben. 1. Motivation Bei der Entwicklung von Geschäftsanwendungen, vor allem im Bereich des Data-Warehousings, spielt das Abfragen, Manipulieren, Darstellen und Speichern von Daten eine entscheidende Rolle. Mit der stetig wachsenden Heterogenität der Datenlandschaft durch die Einführung neuer Technologien wächst parallel dazu die Heterogenität der Ansätze und spezifischen Umsetzungen, diese in Anwendungen zu integrieren. Da sich die Ansätze zur Kommunikation mit den einzelnen Datenquellen zum Teil signifikant unterscheiden, sind Softwareentwickler gezwungen datenquellabhängige Abfragesprachen und Technologien zu erlernen. Die einzelnen Lösungen werden den hohen Anforderungen der Softwareentwicklung, im Speziellen der Programmierung, der IDE-Unterstüzung, geringem Entwicklungsaufwand, einer überschaubarer Komplexität und Permission to make digital or hard copies of all or part of this work for personal or classroom use is granted without fee provided that copies are not made or distributed for profit or commercial advantage and that copies bear this notice and the full citation on the first page. To copy otherwise, or republish, to post on servers or to redistribute to lists, requires prior specific permission and/or a fee. SIGPLAN’05 June 12–15, 2005, Location, State, Country. Copyright © 2004 ACM 1-59593-XXX-X/0X/000X…$5.00. Wartbarkeit nur bedingt gerecht. Ein entscheidender Punkt bei der Integration von Abfragen in imperative Programmiersprachen ist es, die Diskrepanz zwischen den Ansätzen der deklarativen, mengenorientierten Abfragesprachen und den Ansätzen der imperativen Programmiersprachen zu überwinden. Mit LINQ wurde eine streng getypte, deklarative Abfragesprache in objektorientierte Programmiersprachen integriert. Mit LINQ ist es möglich, verschiedenste Datenquellen typsicher abzufragen. 2. Herkömmliche Ansätze Das im Jahre 1976, von Peter P. Chen, erstmals veröffentlichte Entity-Relationship-Modell [1], hat sich im Laufe der Zeit als Grundlage für die Modellierung von Datenbanken etabliert. Da die Nutzung von relationalen Datenbanken weit verbreitet ist, wurden im Laufe der Zeit zahlreiche Ansätze entwickelt, diese in imperative Programmiersprachen zu integrieren. Im folgenden sollen am Beispiel der relationalen Datenbanken die wichtigsten Ansätze, der Abfrage von Daten aus imperativen Programmiersprachen kurz vorgestellt werden. 2.1 Embedded SQL (ESQL) Bei Embedded SQL handelt es sich um eine Spracherweiterung von SQL, die erstmals 1992 im SQL92-Standard definiert wurde [2]. Im Standard ist die Integration von SQL in 7 Programmiersprachen spezifiziert: Ada, C, COBOL, Fortran, MUMPS, Pascal und PL/I. Mit ESQL ist es möglich SQL Befehle typsicher in der Hostsprache zu implementieren, die dann von einen Datenbankmanagementsystem-abhängigem Präprozessor in Sprachkonstrukte der Hostsprache übersetzt werden. Durch eine Integration mit festen syntaktischen Strukturen ist eine Syntaxprüfung durch den Compiler möglich. Allerdings unterscheiden sich die Umsetzungen von ESQL unter den Programmiersprachen in Syntax und Funktionalität [3], so dass es keine Sprachunabhängige generische Lösung darstellt. Die Übersetzung der ESQL in Sprachkonstrukte der Hostsprache durch einen Präprozessor verhindert das dynamische generieren von Abfragen zur Laufzeit und beim Wechsel bzw. Versionsänderungen der Datenbank ist ein erneutes kompilieren der Anwendung notwendig. Des Weiteren ist zum Teil keine vollständige Abbildung in die Hostsprache möglich, da beispielsweise Java kein Sprachkonstrukt enthält einer Variable vom Typ int einen Nullwert zuzuordnen. 2.2 Call Level Interface (CLI) Das Call Level Interface (CLI) ist eine Programmierschnittstelle für den Zugang zu Datenbanken, das nach der Standardisierung 1993 von der Open Group weiterentwickelt wurde [4]. Das Client/Server basierte CLI ermöglicht das dynamische Generieren von SQL Anweisungen zur Kommunikation mit der Datenbank, ohne Kenntnis der konkreten Datenbank haben zu müssen. Auf Basis des CLI wurde mit der Open Database Connectivity (ODBC) eine konkrete Middlewarelösung entwickelt. Dabei wird eine SQL-Abfrage als String an eine Library übergeben, die dann die konkrete Kommunikation mit der Datenbank übernimmt. Der Rückgabewert dieses Aufrufs ist dann entweder ein skalarer Wert oder eine Liste von Tupeln, wobei die Zeilen (Rows) und Spalten (Columns) die resultierende Tabelle der Abfrage repräsentiert. Im folgenden wird ein Java Database Connectivity (JDBC) Beispiel gegeben: String sStr = "SELECT * FROM Cst WHERE id=12"; Statement stmt = conn.prepareStatement( sStr ); ResultSet rslt = stmt.executeQuery(); while (rslt.getNext()) { rslt.getString(„EMail“); } Wie im Beispiel ersichtlicht, wird die SQL Abfrage als Zeichenkette übergeben, was eine statische Syntaxprüfung nahezu unmöglich macht. Das resultierende ResultSet zwingt den Entwickler die konkrete Struktur der Datenbank zu kennen, um die Werte auszulesen zu können. Das Abfragen des Ergebnisses ist nur bedingt typsicher. Es werden Methoden zur Verfügung gestellt, um Typsicherheit zu gewährleisten, beispielsweise getString(), jedoch ist zur Laufzeit nicht gewährleistet, dass es sich in der angegeben Spalte auch wirklich um einen String handelt. Weiterhin neigt Quellcode zur Abfrage von Datenbanken mittels CLI dazu, sehr umfangreich zu werden [5]. 2.3 Object-relational mapping (ORM) ORM ist ein weiterer wichtiger Ansatz, die Diskrepanz zwischen dem objektorientierten und dem relationalen Paradigma zu überbrücken. Dabei wird versucht die Komplexität der Datenbankkommunikation vor dem Entwickler zu verstecken, um transparent Daten aus der Datenbank zu laden, diese zu speichern, zu erneuern oder zu löschen. Trotz der Vorteile die diese Art der Transparenz mit sich bringt, muss sich der Entwickler dennoch immer im klaren sein, dass die Daten aus einer externen Quelle stammen. Bei der Abfrage großer Datenmengen muss der Entwickler den Abbildungsprozess individuell konfigurieren, da diese Technologien sonst sehr ineffizient arbeiten können. Bei den meisten ORM-Technologien, wie beispielsweise der Java Persistence API (JPA), werden Möglichkeiten angeboten das Verhalten der O/R Abbildung mitttels Konfigurationen anzupassen. Um zur Laufzeit optimale Abfragen zu Konstruieren bietet die JPA eine dedizierte Abfragesprache für Objekte der JPA, die Java Persistence Query Language (JPQL), in der Abfragen in einer SQL-ähnlichen Syntax spezifiziert werden können. Jedoch werden Abfragen als Zeichenkette übergeben und die Ergebnisse der Abfragen sind ungetypt. 2.4 Weitere Datenquellen Neben den relationalen Datenbanken haben sich in den letzten Jahren auch objektorientierte Datenbanken etabliert. Die Object Database Management Group (ODMG) hat nicht nur die Standardisierung dieser Datenbanken, sondern auch die Standardisierung der Integration von Abfragen in objektorientierte Programmiersprachen, vorangetrieben. Dabei werden Objekte über eine standardisierte Schnittstelle (Object Definition Language) definiert und mittels einer integrierten Abfragesprache (Object Query Language) abgefragt. Dieser Ansatz bringt viele Vorteile mit sich, beschränkt sich jedoch auch objektorientierte Datenbanken und ist somit von deren Verbreitung abhängig. Die vorgestellten Ansätze beziehen sich lediglich auf Datenbanken. Die Abfrage von anderen Datenquellen erfordet die Benutzung weiterer Technologien und Abfragesprachen. Um beispielsweise Daten abzufragen, die im XMLFormat vorliegen, wird häufig XPath[6] eingesetzt, dessen Syntax sich stark von SQL unterscheidet. Die Kommunikation mit einem Webservice erfolgt mittels SOAPNachrichten und unterscheidet sich ebenfalls stark von der Abfrage anderer Datenquellen. Mit LINQ wird die Komplexität für den Entwickler deutlich gesenkt, da nur noch eine Abfragesyntax beherrscht werden muss, um alle genannten Datenquellen abzufragen. 3. Technologische Grundlagen Bei LINQ handelt es sich um Spracherweiterungen der .NET Frameworksprachen und funktionale Erweiterungen des .NET Frameworks. Um die Konzepte und konkreten Lösungsdetails der Integration von LINQ besser nachvollziehen zu können sollen an dieser Stelle die Grundlagen des .NET Frameworks erläutert werden. Weiterhin werden die wichtigsten Spracherweiterungen der .NET Frameworksprachen vorgestellt, die für LINQ notwendig sind. 3.1 Das .NET Framework Das im Jahre 2002 erstmals veröffentlichte .NET Framework ist eine Software-Plattform für die sprach- und platt- Abbildung 1 .NET Framework Maschinencodegenerierung formunabhängige Softwareentwicklung und –ausführung. LINQ im Rahmen der Frameworkversion 3.5 im Jahr 2007, eingeführt. In der Common Language Infrastructure (CLI vgl. CLI – Call Level Interface) des ECMA Standards ECMA-335 [8], wird das Format von ausführbarem Code und die Laufzeitumgebung, in der dieser Code ausgeführt wird, spezifiziert. Die .NET Softwareplattform ist eine Implementierung der CLI und ermöglicht die sprachunabhängige Softwareentwicklung (Abb. 1). Plattformunabhängigkeit wird über eine betriebssystemspezifische Laufzeitumgebung erreicht, die den Zwischencode in Maschinencode übersetzt. Neben den von Microsoft unterstützten objektorientierten Programmiersprachen C# und Visual Basic, sowie der funktionale Programmiersprache F#, existieren viele Portierungen von anderen Sprachen auf die .NET Plattform [9]. 3.2 Provider für Language Integrated Query Bei LINQ handelt es sich neben einer integrierten Abfragesyntax, um eine Menge von Frameworkfunktionen und Spracherweiterungen. LINQ erlaubt es Abfragen in .NET Frameworksprachen zu schreiben, die dann von sogenannten Providern in konkrete Abfragen gegen die entsprechende Datenquelle übersetzt werden. Folgende Provider sind dabei im .NET Framework enthalten. LINQ to Objects Abfrage von Collections LINQ to XML Abfrage von XML-Dokumenten LINQ to SQL Abfrage von MS-SQL Datenbanken LINQ to Entities Abfrage von beliebigen relationalen Datenbanken auf Basis des Entity-Frameworks LINQ to Dataset Abfrage gegen beliebige Datenbanken auf Basis des .NET Frameworksteils ADO.NET Aufgrund des Providerprinzips wurden seit der Einführung von LINQ weitere Provider entwickelt, z.B. LINQ to LDAP, -RDF, -Google, -Amazon um einige zu nennen. 3.3 Typinferenz Typinferenz erlaubt die Deklaration von Variablen ohne dessen Typ explizit anzugeben. var myString = „Hello world“; Trotz der scheinbar typunsicheren Deklaration wird der entsprechende Typ vom Compiler ermittelt und das var Schlüsselwort im Zwischencode durch den entsprechenden Typ ersetzt. Strenge Typisierung ist so gesichert und illegale Zuweisungen werden vom Compiler erkannt. 3.4 Diese Extension Method erweitert den Typ int um eine Methode add(int b), wobei der erste Parameter, dem das this Schlüsselwort voran gestellt ist, den zu erweiternden Typ angibt. 3.5 Lambda Ausdrücke Mit der .NET Frameworkversion 2.0 wurden Funktionszeiger (delegates) und anonyme Methoden eingeführt (siehe [10] für weitere Informationen). Lambda Ausdrücke sind lediglich eine kompaktere Syntax um anonyme Methoden zu deklarieren. Beginnend mit den Eingabeparametern, gefolgt von dem “Lambda Operator” (=>) wird in einem Ausdruck oder einer Reihe von Anweisungen der eigentliche Rumpf der Methode angegeben, wobei anonyme Methoden Instanzen des Typs Func<T,TResult> sind. Func<int,int,bool> myF = (x,y) => x>y; In diesem Beispiel wird eine Funktion myF deklariert, die zwei int Werte als Eingabe hat und diese Eingabewerte auf einen bool abbildet. Auf diese Weise lassen sich HigherOrder-Functions erstellen. 3.6 Ausdrucksbäume Ein Ausdrucksbaum (Expression Tree) bietet die Möglichkeit, ausführbaren Code in Daten, einen abstrakten Syntaxbaum (AST), zu transformieren. Dazu wird ein Objekt vom Typ Expression erstellt, dem ein Funktionszeiger zugewiesen wird. Dieses Objekt repräsentiert dann den abstrakten Syntaxbaum der zugewiesenen Funktion. Der abstrakte Syntaxbaum, wird benutzt um Code vor der Ausführung zur Laufzeit zu manipulieren. Expression<Func<int,bool>> exp = x => x > 0; Diese Anweisung definiert einen Ausdrucksbaum exp, der den AST für eine Methode darstellt, welche einen int Wert als Eingabe auf true abbildet, wenn der Wert größer als 0 ist. Die Klasse Expression stellt Funktionen zur Verfügung, um auf den abstrakten Syntaxbaum, zur Laufzeit, zuzugreifen und diesen zu manipulieren. Somit kann der spezifizierte Ausführungsplan optimiert und gemäß der betreffenden Datenquelle transformiert werden. Weiterhin kann ein eine Expression, durch Methode Compile(), zur Laufzeit in ausführbaren Code überführt werden. Auf diese Weise werden die LINQ-Provider in die Lage versetzt, Lambda-Ausdrücke oder imperativen Programmcode in Anweisungen der Datenquelle zu überführen. 4. Funktionsweise von LINQ Erweiterungsmethoden Häufig können in objektorientierten Programmiersprachen Klassen nur über Klassencodemanipulation oder Vererbung um Funktionalität erweitert werden. Mit Erweiterungsmethoden (Extension Methods) ist es möglich, Klassen statisch um Funktionalität zu erweitern ohne den Quellcode der Klasse zu manipulieren oder eine Unterklasse zu erstellen. Dazu wird in einer statischen Klasse eine statische Methode deklariert, deren Parameterliste das this Schlüsselwort vorangestellt ist. public static String Add(this int a, int b){ return a + b; } 4.1 Die LINQ Query Syntax LINQ Abfragen und SQL Abfragen sind syntaktisch sehr ähnlich, unterscheiden sich jedoch in einem entscheidenden Punkt. Jede LINQ Abfrage beginnt mit from element in datasource, wobei from und in Schlüsselwörter, datasource eine LINQ-fähige Datenquelle und element ein beliebiger Bezeichner für einen Repräsentanten der Datenquelle darstellen. Danach stehen verschiedene weitere kontextuelle Schlüsselwörter zur Verfügung, beispielsweise where (Restriktion), group by (Gruppierung), orderby (Sortierung), ascending und descending. Das letzte Ele- ment einer LINQ Abfrage ist die Projektion durch das Schlüsselwort select. var qry = from cst i n cb.Cst where customer.Orders.Count > 10 group cst b y cst.Country i nto group o rderby group.Country d escending select group; 4.4 Abfragetransformation Zu jedem Schlüsselwort in der deklarativen LINQ Query Syntax gibt es eine äquivalente Erweiterungsmethode, die den Typ IEnumerable erweitert. Der Compiler der entsprechenden Programmiersprache wandelt diese deklarative Abfrage in die „Method Syntax“ um. var qry = Listing 1 Beispiel: LINQ Query Syntax In Listing 1 wird eine Menge von Kunden nach der Anzahl der abgegebenen Bestellungen gefiltert, diese Kunden dann nach Herkunftsland gruppiert und diese Gruppen absteigend nach dem Namen des Landes sortiert. 4.2 LINQ Erweiterungsmethoden-Operatoren Neben den kontextuellen Schlüsselwörtern gibt es eine Vielzahl von weiteren LINQ-Operatoren, die als Erweiterungsmethoden implementiert sind und über Higher-OrderFunctions die Variation des Verhaltens erlauben. Im Folgenden wird am Beispiel der Aggregationsfunktion Sum(Func<T,int>) gezeigt, wie diese Operatoren eingesetzt werden. Listing 2 Beispiel LINQ Method Syntax In Listing 2 ist das Ergebnis der Transformation von Query Syntax (aus Listing 1), in Method Syntax angegeben. Sollte es sich bei der Datenquelle db.Cst um eine delegierte Datenquelle handeln, dann würde der durch die Method Syntax entstehende AST durch den Provider der Datenquelle transformiert und in ausführbaren Code übersetzt werden. Dieser würde dann die Abfrage gegen die Datenquelle ausführen und das Ergebnis in das objektorientierte Modell transformiert. 5. from cst i n database.Customers select cst.Orders.Sum(order => order.PayedSum) Die Erweiterunsmethode Sum(Func<T,int>) erweitert das Interface IEnumerable<T>, welches von allen Klassen implementiert wird, die mehr als einen Wert halten. Der übergebene Parameter ist eine Funktion, die einen Wert vom generischen Typ T (generischer Typ von IEnumerable) auf einen int Wert abbildet. Im angegebenen Fall wird die Methode Sum auf einem IEnumerable<Order> aufgerufen und benötigt deshalb als Eingabeparameter eine Funktion, die eine Order auf einen int Wert abbildet. Eine Übersicht über alle LINQ Operatoren wird unter [11] gegeben. 4.3 Abfrageausführung und –ergebnisberechnung Grundsätzlich werden LINQ-fähige Datenquellen in 2 Klassen unterteilt: In-memory (Daten befinden sich im Hauptspeicher) und delegierte Datenquellen (Daten befinden sich nicht im Hauptspeicher). Das Ergebnis einer Abfragedeklaration ist entweder ein Objekt vom Typ IEnumerable oder IQueryable, wobei das Interface IQueryable das Interface IEnumerable erweitert. Zum Zeitpunkt der Deklaration wird das Ergebnis der Abfrage nicht berechnet, sondern es wird lediglich die Art und Weise wie dieses ermittelt wird zurückgeliefert. Handelt es sich um eine in-memory Datenquelle, dann liefert die Deklaration ein IEnumerable, welches einen Funktionszeiger auf die Ausführung der Abfrage repräsentiert. Ist die Datenquelle eine Delegierte, dann wird ein IQueryable zurückgeliefert, das den AST der Abfrage spezifiziert. Erst zum Zeitpunkt der Iteration über der Variable, der die Abfrage zugewiesen wurde, wird die eigentliche Abfrage durchgeführt. Dabei übernehmen die LINQ Provider die potentielle Optimierung. Bei delegierten Datenquellen wird die Transformation der Abfrage in die erforderliche Form der Datenquelle, beispielsweise eine SQL Abfrage, durchgeführt. db.Cst.Where( c=>c.Orders.Count > 10) .GroupBy( c => c.Country ) .OrderByDescending( g => g.Country ) .Select( g => g ); Zusammenfassung Es wurde eine kurze Einführung in die Sprachkonzepte und technische Umsetzung von LINQ gegeben und im Vergleich mit herkömmlichen Ansätzen klar herausgearbeitet, dass LINQ das Konzept der integrierten Abfragesprachen weiterführt und durch die Datenquellunabhängigkeit sowie durch die deklarative Syntax, das Arbeiten mit Daten bei der Softwareentwicklung deutlich verbessern kann. References [1] Peter P. Chen: The Entity-Relationship Model – Toward a Unified View of Data, ACM Digital Library, 1976 [2] ISO/IEC 9075:1992: Information Technology – Database Language SQL, International Standardization Organization, 1992 [3] Dr. Karsten Tolle: Embedded SQL, Universität Frankfurt Main, 2008 www.dbis.cs.uni-frankfurt.de/downloads/tolle/ DBWEB2008/Folien_24.10.2008.pdf [4] X/Open: Technical Standard – Data Management: SQL Call Level Interface (CLI), X/Open Company Limited, 1995 [5] Ted Neward: Comparing LINQ and Its Contemporaries, 2005 http://msdn.microsoft.com/en-us/library/aa479863.aspx [6] W3C, XPath Language (XPath), W3C Recommondation, 2007 http://www.w3.org/TR/xpath20/ [7] W3C, Web Service Activity, http://www.w3.org/2002/ws/ [8] ECMA-335 4th: Common Language Infrastructure (CLI), 2006 http://www.ecma-international.org/publications/standards/Ecma335.htm [9] Brian Ritchie: dotnetpowered Language List http://www.dotnetpowered.com/languages.aspx [10] Walter Doberenz, Thomas Gewinnus: Visual C# 2008, Hanser 2008 [11] Hooked in LINQ: Standard Query Operators http://www.hookedonlinq.com/StandardQueryOperators.ashx [12] Fabrice Marquerie, Steve Eichert, Jim Wooley: LINQ in Action Manning Pubn, 2008