29.7 Der LINQ to SQL-Designer (O/R-Designer) 

29.7.1 Handhabung des O/R-Designers 

Wenn Sie die Kapitel 23 bis 28 zu ADO.NET aufmerksam gelesen haben und jetzt an dieser Stelle einen Vergleich zwischen ADO.NET und LINQ to SQL anstellen, werden Sie vermutlich zu dem Schluss kommen, dass der technologische Hintergrund von LINQ to SQL auf ADO.NET basiert. Beide Bibliotheken ähneln sich in vielfacher Hinsicht, aber die Codeimplementierung ist mit LINQ to SQL einfacher und überschaubarer geworden, während sich gleichzeitig die Anzahl der eingesetzten Klassen deutlich reduziert hat. Im Grunde genommen rankt sich alles nur noch um das DataContext-Objekt.
Abbildung 29.2 Das Element »LINQ to SQL-Klassen« hinzufügen
Allerdings gibt es momentan auch einen Wermutstropfen: Die Definition der Entitätsklassen ist sehr aufwendig und scheint die programmiertechnischen Vorteile zunichte zu machen. Aber diese scheinbare Problematik wird von Visual Studio selbst gelöst, denn es stellt einen integrierten Designer zur Verfügung, der es ermöglicht, mittels Drag & Drop die notwendigen Klassen zu erzeugen.
Wir wollen uns das Arbeiten mit dem Designer sofort an einem konkreten Beispiel ansehen und dazu die Tabelle Products der Northwind-Datenbank benutzen. Deshalb starten wir ein Windows Forms-Projekt. Nach dem Anlegen des Projekts markieren Sie im Projektmappen-Explorer das Projekt und öffnen dessen Kontextmenü. Über Hinzufügen • Neues Element… gelangen Sie zu dem in Abbildung 29.2 gezeigten Auswahldialog, in dem Sie LINQ to SQL-Klassen auswählen. Da wir anschließend auf eine Tabelle der Northwind-Datenbank zugreifen wollen, bietet es sich an, abweichend von der Vorgabe die erzeugte Datei Northwind.dbml zu nennen.
Nach der Bestätigung im Dialogfenster wird in Visual Studio der O/R-Designer geöffnet (siehe Abbildung 29.3), in dem später die Entitätsklassen und gegebenenfalls auch deren Beziehung grafisch dargestellt werden.
Abbildung 29.3 Der O/R-Designer
Als Nächstes müssen wir die Datenbankverbindung definieren und anschließend die Tabelle Products angeben. Dazu dient das Tool Server-Explorer, das über das Menü Ansicht von Visual Studio geöffnet werden kann. Im Server-Explorer werden über den Knoten Datenverbindungen die Verbindungen angezeigt, die Sie in Visual Studio angelegt haben. Wird Ihnen hier bereits die Northwind-Datenbank angeboten, können Sie diese benutzen. Ansonsten legen Sie eine neue Datenverbindung über das Kontextmenü Datenverbindungen an.
Sobald im Server-Explorer die Verbindung zur Northwind-Datenbank erstellt ist, markieren Sie im untergeordneten Knoten Tabellen die Tabelle Products und ziehen diese in den linken Fensterbereich des O/R-Designers. Danach wird die Tabelle im Designer mit allen ihren Feldern visualisiert dargestellt (siehe Abbildung 29.4). Möchten Sie mit gespeicherten Prozeduren oder Funktionen arbeiten, müssen Sie diese aus dem Server-Explorer in den rechten Fensterbereich des Designers ziehen.
Abbildung 29.4 Die Entity-Klasse der Tabelle »Products« im O/R-Designer
Bis jetzt wurden mehrere Dateien dem aktuellen Projekt hinzugefügt:
- eine DBML-Datei (hier: Northwind.dbml), hinter der sich die Definition der Metadaten der Entitätsklasse im XML-Format verbirgt. In Abbildung 29.5 sehen Sie den Inhalt der generierten DBML-Datei.
- die Datei Northwind.dbml.layout, die Informationen für das Layout im Designer enthält
- die Datei Northwind.designer.cs, die die generierten Klassen enthält
Darüber hinaus wird dem Projekt auch die Anwendungskonfigurationsdatei app.config hinzugefügt, in der die Verbindungsinformationen zu der ausgewählten Datenbank eingetragen sind. Sie können also zu einem späteren Zeitpunkt die Verbindungsdaten jederzeit an die der Produktivumgebung anpassen.
Abbildung 29.5 Inhalt der ».dbml«-Datei
Die Datei Northwind.designer.cs beschreibt in unserem Beispiel zwei Klassendefinitionen: Es handelt sich einerseits um die Klasse NorthwindDataContext, die von der Klasse DataContext abgeleitet ist und unter anderem für den Verbindungsaufbau zum SQL Server zuständig ist. Die zweite Klasse ist die Entitätsklasse Products. Diese Klasse beschreibt alle Spalten der Originaltabelle, die aber bei Bedarf auch auf die benötigten Spalten reduziert werden können. Bedenken Sie aber, dass später nur die Daten der Spalten abgefragt werden, die tatsächlich benötigt werden. Sprechen nicht schwerwiegende Gründe gegen die Aufnahme aller Spalten in der Entitätsklasse (z. B. Sicherheitsaspekte), sollten Sie keine Reduzierung vornehmen.
Sowohl die Definition der DataContext-Klasse als auch die Definition der Entitätsklasse sollten wir uns etwas genauer ansehen. Beide beherbergen Klassenmitglieder, die über das hinausgehen, was in Abschnitt 29.6, »Die Klasse ›DataContext‹«, erörtert worden ist.
29.7.2 Die abgeleitete »DataContext«-Klasse 

Die Klasse NorthwindDataContext ist von der Klasse DataContext abgeleitet. Im Wesentlichen bietet sie uns für jede verwaltete Entitätsklasse einen vereinfachten Zugriff über eine Eigenschaft, die so heißt wie die Entitätsklasse selbst. Haben wir im O/R-Designer die Tabelle Products hinzugefügt (und den Klassenbezeichner in Product geändert), sieht der automatisch generierte Code wie folgt aus:
public System.Data.Linq.Table<Product> Product { get { return this.GetTable<Product>(); } }
Damit ist es möglich, mit
NorthwindDataContext context = new NorthwindDataContext(); Table<Product> products = context.Product;
direkt eine Liste der Product-Entitäten zu erstellen. Ein wenig erinnert uns das an typisierte DataSets.
Neben mehreren Konstruktoren, deren Parameterlisten sich nicht von denen der Basisklasse unterscheiden, weist die abgeleitete DataContext-Klasse eine Reihe partieller Methoden auf. Partielle Methoden sind mit C# 3.0 eingeführt worden. Sie sind eine logische Fortsetzung der Idee partieller Klassen. Sind in einer Klasse partielle Methoden definiert, können diese in einer Erweiterung der partiellen Klasse definiert werden. Betrachten wir dazu ein einfaches Beispiel:
partial class DataObject { partial void OnCreated(); public DataObject() { OnCreated(); } }
In der Klasse DataObject ist die partielle Methode OnCreated definiert. Grundsätzlich dürfen partielle Methoden nur void sein. Ein Zugriffsmodifizierer ist unzulässig, partielle Methoden sind daher immer private. Wird ein Objekt der Klasse DataObject erzeugt, passiert noch nichts, denn der Compiler ignoriert die partielle Methode, da sie keinen Code enthält.
Eine partielle Methode müssen Sie als ein Angebot ansehen, das Sie annehmen können oder nicht – vergleichbar mit dem Angebot, auf ein Ereignis zu reagieren oder nicht. Deshalb finden Sie sehr häufig Hinweise auf die nahe Verwandtschaft von Ereignissen und partiellen Methoden.
Erweitern Sie die Klasse DataObject, können Sie die partielle Methode implementieren.
partial class DataObject { partial void OnCreated() { Console.WriteLine("In der partiellen Methode"); } }
Beim Kompilieren wird aus den beiden separaten Klassendefinitionen eine Klasse erzeugt, wobei nun auch die partielle Methode berücksichtigt wird. Wird beim Instanziieren der Klasse DataObject der Konstruktor aufgerufen, wird auch die partielle Methode ausgeführt.
Nach dieser Erläuterung kehren wir zu der DataContext-Klasse zurück, die in unserem Beispiel vier partielle Methoden anbietet:
partial void OnCreated(); partial void InsertProduct(Product instance); partial void UpdateProduct(Product instance); partial void DeleteProduct(Product instance);
OnCreated wird in jedem Konstruktor aufgerufen. Möchten Sie bei der Instanziierung noch weitere Operationen begleitend ausführen lassen, fügen Sie dem Projekt eine neue Klassendefinition hinzu und implementieren die Methode OnCreated, wie im folgenden Codefragment zu sehen ist:
partial class NorthwindDataContext { partial void OnCreated() { // Anweisungen } }
Kommen wir nun zu den anderen drei partiellen Methoden InsertXxx, UpdateXxx und DeleteXxx. Diesen drei Methoden ist als Suffix der Bezeichner der Entitätsklasse angehängt. Wie Sie wissen, wird die Datenbank durch den Aufruf der Methode SubmitChanges aktualisiert. Die erforderlichen SQL-Statements werden dabei automatisch erzeugt und können durch das Attribut UpdateCheck beeinflusst werden. In den meisten Fällen ist das vollkommen ausreichend, um Einfluss auf die Aktualisierung auszuüben.
Ist eine Aktualisierungsoperation gefordert, die über die vorgegebenen Möglichkeiten hinausgeht, können Sie durch Implementierung der drei Methoden die Vorgabe überschreiben. Wenn wir beispielsweise annehmen, Sie möchten die Preise aller Artikel der Products-Tabelle verdoppeln, dann könnten Sie mit
partial class NorthwindDataContext {
partial void UpdateProduct(Product instance) {
this.ExecuteCommand(
"UPDATE Products SET UnitPrice= UnitPrice *2");
}
}
den Aktualisierungsprozess selbst festlegen. Die Methode ExecuteCommand des DataContext-Objekts können Sie natürlich auch außerhalb einer partiellen Methode aufrufen.
29.7.3 Entitätsklassen 

Werfen wir nun einen Blick in die vom O/R-Designer erzeugte Entitätsklasse. Diese ist bereits mit dem Table-Attribut verknüpft. Alle spaltenbeschreibenden Eigenschaften weisen das Attribut Column auf, dessen Parameter auf die üblichen Standardwerte eingestellt sind. Möchten Sie diese spezifisch einstellen, ist das im Eigenschaftsfenster am einfachsten. Markieren Sie dazu nur die betreffende Spalte im Designer.
Etwas unglücklich ist die Bezeichnung der Entitätsklasse, die für die Tabelle Products erzeugt wird. Sie heißt so wie die Tabelle, also Products. Inzwischen wissen Sie, dass eine Instanz dieser Klasse einen einzelnen Datensatz abbildet und nicht mehrere, wie man der Pluralisierung entnehmen könnte. Daher sollten Sie Products in Product ändern. Da Sie später häufig eine Liste mehrerer gemappter Datensätze benötigen, können Sie die Liste Products oder products nennen und vermeiden so Irritationen.
Den Bezeichner der Entitätsklasse können Sie im Eigenschaftsfenster ändern. Sie müssen das aber auch bei jeder weiteren Entitätsklasse tun. Eine Alternative ist es, die Vorgabe der Pluralisierung grundsätzlich in Visual Studio zu ändern. Eine solche Einstellungsmöglichkeit gibt es. Sie können sie im Optionen-Dialog vornehmen, den Sie über Extras • Optionen öffnen. Wählen Sie aus der linken Liste das Element Datenbanktools aus und anschließend den Eintrag O/R-Designer. Im rechten Bereich des Dialogs wird die Eigenschaft Aktiviert angezeigt, die Sie von False auf True einstellen.
Jede vom O/R-Designer erzeugte Entitätsklasse implementiert mit INotifyPropertyChanging und INotifyPropertyChanging zwei Schnittstellen. Über diese Interfaces werden der Entitätsklasse die beiden Ereignisse PropertyChanging und PropertyChanged aufgezwungen. Entsprechend der üblichen Namenskonvention wird das Ereignis PropertyChanging ausgelöst, ehe ein Feldwert der Entität aktualisiert wird, und PropertyChanged wird ausgelöst, nachdem der Feldwert aktualisiert worden ist. Sie können in Ihrem Code auf die beiden Ereignisse reagieren, wenn Sie im Zusammenhang mit einer Feldaktualisierung noch weitere Operationen ausführen lassen wollen. Sehen wir uns exemplarisch die Definition der Spalte ProductName in der Entitätsklasse an:
public string ProductName {
get {
return this._ProductName;
}
set {
if ((this._ProductName != value)) {
this.OnProductNameChanging(value);
this.SendPropertyChanging();
this._ProductName = value;
this.SendPropertyChanged("ProductName");
this.OnProductNameChanged();
}
}
}
Mit der Anweisung
this._ProductName = value;
wird ein neuer Wert in das Feld ProductName der Entität geschrieben. Ehe die Aktualisierung der Eigenschaft erfolgt, wird mit SendPropertyChanging eine Methode aufgerufen, und direkt nach der Aktualisierung folgt SendPropertyChanged. Diese beiden Methoden kapseln die Ereignisauslösung und prüfen dabei, ob sich ein Abnehmer des Ereignisses registriert hat.
protected virtual void SendPropertyChanging() {
if ((this.PropertyChanging != null)) {
this.PropertyChanging(this, emptyChangingEventArgs);
}
}
protected virtual void SendPropertyChanged(String propertyName) {
if ((this.PropertyChanged != null)) {
this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
Schauen Sie sich noch einmal den dynamisch erzeugten Code der Spalte ProductName an. Sie werden mit OnProductNameChanging und OnProductNameChanged noch zwei weitere Methoden erkennen. Hierbei handelt es sich um den Aufruf von zwei partiellen Methoden, die in der Entitätsklasse ebenfalls vordefiniert sind.
Da partielle Methoden als Ereignis interpretiert werden können, stellt sich an dieser Stelle die Frage, worin der Unterschied zwischen einer partiellen Methode (beispielsweise OnProductNameChanging) und einem Ereignis (PropertyChanging) besteht?
Implementieren Sie eine partielle Methode, sind alle Komponenten Nutznießer dieses »Ereignisses«. Die zugrunde liegende Entitätsklasse wird also um ein allgemeines Feature erweitert, von dem alle zugreifenden Komponenten profitieren. Ein Ereignis, wie zum Beispiel PropertyChanging, bleibt weiterhin eine Option, die Sie als Entwickler im Einzelfall nutzen können – oder auch nicht.