November 2016

Band 31, Nummer 11

Cutting Edge: Code First und Datenbankinitialisierung

Von Dino Esposito | November 2016

Dino EspositoAuch wenn der Begriff „DevOps“ noch relativ neu ist und in letzter Zeit auf zahlreiche weitere Aktivitäten ausgeweitet wurde (insbesondere automatisierte Tests und Bereitstellungen), glaube ich, dass das erste Beispiel eines automatisierbaren Entwicklervorgangs so alt wie die Software selbst ist. Ich denke dabei an die Möglichkeit, die Datenbank während der Anwendungseinrichtung zu erstellen und zu initialisieren. Viele Softwareunternehmen entwickeln vertikale Systeme, die dann an eine Vielzahl von Kunden verkauft und an deren Anforderungen angepasst werden.

Die Aspekte, die angepasst werden können, hängen von den Merkmalen des Produkts und dem Geschäftsfeld ab. Ich wage aber zu behaupten, dass jede vertikale Softwareanwendung mindestens über eine kundenspezifische Datenbank verfügen muss. Aus diesem Grund muss die Datenbank mit den Tabellen und Schemas erstellt werden, die für den Kontext erforderlich sind, und sie muss mit Ad-hoc-Daten aufgefüllt werden.

Nicht alle erforderlichen Aufgaben können immer automatisiert und in das Produkt selbst integriert werden. Denken Sie z. B. an den Import vorhandener Daten. Unabhängig davon, ob die zu importierenden Daten aus Excel-Dateien oder Legacydatenbanken stammen, ist die Wahrscheinlichkeit groß, dass irgendein Importmechanismus erstellt werden muss, um die Daten zu verarbeiten und in den neuen Speicher zu laden. Dabei ist es hilfreich, Entity Framework 6.x Code First in der Datenzugriffsschicht der Anwendung einzusetzen, weil so wenigstens das Erstellen der Datenbankschemas und -tabellen auf einfache Weise automatisiert werden und transparent bei der ersten Ausführung der Anwendung stattfinden kann.

In diesem Artikel beschreibe ich einige der Features, die in Code First immer schon enthalten waren, aus der Perspektive einer Anwendung für mehrere Kunden. Insbesondere beschäftige ich mich mit dem Erstellen und Auffüllen einer Datenbank mit Daten sowie dem programmgesteuerten Definieren ihres Namens und ihrer Verbindungszeichenfolge. 

Schaffen einer Grundlage für Tabellenschemas

Angenommen, Sie verfügen über ein nagelneues Visual Studio-Projekt, das bereits mit dem Entity Framework 6.x NuGet-Paket verknüpft ist. Der nächste Schritt kann dann im Erstellen einer Klassenbibliothek für den Datenzugriff oder wenigstens eines eigenen Ordners im aktuellen Projekt zum Speichern aller Dateien bestehen, die sich durch das Arbeiten mit der Datenzugriffsfunktion ergeben. Gemäß den Entity Framework-Regeln müssen Sie über eine DbContext-Klasse verfügen, die den Einstiegspunkt in das Datenverwaltungssubsystem der Anwendung darstellt. Das folgende Beispiel zeigt eine solche anwendungsspezifische Klasse:

public class RegionsContext : DbContext
{
  ...
}

Aus der Sicht der Anwendung ist die von DbContext abgeleitete Klasse nicht mehr und nicht weniger als die Datenbank. Von der Klasse wird erwartet, dass sie mehrere DbSet<T>-Eigenschaften bereitstellt: eine Eigenschaft für jede Entitätssammlung, die von der Anwendung verwaltet wird. Eine DbSet<T>-Eigenschaft ist das logische Äquivalent zu einer Tabelle in einer physischen Datenbank:

public class RegionsContext : DbContext
{
  public DbSet<Region> Regions { get; set; }
}

Die Aufgabe des hier gezeigten Codeausschnitts besteht darin, es der Anwendung zu ermöglichen, mit einer relationalen Datenbank zusammenzuarbeiten, die eine Tabelle namens „Regions“ enthält. Wie sieht es mit dem Schema der Tabelle aus? Das Schema wird vom öffentlichen Layout der Region-Klasse festgelegt. Code First stellt eine Sammlung von Attributen und eine Fluent-Syntax zum Definieren einer Zuordnung zwischen Klasseneigenschaften und Spalten der zugrunde liegenden Tabelle zur Verfügung. Auf die gleiche Weise können Sie Indizes, Primärschlüssel, Identitätsspalten, Standardwerte und alle anderen Elemente definieren, die Sie in der zugrunde liegenden Datenbank in Spalten und Tabellen konfigurieren können. Dies ist eine Minimalversion der Klasse „Region“:

public class Region
{
  public Region()
  {
    Languages = "EN";
  }
  [Key]
  public string RegionCode { get; set; }
  public string RegionName { get; set; }
  public string Languages { get; set; }
  ...
}

Wie hier gezeigt, verfügen alle Datensätze in der Regions-Tabelle über drei nvarchar­(MAX)-Spalten („RegionCode“, „RegionName“ und „Languages“), und „RegionCode“ wird als Primärschlüsselspalte festgelegt. Außerdem wird für jede neu erstellte Instanz der Region-Klasse die Languages-Eigenschaft auf den Wert „EN“ festgelegt. Auf diese Weise kann sichergestellt werden, dass „EN“ der Standardwert für die Spalte ist, wenn ein neuer Datensatz durch den Code hinzugefügt wird. Beachten Sie jedoch, dass durch das hier gezeigte Festlegen eines Werts im Konstruktor einer Code First-Lösung nicht automatisch eine Standardwertbindung in der zugrunde liegenden Datenbankkonfiguration hinzugefügt wird.

Benennen der Datenbank

In einer Code First-Lösung durchlaufen alle Verbindungen mit der Datenbank die von DbContext abgeleitete Klasse, die Verbindungen nach Bedarf öffnet und schießt. Dabei stellt sie weiterhin die Verbindung öffentlich als eine Eigenschaft bereit, damit der Code die vollständige Steuerung der Öffnungs- und Schließvorgänge übernehmen kann. Wie aber sehen die Details der Verbindungszeichenfolge aus und (noch viel wichtiger) wie stellen Sie Details als Parameter bereit?

Wenn Sie eine von DbContext abgeleitete Klasse erstellen, müssen Sie einen Konstruktor bereitstellen. Das folgende allgemeine Beispiel zeigt dies:

public RegionsContext(string conn) : base(conn)
{
  ...
}

Die DbContext-Klasse besitzt einen Konstruktor, der die Verbindungszeichenfolge als Parameter annimmt. Die einfachste Vorgehensweise besteht daher im Spiegeln der Funktion des zugrunde liegenden Konstruktors durch den Konstruktor Ihrer abgeleiteten Klasse. Intelligenterweise enthält die DbContext-Klasse Programmlogik zum Verarbeiten der übergebenen Zeichenfolge. Von jeder Zeichenfolge, die Sie in der Form „Name=XXX“ übergeben, wird angenommen, dass sie angibt, dass die tatsächliche Verbindungszeichenfolge im XXX-Eintrag im Abschnitt „connectionstrings“ der Konfigurationsdatei der Anwendung (also „web.config“ für ein Webprojekt) enthalten ist. Andernfalls wird angenommen, dass jede übergebene Zeichenfolge der Name der zu erstellenden Datenbank ist. In diesem Fall wird erwartet, dass weitere Details der Verbindungszeichenfolge (z. B. die Anmeldeinformationen und der Serverspeicherort) im Block „defaultConnectionFactory“ des Abschnitts „entityFramework“ in der Konfigurationsdatei enthalten sind. Beachten Sie, dass bei jedem Hinzufügen des Entity Framework-Pakets zu einem Visual Studio-Projekt die Konfigurationsdatei automatisch geändert wird, um den Abschnitt „entityFramework“ zu unterstützen. Abbildung 1 zeigt die zugehörige Auflistung, die aus Gründen der Klarheit etwas berichtigt wurde.

Abbildung 1: Geänderte Beispieldatei „web.config“ für die Unterstützung von Entity Framework Code First

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <configSections>
    <section name="entityFramework"
      type="System.Data.Entity.Internal.ConfigFile.EntityFrameworkSection,..." />
  </configSections>
    <startup>
      <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
    </startup>
  <connectionStrings>
    <!-- Your explicit connection strings -->
  </connectionStrings>
  <entityFramework>
    <defaultConnectionFactory type=
      "System.Data.Entity.Infrastructure.SqlConnectionFactory,
      EntityFramework">
    <parameters>
    <parameter value="Data Source=(local); Integrated Security=True;" />
    </parameters>
  </defaultConnectionFactory>
  <providers>
    <provider invariantName="System.Data.SqlClient"
      type="System.Data.Entity.SqlServer.SqlProviderServices, ..." />
    </providers>
  </entityFramework>
</configuration>

Die meisten Beispiele zu Code First basieren auf einer festen und konstanten Verbindungszeichenfolge, auf die aus der Konfigurationsdatei verwiesen wird oder die explizit an die context-Klasse übergeben wird. Aus diesem Grund erstellt Code First die Datenbank mithilfe der angegebenen Verbindungszeichenfolge bei der ersten Ausführung der Anwendung. Untersuchen wir diesen Aspekt etwas genauer.

Die DbContext-Klasse unterstützt vier Initialisierungsstrategien, die in Abbildung 2 aufgeführt werden.

Abbildung 2: Code-First-Datenbankinitialisierungsstrategien

Strategie Beschreibung
CreateDatabaseIfNotExists

Überprüft, ob die Datenbank vorhanden ist, und erstellt sie, wenn keine Datenbank gefunden wird. Wenn die Datenbank vorhanden ist, jedoch ein inkompatibles Schema aufweist, wird eine Ausnahme ausgelöst.

Hinweis: Dies ist der Standardinitialisierer.

DropCreateDatabaseIfModelChanges Erstellt die Datenbank, wenn sie noch nicht vorhanden ist. Wenn die Datenbank vorhanden ist, aber ein inkompatibles Schema aufweist, wird die vorhandene Datenbank verworfen und eine neue Datenbank erstellt.
DropCreateDatabaseAlways Bei jeder Ausführung der Anwendung wird die vorhandene Datenbank verworfen und eine neue Datenbank erstellt.
Benutzerdefinierter Initialisierer

Eine benutzerdefinierte Initialisiererklasse, die Sie erstellen, um das gewünschte Verhalten bereitzustellen, das keine der anderen Optionen bietet.

Hinweis: Sie müssen diese Option zum Hinzufügen von Masterinhalt zur Datenbank verwenden.

Gemäß dem Standardverhalten von „CreateDatabaseIfNotExists“ wird bei jedem Erstellen der context-Klasse überprüft, ob die referenzierte Datenbank vorhanden und erreichbar ist. Wenn dies nicht der Fall ist, wird die Datenbank erstellt. Wenn die Datenbank vorhanden und erreichbar ist, aber kein Schema aufweist, das kompatibel mit dem öffentlichen Layout von Entitätsklassen ist, wird eine Ausnahme ausgelöst. Zum Entfernen der Ausnahme müssen Sie die Entitätsklasse (oder wahrscheinlicher das Schema der Datenbank) über die Schnittstelle für die Datenbankprogrammierung oder ein Entity Framework-Migrationsskript bearbeiten.

Ich finde diese Option ideal, wenn die Anwendung die Produktionsebene erreicht. Während der Entwicklungsphase bevorzuge ich hingegen die Option „DropCreateDatabaseIfModelChanges“, die Sie im Wesentlichen vor Datenbankwartungsaufgaben bewahrt: Sie müssen nur die Entitätsklassen wie gewünscht optimieren, und Entity Framework repariert dann beim nächsten Drücken von F5 in Visual Studio die Datenbank. Zum Aktivieren der Initialisierungsstrategie Ihrer Wahl fügen Sie die folgende Zeile zum Konstruktor der benutzerdefinierten DbContext-Klasse hinzu:

Database.SetInitializer<YourDbContext>(
  new DropCreateDatabaseIfModelChanges<YourDbContext>());

Beachten Sie, dass Sie den Datenbankinitialisierer auch in der Konfigurationsdatei festlegen können. Dies kann sinnvoll sein, wenn Sie planen, verschiedene Strategien in der Produktion und der Entwicklung zu verwenden.

Zusammenfassend lässt sich sagen, dass Code First Ihnen das Schreiben einer Anwendung ermöglicht, die automatisch alle zugehörigen Datenbanktabellen bei ihrer ersten Ausführung erstellt. Anders gesagt: Sie müssen nur Dateien und Binärdateien kopieren und die Anwendung dann starten. Dieses Verhalten funktioniert optimal, wenn das System für einen einzelnen Kunden erstellt wird. Wenn Sie ein System für mehrere Kunden erstellen, sollten Sie ein Setuphilfsprogramm verwenden.

Ein Grund, sich für einen etwas anderen Ansatz zu entscheiden, besteht darin, dass Sie ggf. die Datenbank anders nennen möchten. Sie können dem Namen z. B. ein kundenspezifisches Präfix voranstellen. Abbildung 3 zeigt die Struktur eines solchen Befehlszeilen-Hilfsprogramms. Das Programm übernimmt das Kundenpräfix aus der Befehlszeile, formatiert den Datenbanknamen wie gewünscht und löst dann die von DbContext abgeleitete Klasse aus, die die Datenbank erstellt und mit den entsprechenden Anfangsdaten auffüllt.

Abbildung 3: Kundenspezifischer Name der Datenbank

class Program
{
  static void Main(string[] args)
  {
    var prefix = args[0];
                // boundary checks skipped
    var dbName = String.Format("yourdb_{0}", prefix);
    using (var db = new YourDbContext(dbName))
    {
      // Fill the database in some way
    }
  }
}

Anfängliches Auffüllen der Datenbank mit Daten

Jedes System, das die Anforderungen mehrerer Kunden im gleichen Geschäftsfeld erfüllen soll, muss über mehrere Tabellen verfügen, die für das Speichern von Optionen und Einstellungen vorgesehen sind, die sich von Kunde zu Kunde unterscheiden. Diese Informationen müssen bei der Installation der Software bereitgestellt werden. In der Realität wird ein Teil der Anfangslast der Datenbank von allen Installationen gemeinsam verwendet. Ein anderer Teil ist jedoch kundenspezifisch. Der Teil, der von den Daten des Kunden abhängt, wird häufig aus externen Quellen importiert und erfordert eine Ad-hoc-Routine. Dies gilt unabhängig davon, ob es sich um ein Skript oder kompilierten Code handelt. Abhängig vom Kontext kann es sogar sinnvoll sein, über einen Abhängigkeitsinjektionsmechanismus zum Generalisieren der Struktur der Importmodule im Setuphilfsprogramm nachzudenken, der die Datenbank initialisiert. Für statischen Datenbankinhalt bietet Code First jedoch Ad-hoc-Dienste.

Benutzerdefinierte Initialisierer

Damit die Datenbank während des Initialisierungsvorgangs mit Daten aufgefüllt wird, ist es erforderlich, einen benutzerdefinierten Datenbankinitialisierer wie in Abbildung 2 beschrieben zu erstellen. Ein benutzerdefinierter Initialisierer ist eine Klasse, die von einem der vordefinierten Initialisierer wie „DropCreateDatabaseIfModel­Changes“ erbt. Die einzige strenge Anforderung für diese Klasse besteht darin, dass die Seed-Methode überschrieben wird:

public class YourDbInitializer : DropCreateDatabaseAlways<YourDbContext>
{
  protected override void Seed(YourDbContext context)
  {              
    ...
  }
}

In der Implementierung der Seed-Methode führen Sie Code aus, der die Tabellen der Datenbank mit Daten auffüllt und dabei den bereitgestellten DbContext für den Zugriff auf die Datenbank verwendet. Mehr ist nicht nötig.

Wenn Sie eine Anwendung für mehrere Kunden planen, ist das Definieren eines benutzerdefinierten Initialisierers ein guter Schachzug, weil er einen einzelnen Punkt darstellt, auf den Sie sich konzentrieren können, um die Anfangsform der Datenbank kundenspezifisch zu gestalten. Der Initialisierer ist eine einfache C#-Klasse und kann daher mithilfe von Abhängigkeitsinjektionstools mit bestimmten Teilen der Programmlogik verbunden werden, die Daten aus dem Quellspeicherort importieren kann.

Nicht zuletzt können Datenbankinitialisierer vollkommen deaktiviert werden, damit das Setup der Datenbank ein vollkommen eigenständiger Vorgang bleibt. Dieser kann sogar von einem anderen IT- oder DevOps-Team verwaltet werden. Wenn Sie das Code First-Framework anweisen möchten, Initialisierer zu ignorieren, müssen Sie Code im Konstruktor der benutzerdefinierten DbContext-Klasse verwenden:

Database.SetInitializer<YourDbContext>(null);

Dies ist z. B. eine sichere Option, wenn Sie Updates für vorhandene Systeme freigeben. Durch das Deaktivieren von Initialisierern wird sichergestellt, dass unter keinen Umständen vorhandene Daten verloren gehen.

Zusammenfassung

Zusammenfassend lässt sich sagen, dass durch Code First das Schreiben von mehrinstanzenfähigen Anwendungen und Anwendungen für mehrere Kunden nicht schwieriger als das Erstellen von Anwendungen ist, die speziell für eine bekannte Konfiguration und einen bekannten Kunden geschrieben werden. Es sind nur Kenntnisse bezüglich der Zuweisung von Verbindungszeichenfolgen sowie des Initialisierungsvorgangs erforderlich. In Entity Framework Core bleiben die Kernprinzipien unverändert, obwohl sich die Details der effektiven Funktionsweise unterscheiden. Insbesondere weist die neue DbContext-Klasse eine überschreibbare OnConfiguring-Methode auf, über die Sie den Kontext mit dem Datenbankanbieter Ihrer Wahl verbinden. Anschließend übergeben Sie Anmeldeinformationen und andere Elemente an sie.


Dino Espositoist Autor von „Microsoft .NET: Architecting Applications for the Enterprise“ (Microsoft Press, 2014) und „Modern Web Applications with ASP.NET“ (Microsoft Press, 2016). Esposito ist Technical Evangelist für die .NET- und Android-Plattformen bei JetBrains und spricht häufig auf Branchenveranstaltungen weltweit. Auf software2cents.wordpress.com und auf Twitter unter twitter.com/despos lässt er uns wissen, welche Softwarevision er verfolgt.

Unser Dank gilt dem folgenden technischen Experten bei Microsoft für die Durchsicht dieses Artikels: Andrea Saltarello (andrea.saltarello@manageddesigns.it)
Andrea Saltarello ist ein italienischer Unternehmer und Softwarearchitekt aus Mailand, der leidenschaftlich gern Code für echte Projekte schreibt, um Feedback zu seinen Entwurfsentscheidungen zu erhalten. Als Trainer und Sprecher wurde er vielfach für Kurse und Konferenzen in ganz Europa engagiert, z. B. für TechEd Europe, DevWeek und Software Architect. Seit 2003 ist er ein Microsoft MVP, und er wurde vor Kurzem zum Microsoft Regional Director ernannt. Er liebt Musik und ist Fan von Depeche Mode, deren künstlerisches Schaffen er besonders schätzt, seit er „Everything Counts“ zum ersten Mal gehört hat.