Näher an den Daten mit LINQ to SQL


Artikel erschienen in Swiss IT Magazine 2008/11

     

Mit LINQ to SQL begibt sich Microsoft wissentlich in ein Terrain, das einem Haifischbecken nicht unähnlich ist. Dutzende etablierter O/R-Mapper-Anbieter, von der Open-Source-Lösung mit Kultcharakter (NHibernate), über kommerzielle Lösungen mit kostenloser Express Edition für den Einstieg (OpenAccess von Vanatec) über eine kaum überschaubare Phalanx von Individuallösungen, hinter denen oft nur ein einsamer Entwickler steht, tummeln sich darin seit Jahren.



Die Antwort auf die Frage, warum Microsoft so spät dazu- kommt, hat etwas mit Fehlern bei der strategischen Ausrichtung zu tun, die in der Vergangenheit gemacht wurden (die Stichworte ObjectSpaces und WinFS sollen an dieser Stelle genügen). Doch das ist Vergangenheit, mit LINQ to SQL steht eine solide Lösung als Teil von Visual Studio 2008 zur Verfügung, die darauf wartet, eingesetzt zu werden.


Aus Tabellen werden Objekte

Lange wurde nur darüber geredet, seit Ende 2007 ist es mit dem .NET Framework 3.5 endlich offiziell: Das Language Integrated Query Framework, kurz LINQ, mit dem sich SQL-ähnliche Abfragen direkt in den Code eines C#- oder Visual Basic-Programms einbauen lassen. So gibt etwa die Abfrage im Codebeispiel 1 (siehe Kasten auf Seite 45) alle Kunden mit offenen Rechnungen im Postleitzahlgebiet 8 als Objekte (auf der Grundlage eines anonymen Typs) zurück.


Auch wenn diese Abfrage an SQL erinnert, mit der universellen Datenbanksprache hat sie nichts zu tun. Die Microsoft-Entwickler kamen bei der Umsetzung von LINQ lediglich auf die (naheliegende) Idee, den in C# 3.0 und VB 9.0 eingebauten Abfrageerweiterungen vertraute Namen wie From, Join oder Select zu geben.



In der Praxis sollen die Daten, die in der LINQ-Abfrage so elegant verarbeitet werden, aus einer Datenbank kommen. Damit die Collections Kunden und Rechnungen tatsächlich Datensätze enthalten, müsste normalerweise der Entwickler den üblichen ADO.NET-Infrastrukturcode schreiben, über einen Datenadapter die Datensätze lesen und am Ende wieder in die Datenbank zurückschreiben. Nicht nur, dass dies eine lästige und fehleranfällige Routineaufgabe ist, vor allem das «Mappen» der Felder auf die Eigenschaften der einzelnen Klassen muss direkt im Quellcode vorgenommen werden, was keine flexible Lösung ist.


Vorhang auf für LINQ to SQL

Da es nicht nur naheliegend, sondern auch relativ einfach ist, das Mappen von Datenbanktabellen auf Klassen zu automatisieren, gibt es seit vielen Jahren eine Kategorie von Werkzeugen, die als objektrelationale Mapper, kurz O/R-Mapper (wenngleich dies keine «genormte» Schreibweise ist), bezeichnet werden. Sie erlauben es nicht nur, die relationalen Datenbanktabellen auf Klassen umzusetzen, sondern sie lösen auch das «Persistenzproblem», in dem sie das direkte Abspeichern von Objekten in einer Datenbank erlauben. Microsoft hat nicht eine, sondern gleich zwei O/R-Lösungen im Angebot: LINQ to SQL und das Entity Framework (mehr dazu am Ende des Artikels). Bei LINQ to SQL, das ein Bestandteil von .NET 3.5 und Visual Studio 2008 ist, stehen Einfachheit und der typische RAD-Ansatz (Rapid Application Development) im Vordergrund. Es bietet trotzdem unter anderem Transaktionen, gespeicherte Prozeduren und «Optimistic Concurrency».



LINQ to SQL basiert (natürlich) auf ADO.NET 2.0, ein DataSet ist allerdings nicht im Spiel. Im Mittelpunkt steht die neue DataContext-Klasse, welche die zuvor ausgewählten Tabellen als typisierte Table-Objekte (alle im Namespace System.Data.Linq) zur Verfügung stellt. Das DataContext-Objekt ist die Objekt-orientierte Repräsentation der Datenbank mit den beim Entwurf ausgewählten Tabellen und Feldern. Das «Mapping» geschieht mit Hilfe eines einfachen, aber komfortablen Designers, der über die LINQ-to-SQL-Vorlage zu einem Projekt hinzugefügt wird, und auf dessen Innenfläche die ausgewählten Tabellen wie eventuell auch gespeicherte Prozeduren abgelegt werden. Eventuell vorhandene Beziehungen werden dabei übernommen. Das Ergebnis ist eine XML-Datei (Erweiterung .dbml), welche die ausgewählten Tabellen und ihre Felder beschreibt (alternativ kann ein solches Konfigurationsmodell beim Instanzieren der DataContext-Klasse direkt angegeben werden). Im Hintergrund wirkt das kleine Tool Sqlmetal.exe, das auch direkt über die Kommandozeile gesteuert werden kann. Das Mapping wird durch Attribute beschrieben, die im Namespace System.Data.Linq.Mapping definiert sind. Die Definition einer Klasse, die eine Tabelle Kunde «mappt», sieht wie folgt aus:




Table(Name:=»dbo.Kunden»)> _
Partial Public Class Kunden

Neben partiellen Klassen nutzt der Designer auch die mit C# 3.0 und VB 9.0 eingeführten partiellen Methoden. Damit lässt sich beispielsweise eine Validierung sehr einfach in die bereits vorbereiteten Methodenrahmen einfügen.
Mit dem Hinzufügen einer LINQ-to-SQL-Vorlage steht eine DataContext-Klasse zur Verfügung, die alle Tabellen als Eigenschaften anbietet. Jetzt kommt LINQ ins Spiel, mit dem sich die Collections auf die exakt gleiche Weise abfragen lassen wie eingangs gezeigt. Wie es für LINQ üblich ist, wird die Abfrage erst ausgeführt, wenn die Collection durchlaufen wird (durch Setzen von DeferredLoadingEnabled auf False kann dies geändert werden). Das Aktualisieren von Objekten ist genauso möglich, wie das Hinzufügen oder Löschen:



Dim NeuKunde As New Kunden()
NeuKunde.Name = «Dr. Kurt Felix»
NeuKunde.Stadt = «Neuchâtel»
Kr.Kunden.InsertOnSubmit(NeuKunde)



Am Ende sorgt ein Aufruf von SubmitChanges() dafür, dass alle Änderungen zurück in die Datenbank geschrieben werden. Entweder per SQL oder über Stored Procedures, in beiden Fällen aber über reguläre SQLCommand-Objekte. LINQ ist bei diesen Dingen nicht mehr involviert. Auch eine Datenbindung ist möglich, denn ein LINQ-Table lässt sich beispielsweise direkt an die DataSource-Eigenschaft eines DataGridView binden.



Eventuelle Änderungen, die im DataGridView gemacht werden, werden per SubmitChanges in der Datenbank abgelegt. LINQ to SQL kann auch für das Persistieren eines Datenbank-unabhängigen Objektmodells genutzt werden, in dem im Designer ein «Entitätsmodell» von Grund auf neu angelegt und beim Instanzieren der DataContext-Klasse die Verbindungszeichenfolge zur SQL-Server-Datenbank angegeben wird. Die Namen der Tabellen und Felder müssen nicht mit den Namen der Klassen und ihren Eigenschaften übereinstimmen, denn genau dafür gibt es das Mapping über die Source-Eigenschaft. Auch hier genügt ein Aufruf von SubmitChanges(), um die Objektzustände in die Datenbank zu schreiben.


SQL-Code visualisieren

Der grosse Vorteil von LINQ to SQL ist die Leichtigkeit, mit der sich auch komplexere Abfragen schreiben lassen. Die Abfrage im Codebeispiel 3 gibt zusätzlich die Anzahl der offenen Rechnungen aus, in dem diese über eine weitere Abfrage in den anonymen Typ eingebaut wird.



Nicht nur die Syntax ist vertraut, Visual Studio bietet beim Zusammenstellen der Abfrage Eingabehilfen an, was gerade für das Kennenlernen eine grosse Hilfe ist. Spätestens zu diesem Zeitpunkt dürften sich Performance-bewusste Datenbankentwickler dafür interessieren, wie das SQL aussieht, das durch eine solche Abfrage an die Datenbank geschickt wird. Die DataContext-Klasse besitzt dazu eine Log-Eigenschaft, der einfach ein «Standard-out-Gerät» zugewiesen wird. Eine andere Möglichkeit, den SQL-Code sichtbar zu machen, besteht in der GetCommand-Methode des DataContext-Objekts:




?Kr.GetCommand(KundenMitOffeneRechnungen).CommandText



Produziert wird der SQL-Code erst, wenn die Abfrage ausgeführt wird, etwa beim Aufruf von SubmitChanges() oder beim Durchlaufen der Collection. Deutlich komfortabler ist ein kleiner Visualizer, der auch für Visual Studio 2008 separat heruntergeladen werden muss (via http://weblogs.asp.net/scottgu). Wurde die DLL in das Visual-Studio-Verzeichnis %Programfiles%\Microsoft Visual Studio 9.0\Common7\Packages\Debugger\Visualizers kopiert, wird beim Betrachten der DataContext-Variablen während einer Pro­gramm­unterbrechung eine kleine Lupe angezeigt, über die der SQL-Code in einem eigenen Fenster betrachtet werden kann.


Fortgeschrittenere Features

LINQ to SQL vereinfacht das Erstellen eines Daten-Layers drastisch, hat aber auch ein paar fortgeschrittenere Features zu bieten. Für das Auflösen von Schreibkonflikten verlässt es sich auf das bei ADO.NET vertraute «Optimistic Concurrency»-Modell, das ohne Sperren auskommt und bei dem nicht durchführbare Änderungen in Gestalt der ChangeConflicts-Auflistung der DataContext-Klasse «gemeldet» werden.

Über sie lässt sich zum Beispiel nach dem Aufruf von SubmitChanges() feststellen, welche Datensätze nicht aktualisiert wurden (dieses Verhalten kann über das UpdateCheck-Feld des Column-Attributs gesteuert werden – der Wert «Never» schaltet «Optimistic Concurrency» ab). Eine bessere Performance versprechen kompilierte Queries. Bei ihnen wird der «Expression Tree», der einer LINQ-Abfrage stets zugrunde liegt, nicht jedes Mal neu aufgebaut. Der im Codebeispiel 3 formulierte Befehl bereitet eine vorkompilierte Abfrage in Visual Basic vor. Diese gibt anschliessend alle Kunden aus einer bestimmten Stadt zurück:



Dim KundenChur = KundenNachStadt(Kr, «Chur»)




Spätestens an diesem Punkt wird deutlich, wie eng LINQ to SQL mit den Spracherweiterungen von Visual Basic 9.0 und C# 3.0 verzahnt ist. Wer LINQ to SQL effektiv einsetzen will, tut daher gut daran, sich mit diesen nicht gerade trivialen Themen eingehend zu beschäftigen. Zum Glück gibt es Tools, die einem das Einarbeiten erleichtern. Ein solches Tool ist LINQPad von Joseph Albahari (http://www.linqpad.net), mit dem sich beliebige LINQ-Abfragen mit oder ohne Einbeziehung einer Datenbank durchführen lassen.

Ein weiterer Tip für mehr Performance besteht darin, die ObjectTracking­Enabled-Eigenschaft auf False zu setzen, wenn sicher ist, dass keine Updates durchgeführt werden sollen. In diesem Fall verzichtet LINQ to SQL darauf, Änderungen an den Objekten zu verfolgen.


Die nächste Stufe - das Entity Framework

Das wichtigste Merkmal von LINQ to SQL geht nicht unbedingt aus seinem Namen hervor. Es funktioniert nur mit dem Microsoft SQL Server ab Version 2000 (und den kostenfreien Express-Editionen). Theoretisch kämen auch SQL-Server-2005-Compact-Edition-Datenbanken in Frage, allerdings muss das erforderliche Mapping-Schema direkt mit dem Kommandozeilentool Sqlmetal.exe angelegt werden.


Das bedeutet aber nicht, dass andere Datenbanken ausgeschlossen werden. Wer eine Oracle-, Informix- oder MySQL-Datenbank anbinden möchte, muss auf das kommende ADO.NET Entity Framework (ADO.NET EF) warten, das auf einem offenen Provider-Modell basiert und bereits Bestandteil der Beta 1 des Service Pack 1 für .NET 3.5 und Visual Studio 2008 ist. Mit dem Entity Framework kommt eine weitere Abstraktionsebene oberhalb des Datenbankschemas ins Spiel, die allgemein als konzeptionelles Modell bezeichnet wird.



Mit «LINQ to Entities» wird man in Zukunft Abfragen gegen dieses Modell ausführen und nicht mehr gegen Klassen, die sich direkt vom Datenbankschema ableiten. Die Frage, wann LINQ to SQL und wann EF zum Einsatz kommen sollten, wird in den kommenden Monaten noch für viel Diskussionsstoff in der Community sorgen. Die einfache Formel lautet, LINQ to SQL für einfache Ansprüche und der Festlegung auf den MS SQL Server, EF für den Rest.

Der Autor

Peter Monadjemi ist IT-Journalist und hat sich seit vielen Jahren auf Themen im Microsoft-Entwicklerumfeld spezialisiert.




Artikel kommentieren
Kommentare werden vor der Freischaltung durch die Redaktion geprüft.

Anti-Spam-Frage: Wieviele Zwerge traf Schneewittchen im Wald?
GOLD SPONSOREN
SPONSOREN & PARTNER