Datenpunkte

Code First-Funktionen in Entity Framework 6

Julie Lerman

Beispielcode herunterladen.

Julie LermanIn meinem Artikel vom Dezember 2013 „Entity Framework 6: Die Ninja-Edition“ (msdn.microsoft.com/magazine/dn532202) habe ich viele der neuen Features in Entity Framework 6 (EF6) beschrieben. Allerdings konnte ich nicht detailliert auf jedes einzelne Feature eingehen. Deshalb widme ich mich in diesem Monat einigen spezifischen EF6-Erweiterungen für Code First. Zwei der Features davon sind für Code First-Zuordnungen relevant, die anderen beziehen sich auf Code First-Migrationen.

Viele Fluent-API-Zuordnungen auf einmal laden

Es gibt zwei Möglichkeiten, Fluent-Zuordnungen (von Klassen zur Datenbank) für ein Modell anzugeben. Eine Möglichkeit verbirgt sich direkt in der OnModelCreating-Methode der DbContext-Klasse:

modelBuilder.Entity<Casino>()
  .Property(c=>c.Name).IsRequired().HasMaxLength(200);
modelBuilder.Entity<PokerTable>()
  .Property(c => c.SerialNo).HasColumnName("SerialNumber");

Wenn Sie viele Zuordnungen haben, können Sie diese basierend auf dem Typ in einzelne EntityTypeConfiguration-Klassen gruppieren und anschließend mit folgendem Code zum Modell-Generator hinzufügen:

modelBuilder.Configurations.Add(new CasinoConfiguration());
modelBuilder.Configurations.Add(new PokerTableConfiguration());

Haben Sie jedoch viele Zuordnungen für eine Vielzahl von Entities, erhalten Sie möglicherweise viele sich wiederholende modelBuilder.Configurations.Add-Methoden in „OnModelCreating“. Um dies zu verhindern, können Sie nun alle „EntityTypeConfigurations“ aus einer gegebenen Assembly mit einer einzelnen Methode laden. Ich nutze hier die neue AddFromAssembly-Methode zum Laden von Konfigurationen, die in der ausführenden Assembly für die laufende Anwendung angegeben sind.

modelBuilder.Configurations
  .AddFromAssembly(Assembly.GetExecutingAssembly())

Das Schöne bei dieser Methode ist, dass es keinerlei Einschränkungen hinsichtlich des Umfangs der Konfigurationen gibt, die geladen werden. Die benutzerdefinierten EntityTypeConfiguration-Klassen können sogar als „private“ gekennzeichnet sein und werden von der Methode gefunden. „AddFromAssembly“ erfasst auch Vererbungshierarchien in „EntityTypeConfigurations“.

„AddFromAssembly“ resultiert aus einem der vielen Communitybeiträge von Unai Zorrilla. Weitere Informationen erhalten Sie in Zorrillas Blogbeitrag „EF6: Automatisches Festlegen von Konfigurationen“ unter bit.ly/16OBuJ5. Er freut sich sicherlich über ein gepostetes Dankeschön.

Definieren eines eigenen Standardschemas

In meiner Rubrik „Datenpunkte“ vom März 2013, „Spielen mit der EF6-Alphaversion“ (msdn.microsoft.com/magazine/jj991973), sprach ich über eine Schemaunterstützung für Code First. Bei einem der neuen Features handelt es sich um eine Zuordnungsfunktion, die Sie in „OnModelCreating“ konfigurieren können: „HasDefaultSchema“. Damit können Sie das Datenbankschema für alle Tabellen festlegen, denen ein Kontext zugeordnet wird, ohne den EF-Standard von „dbo“ zu verwenden. Im Artikel „Entity Framework 6: Die Ninja-Edition“ habe ich das Ausführen von rohem SQL-Code im Zusammenhang mit „DbTransactions“ beschrieben:

("Update Casino.Casinos set rating= " + (int) Casino.Rating)

Ihnen ist vermutlich das Casinoschema aufgefallen, das ich in SQL angegeben habe. Der Grund für ein Schema mit diesem Namen ist, dass ich dies in der OnModelCreating-Methode von „DbContext“ (CasinoSlotsModel) festgelegt habe.

modelBuilder.HasDefaultSchema("Casino");

Darüber hinaus habe ich über die neue Unterstützung in EF6 für Code First-Migrationen gesprochen, die auf einer Datenbank mit verschiedenen Schemas ausgeführt werden. Dies hat sich seit der Alphaversion von EF6 nicht geändert, sodass Sie bei Bedarf den Artikel in der entsprechenden Rubrik nachlesen können.

Migrationsskripts zum erneuten Erstellen einer Datenbank von einem beliebigen Punkt aus

Eines der neuen Features für Code First-Migrationen, die in den Spezifikationen und jedem Artikel aufgeführt sind, in dem auf die Spezifikationen eingegangen wird, nennt sich „idempotent migrations scripts“ (idempotente Migrationsskripts). Nun, vielleicht sind Sie ja Informatiker oder DBA. Ich nicht, deshalb musste ich die Bedeutung von „idempotent“ (Idempotenz) nachschlagen. Wikipedia bezieht sich auf einen IBM-Techniker und definiert den Begriff folgendermaßen(bit.ly/9MIrRK): „In der Informatik wird der Begriff „Idempotenz“ verwendet, um eine Operation zu beschrieben, die unabhängig davon, ob sie einmal oder mehrmals ausgeführt wird, dasselbe Ergebnis liefert.“ Ich musste zudem nachsehen, wie das Wort ausgesprochen wird. Die englische Aussprache ist wie folgt: eye-dem-poe-tent.

In der Welt der Datenbanken kann mit der Idempotenz ein SQL-Skript beschrieben werden, das immer denselben Einfluss auf eine Datenbank hat und dies unabhängig von deren Zustand. Bei Code First-Migrationen prüft ein solches Skript vor dem Ausführen der Migration, ob die Migration bereits ausgeführt wurde. Dies ist ein spezifisches Feature für den -script-Parameter von „Update-Database“.

Mit EF konnten schon immer Skripts erstellt werden, die alle Migrationsschritte ab einem bestimmten Startpunkt (Quelle) bis, optional, zu einem bestimmten Endpunkt (Ziel) durchlaufen. Dieser NuGet-Befehl erwirkt folgendes Verhalten:

Update-Database -Script
  –SourceMigration:NameOfStartMigration
  –TargetMigration:NameOfEndMigrationThatIsntLatest

Neu dabei ist, dass das generierte Skript nun wesentlich intelligenter ist, wenn der Befehl auf eine bestimmte Weise aufgerufen wird:

Update-Database -Script -SourceMigration $InitialDatabase

Dies funktioniert auch, wenn Sie $InitialDatabase durch 0 ersetzen. Allerdings ist nicht garantiert, dass diese Alternative auch in zukünftigen Versionen unterstützt wird.

Als Reaktion darauf startet das Skript mit der ursprünglichen Migration und durchläuft alle Migrationen bis hin zur aktuellen. Deshalb stellt die Syntax die Namen der Ziel- und Quellmigrationen nicht explizit bereit.

Durch diesen spezifischen Befehl aber fügt EF6 eine Logik zum Skript hinzu, die überprüft, welche Migrationen bereits angewendet wurden, ehe SQL für eine bestimmte Migration ausgeführt wird. Nachfolgend ein Codebeispiel aus dem Skript:

    IF @CurrentMigration < '201310311821192_AddedSomeNewPropertyToCasino'
    BEGIN
      ALTER TABLE [Casino].[Casinos] ADD [AgainSomeNewProperty] [nvarchar](4000)
      INSERT [Casino].[__MigrationHistory]([MigrationId], [ContextKey], [Model], [ProductVersion])
      VALUES (N'201310311821192_AddedSomeNewPropertyToCasino',
        N'CasinoModel.Migrations.Configuration', HugeBinaryValue , 
        N'6.1.0-alpha1-21011')
    END

Der Code prüft in der _MigrationHistory-Tabelle, ob das AddedSomeNewPropertyToCasino-Skript bereits in der Datenbank ausgeführt wurde. Ist dies der Fall, wird der SQL-Code der Migration nicht ausgeführt. Vor EF6 hat das Skript den Code einfach ausgeführt, ohne zu prüfen, ob dieser bereits vorher ausgeführt worden war.

Anbieterfreundliche Migrationsverlaufstabellen

Mit EF6 können Sie durch ein neues Feature namens „Customizable Migrations History Table“ (anpassbare Migrationsverlaufstabelle) festlegen, wie die _MigrationHistory-Tabelle definiert wird. Das ist wichtig, wenn Sie Daten externer Anbieter nutzen, deren Anforderungen von den Standards abweichen. Abbildung 1 zeigt das Standardschema für die Tabelle.

Nachfolgend ein Beispiel für einen sinnvollen Einsatz. Ein Entwickler hat auf CodePlex festgestellt, dass er, da jedes Zeichen in den beiden primären Schlüsseln größer sein kann als 1 Byte, eine Fehlermeldung erhält, die darauf hinweist, dass die Länge des Verbundschlüssels, der mit „MigrationId“ und „ContextKey“ erstellt wurde, die zulässige Schlüssellänge für eine MySQL-Tabelle überschreitet: 767 Byte (bit.ly/18rw1BX). Zur Lösung des Problems nutzt das MySQL-Team „HistoryContext“ intern, um die Länge der beiden Schlüsselspalten zu ändern, während an der EF6-Version von MySQL Connector für ADO.NET gearbeitet wird (bit.ly/7uYw2a).

Beachten Sie in Abbildung 1, dass die __MigrationHistory-Tabelle dasselbe Schema erhält, das ich mit „HasSchema“ für den Kontext definiert habe: Casino. Nach Ihren Konventionen sollen möglicherweise nicht datenbezogene Tabellen von einem Schema mit eingeschränkten Berechtigungen verwendet werden. In diesem Fall möchten Sie den Namen des Schemas vermutlich ändern. Hierzu können Sie „HistoryContext“ einsetzen, um beispielsweise festzulegen, dass das admin-Schema verwendet wird.

Default Schema for the __MigrationHistory Table
Abbildung 1: Standardschema für die __MigrationHistory-Tabelle

„HistoryContext“ wird von „DbContext“ abgeleitet, deshalb sollte Ihnen der Code vertraut sein, wenn Sie in der Vergangenheit bereits mit „DbContext“ und Code First gearbeitet haben. Abbildung 2 zeigt eine HistoryContext-Klasse, die ich zum Festlegen des admin-Schemas definiert habe.

Abbildung 2: Benutzerdefinierter „HistoryContext“ für eine Neudefinition der __MigrationHistory-Tabelle

public class CustomHistoryContext : HistoryContext
{
  public CustomHistoryContext
   (DbConnection dbConnection, string defaultSchema)
     : base(dbConnection, defaultSchema)
  {
  }
  protected override void OnModelCreating(DbModelBuilder modelBuilder)
  {
    base.OnModelCreating(modelBuilder);
    modelBuilder.Entity<HistoryRow>()
      .ToTable("__MigrationHistory", "admin");
  }
}

Sie können auch bekannte API-Aufrufe wie „Property().HasColumnType“, „HasMaxLength“ oder „HasColumnName“ verwenden. Wenn Sie beispielsweise die Länge von „ContextKey“ ändern müssen, können Sie folgendermaßen vorgehen:

modelBuilder.Entity<HistoryRow>()
  .Property(h => h.ContextKey).HasMaxLength(255);

Wenn Sie bereits den Artikel des letzten Monats gelesen haben, sollten Sie mit der EF6-DbConfiguration vertraut sein. Damit informieren Sie Ihr Modell über die CustomHistoryContext-Datei. Geben Sie in Ihrem benutzerdefinierten DbConfiguration-Konstruktor den zu verwendenden „HistoryContext“ an. Hier habe ich den Kontext für den SQL Server-Anbieter festgelegt, um „CustomHistoryContext“ zu verwenden:

SetHistoryContext(
  SqlProviderServices.ProviderInvariantName,
     (connection, defaultSchema) =>
  new CustomHistoryContext(connection,
     defaultSchema));

Die Funktionen zur Datenbankinitialisierung und -migration „sehen“ diesen zusätzlichen Kontext und konstruieren SQL entsprechend. Die Tabelle aus Abbildung 3 wurde mit dem benutzerdefinierten „HistoryContext“ erstellt, um den Schemanamen der __MigrationHistory-Tabelle in „admin“ zu ändern. (Den Beispielcode zum Ändern der Spaltenlänge habe ich weggelassen.)

Customized __MigrationHistory Table
Abbildung 3: Benutzerdefinierte __MigrationHistory-Tabelle

„HistoryContext“ ist ein leistungsstarkes Feature, sollte aber mit Bedacht eingesetzt werden. Wenn Sie Glück haben, hat der von Ihnen genutzte Datenbankanbieter dieses Feature bereits eingesetzt, um eine für die Zieldatenbank relevante __MigrationHistory-Tabelle festzulegen, und Sie brauchen sich darum nicht mehr zu kümmern. Ist dies nicht der Fall, können Sie im MSDN-Dokument nach Informationen und Anleitungen zu diesem Feature suchen (bit.ly/16eK2pD).

Erstellen von benutzerdefinierten Migrationsoperationen

Wenn Sie bereits zuvor Migrationen verwendet haben – nicht automatisch, sondern durch explizites Erstellen und Ausführen von Migrationen über das Fenster der Paket-Manager-Konsole –, sind Sie vielleicht auf die Migrationsdateien gestoßen, die von „add-migration“ erstellt wurden. In diesem Fall haben Sie vermutlich auch bemerkt, dass Code First-Migrationen eine stark typisierte API haben, um alle Änderungen zu beschreiben, die am Datenbankschema vorzunehmen sind: System.Data.Entity.Migrations.DbMigration.

Abbildung 4 enthält ein Beispiel der CreateTable-Methode, die eine Reihe von Attributen festlegt.

Abbildung 4: DbMigrations.CreateTable-Methode

CreateTable(
  Casino.SlotMachines",
  c => new
    {
      Id = c.Int(nullable: false, identity: true),
      SlotMachineType = c.Int(nullable: false),
      SerialNumber = c.String(maxLength: 4000),
      HotelId = c.Int(nullable: false),
      DateInService = c.DateTime(nullable: false),
      HasQuietMode = c.Boolean(nullable: false),
      LastMaintenance = c.DateTime(nullable: false),
      Casino_Id = c.Int(),
    })
  .PrimaryKey(t => t.Id)
  .ForeignKey("Casino.Casinos", t => t.Casino_Id)
  .Index(t => t.Casino_Id);

Anbieter übersetzen diese API-Aufrufe dann in datenbankspezifisches SQL.

Es gibt Methoden zum Erstellen von Tabellen und Indizes, zum Erstellen und Ändern von Eigenschaften, zum Ablegen von Objekten und vieles mehr. Es handelt es sich um eine sehr komplexe API, wie Sie anhand der aufgeführten Möglichkeiten aus Abbildung 5 ersehen können, einschließlich der Möglichkeit, SQL-Code auch direkt auszuführen. Für einige Ihrer Bedürfnisse mag diese API aber nicht komplex genug sein. So fehlt beispielsweise eine Methode zum Erstellen von Datenbankansichten, zum Festlegen von Berechtigungen und vieles mehr.

DbMigrations Database Schema Operations
Abbildung 5: DbMigrations-Datenbankschema-Operationen

Wieder einmal kam auch hier die Rettung aus der Community. In EF6 haben Sie nun die Möglichkeit, benutzerdefinierte Migrationsoperationen zu erstellen, die Sie durch das Anpassen von durch „add-migration“ generierten Migrationsklassen aufrufen können. Dies haben wir einem anderen Entwickler aus der CodePlex-Community zu verdanken: Iñaki Elcoro, aka iceclow.

Zum Erstellen einer eigenen Operation sind einige Schritte auszuführen. Ich zeige Ihnen das Wesentliche eines jeden Schritts. Der Download dieses Artikels enthält den vollständigen Code und zeigt auf, wie die Schritte organisiert sind.

  • Definieren Sie die Operation. Ich habe hier, wie in Abbildung 6 aufgeführt, eine „CreateViewOperation“ definiert.
  • Erstellen Sie eine Erweiterungsmethode, um auf die Operation zu zeigen. Dadurch wird der Aufruf über „DbMigration“ einfach:
public static void CreateView(this DbMigration migration,
  string viewName, string viewqueryString)
{
  ((IDbMigration) migration)
    .AddOperation(new CreateViewOperation(viewName,
       viewqueryString));
}
  • Definieren Sie den SQL-Code für die Operation in der Generate-Methode einer benutzerdefinierten SqlServerMigrationSqlGenerator-Klasse, wie in Abbildung 7 gezeigt.
  • Weisen Sie die DbConfiguration-Klasse an, die benutzerdefinierte SqlServerMigrationSqlGenerator-Klasse zu verwenden:
SetMigrationSqlGenerator("System.Data.SqlClient",
  () => new CustomSqlServerMigrationSqlGenerator());

Abbildung 6: Benutzerdefinierte Migrationsoperation zum Erstellen einer Datenbankansicht

public class CreateViewOperation : MigrationOperation
{
  public CreateViewOperation(string viewName, string viewQueryString)
    : base(null)
  {
    ViewName = viewName;
    ViewString = viewQueryString;
  }
  public string ViewName { get; private set; }
  public string ViewString { get; private set; }
  public override bool IsDestructiveChange
  {
    get { return false; }
  }
}

Abbildung 7: Benutzerdefinierte SqlServerMigrationSqlGenerator-Klasse

public class CustomSqlServerMigrationSqlGenerator
  : SqlServerMigrationSqlGenerator
{
  protected override void Generate(MigrationOperation migrationOperation)
  {
    var operation = migrationOperation as CreateViewOperation;
    if (operation != null)
    {
      using (IndentedTextWriter writer = Writer())
      {
        writer.WriteLine("CREATE VIEW {0} AS {1} ; ",
                          operation.ViewName,
                          operation.ViewString);
        Statement(writer);
      }
    }
  }
}

Sie können jetzt die neue Operation in einer Migrationsdatei verwenden, und „Update-Database“ weiß, was zu tun ist. Abbildung 8 zeigt die Verwendung der CreateView-Operation und erinnert daran, dass Sie auch eine Operation zum Entfernen der Ansicht erstellen müssen, die von der Down-Methode aufgerufen wird, wenn Sie diese Migration entladen möchten.

Abbildung 8: Verwenden der neuen CreateView-Operation

public partial class AddView : DbMigration
  {
    public override void Up()
    {
      this.CreateView("dbo.CasinosWithOver100SlotMachines",
                      @"SELECT  *
                        FROM    Casino.Casinos
                        WHERE  Id IN  (SELECT   CasinoId AS Id
                        FROM     Casino.SlotMachines
                        GROUP BY CasinoId
                        HAVING COUNT(CasinoId)>=100)");
    }
    public override void Down()
    {
      this.RemoveView("dbo.CasinosWithOver100SlotMachines");
    }
  }

Nachdem ich „Update-Database“ aufgerufen habe, können Sie die neue Ansicht in meiner Datenbank in Abbildung 9 sehen.

The Newly Created View Generated by Update-Database
Abbildung 9: Die neue von „Update-Database“ erstellte Ansicht

Die Entwicklung von Code First geht weiter

Nach der Bereitstellung der wesentlichen Features von Code First haben Microsoft und einige Entwickler aus der Community die Gelegenheit genutzt, EF6 mit Features zu optimieren, die nun flexibler eingesetzt werden können. Dies wird auch nach der Veröffentlichung von EF6 nicht aufhören. Wenn Sie die CodePlex-Arbeitsaufgaben nach Versionen höher als 6.0.1 filtern (unter bit.ly/1dA0LZf), sehen Sie, dass noch weitere Verbesserungen an den zukünftigen Versionen von EF6 und Code First vorgenommen werden. Diese Elemente befinden sich in unterschiedlichen Zuständen. Möglicherweise möchten auch Sie an einem arbeiten.

Julie Lerman ist Microsoft MVP, .NET-Mentor und Unternehmensberaterin und lebt in den Bergen von Vermont. Sie hält weltweit in Benutzergruppen und bei Konferenzen Vorträge zum Thema „Datenzugriff“ und zu anderen Microsoft .NET-Themen. Julie Lerman führt unter thedatafarm.com/blog einen Blog. Sie ist die Autorin von „Programming Entity Framework“ (2010) sowie der Ausgaben „Code First“ (2011) und „DbContext“ (2012). Alle Ausgaben sind im Verlag O’Reilly Media erschienen. Folgen Sie ihr auf Twitter unter twitter.com/julielerman, und besuchen Sie ihre Pluralsight-Kurse unter juliel.me/PS-Videos.

Unser Dank gilt dem folgenden technischen Experten für die Durchsicht dieses Artikels: Rowan Miller (Microsoft)