Language Integrated Query (LINQ)

Werbung
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
Herunterladen