Von der Datenbank bis zur Oberfläche mit .NET, Teil 1: Datenzugriff und Logik

Seite 2: Datenbank & Datenzugriff

Inhaltsverzeichnis

Darauf erzeugt der Entwickler aus dem Objektmodell die relationale Datenbank. Dafür ist auf der Designeroberfläche der EDMX-Datei im Kontextmenü "Generate Database from Model" auszuwählen. Im Standard muss man nun eine SQL-Server-Instanz (z. B. .\sqlexpress für den lokalen SQL Server Express) angeben. Durch ADO.NET-Entity-Framework-Treiber anderer Hersteller kann die Funktion auch für Oracle & Co. bereitstehen. Der folgende Assistent ist selbsterklärend. Der Entwickler kann eine Datenbank wählen oder den Namen einer noch nicht existierenden Datenbank eingeben; dann wird diese angelegt.

Schließlich entsteht ein SQL-Skript mit Create-Table- und Create-Index-Befehlen. Man kann es aus Visual Studio heraus mit Execute SQL in der Symbolleiste "Transact SQL Editor" ausführen (alternativ: Tastenkombination Ctrl + Shift + e). Die erzeugte Datenbank lässt sich dann im Server Explorer betrachten. Hier sollte man das Modell manuell mit Testdaten befüllen (siehe Abb. 4).

Erzeugte Datenbank und manuelle Befüllung im Server Explorer (Abb. 4)

Für die Datenzugriffsschicht ergänzt man die Projektmappe um ein weiteres Klassenbibliothek-Projekt mit Namen WWWings_DZS. Es benötigt einen Verweis auf das WWWings_GO-Projekt und die .NET-Framework-Assembly System.Data.Entity, die man via Add Reference hinzufügen kann. In diesem Projekt gilt es, C#-Klassen nach folgendem Muster anzulegen:

  1. Für jede der beiden zentralen Entitätsklassen gibt es eine Manager-Klasse.
  2. Die DataManager-Klasse hält jeweils eine Instanz des EF-Kontexts.
  3. Die DataManager-Klasse implementiert IDisposable und vernichtet in Dispose() den EF-Kontext.
FlugDataManager GetFlug(ID) GetFluege(Abflugort, Zielort) ReduceFreiePlatzAnzahl(ID, Platzanzahl) GetFlughaefen() PassagierDataManager GetPassagier(ID) GetPassagiere(Name) AddPassagierZuFlug() SavePassagiere Set()

Listing 2 und 3 zeigen die Implementierung der Methoden mit LINQ-to-Entities-Abfragen gegen den Entity-Framework-Kontext. Zu beachten ist, dass der Kontext nur ein Attribut für den Zugriff auf alle Personen kennt. Um daraus die Passagiere zu filtern, ist der Zusatz OfType<Passagier>() notwendig. Das Filtern erfolgt aber nicht im RAM, sondern zum Glück in der Datenbank. Die Änderungen in den Objekten sind mit SaveChanges() abzuschließen. Mit SingleOrDefault() arbeitet man beim Filtern eines Datensatzes über den Primärschlüssel und erhält ein einzelnes Objekt (oder null) zurück. ToList() liefert hingegen eine Liste von Objekten. Wenn es keine gibt, ist diese leer.

Aufmerksamkeit verdient GetFluege(), denn hier wird eine LINQ-Abfrage abhängig von den Parametern aus verschiedenen Where-Bedingungen zusammengebaut. Das geschieht aufgrund der sogenannten "verzögerten Ausführung" von LINQ-to-Entities: Erst wenn die Daten wirklich gebraucht werden (z. B. ausgelöst durch SingeOrDefault() oder ToList()), wird die Abfrage in SQL umgewandelt und zur Datenbank gesendet. Solange kann man im Programmcode die Abfrage noch beliebig modifizieren.

Spannend ist auch GetFlughaefen(), da hier mit den LINQ-Operatoren Distinct() und Union() gearbeitet wird. Die ersten beiden Distinct()-Aufrufe finden in der Datenbank statt. Union() und das dritte Distinct() sind dann aber eine LINQ-to-Object-Operation im RAM, da zu dem Zeitpunkt die beiden Teillisten ja schon aus der Datenbank gelesen sind.

ApplyChanges() übernimmt in SavePassagierSet() die Änderungen an allen übergebenen Passagieren. Nur informativ wird vor dem Speichern mit SaveChanges() ausgelesen, wie viele Änderungen es zu speichern gilt. SavePassagierSet() liefert diese Änderungsinformation als out-Parameter zurück. Der eigentliche Rückgabetyp ist wieder eine Liste von Passagieren. Diese Liste enthält dann nur noch die neu angelegten Passagiere, weil diese erst durch das Speichern die Primärschlüssel-ID von der Datenbank erhalten haben. SavePassagierSet() muss diese Objekte zurückliefern, damit der Aufrufer die IDs bekommt.

Beim Verwenden einer deutschen Version von Visual Studio muss man einige Namen im Listing anpassen, zum Beispiel erzeugt Visual Studio dort "FlugMenge" und nicht "FlugSet".

Außerdem soll der Entity-Framework-Kontext, der standardmäßig in der Assembly liegt, wo sich die .edmx-Datei befindet (also in WWWings_GO), in die WWWings_DZS verschoben werden. Der Grund liegt darin, dass die Vermengung von EF-Entitätsklassen und -Kontext in WWWings_GO dazu führen würde, dass die Benutzeroberfläche Zugang zum Kontext hätte (siehe Abb. 5). Der Client soll aber auf keinen Fall direkt auf die Datenbank zugreifen können.

Architekturdiagramm des bisherigen Stands des Fallbeispiels (Abb. 5)

Diese Trennung ist nicht so einfach mit der Standard-Codegenerierungsvorlage des Entity Framework umzusetzen. Daher wählt man am besten auf der Designeroberfläche der .edmx-Datei zunächst eine andere Codegenerierungsvorlage mit Add Code Generation Item und dort nun den "ADO.NET Self-Tracking Entity Generator". Daraus entstehen nicht nur trennbare Klassen, sondern auch Klassen, die auf dem Client ihren Änderungsstatus selbst verfolgen können. Das ist bei physikalischer Schichtentrennung (n-Tier-Modell) eine wichtige Funktion, damit der Client selbst entscheiden kann, welche Objekte er zum Speichern zurück zum Server senden muss. Auf die physikalische Schichtentrennung geht der zweite Teil des Tutorials ein.

Stand des Projekts nach Anwendung der neuen Codegenerierungsvorlage und Verschieben der Kontextklasse (Abb. 6)

Wählt man als Dateiname WWWingsModell.tt, entstehen tatsächlich aber mehrere Dateien: WWWingsModell.tt (und ihr untergeordnet je eine Datei pro Entitätsklasse) sowie WWWingsModell.Context.tt (und ihr untergeordnet zwei Dateien für die Kontextklasse). Nun kann man WWWingsModell.Context.tt per Drag & Drop nach WWWings_DZS kopieren und dann in WWWings_GO löschen. Darauf sollte der Stand aus Abbildung 6 erreicht sein.

Damit die verschobene WWWingsModell.Context.tt-Datei zukünftige Änderungen an der .edmx-Datei berücksichtigen kann, muss der Entwickler in Zeile 13 den Pfad zur .edmx-Datei richtig setzen. Aktuell sollte dort

string inputFile = @"WWWingsModell.edmx";

stehen. Das ist durch

string inputFile = @"..\WWWings_GO\WWWingsModell.edmx";

zu ersetzen. Der Pfad sollte relativ sein. Außerdem ist dann nach Zeile 211 (nach using System.Linq;) noch

using WWWings_GO;

einzufügen. Die Zeilennummern könnten sich durch ein Software-Update natürlich verändern. Die beiden Projekte sollten nun ohne Fehler kompilierbar sein. Listing 4 zeigt außerdem eine einfache Erweiterung der Kontextklasse, die bei jedem Speichervorgang in eine Protokolldatei die Änderungen mit Datum und dem ausführenden Benutzer beschreibt.