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.
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.
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:
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:
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 ObjectTrackingEnabled-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.
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.
Peter Monadjemi ist IT-Journalist und hat sich seit vielen Jahren auf Themen im Microsoft-Entwicklerumfeld spezialisiert.