Datenpunkte

Fehlerquellen und Zeiger für eine Basisklasse zum Protokollieren in Entity Framework-Modellen

Julie Lerman

 

Julie LermanVor Kurzem bat mich ein Kunde um Unterstützung, da ab und an große Leistungsprobleme bei seinem Entity Framework-bezogenem Code auftraten. Mithilfe eines Tools zur Profilerstellung für die von Entity Framework generierten Abfragen entdeckten wir eine 5.800 Zeilen umfassende SQL-Abfrage an die Datenbank. (Weitere Informationen über Tools zur Profilerstellung finden Sie in meiner Kolumne zu Datenpunkten vom Dezember 2010, „Profilerstellung für Datenbankaktivitäten im Entity Framework“, unter msdn.microsoft.com/magazine/gg490349.) Ich erschrak, als ich im EDMX-Modell eine Vererbungshierarchie sah, von der ich allen Freunden und Entwicklern abgeraten hatte. Das Modell enthielt eine einzelne Basisentität, von der alle anderen Entitäten abgeleitet wurden. Durch die Basisentität wurde sichergestellt, dass jede Entität Eigenschaften zum Erfassen von Protokolldaten enthielt, zum Beispiel DateCreated und DateLastModified. Dieses Modell wurde mit Model First erstellt. Die Vererbung wurde daher von Entity Framework als ein Tabelle pro Typ (TPT)-Modell interpretiert, in dem jede Entität ihrer eigenen Tabelle in der Datenbank zugeordnet wird. Das sieht auf den ersten Blick ganz normal aus.

Aber in Entity Framework ist die TPT-Vererbung für ihr allgemeines Muster zur Abfrageerstellung berüchtigt, das zu erweiterten SQL-Abfragen mit schlechter Leistung führen kann. Möglicherweise starten Sie mit einem neuen Modell, bei dem Sie TPT vermeiden können, oder Sie müssen bei einem vorhandenen Modell und der TPT-Vererbung bleiben. In jedem Fall ist das Ziel der Kolumne diesen Monat, die potenziellen Leistungsfehlerquellen von TPT in diesem Szenario zu verdeutlichen. Ich möchte Ihnen außerdem zeigen, wie Sie die Fehlerquellen vermeiden können, wenn es noch nicht zu spät ist, das Modell und das Datenbankschema zu ändern.

Das problematische Modell

Abbildung 1 zeigt ein Beispiel für ein Modell, das ich viel zu häufig gesehen habe. Es enthält eine Entität mit Namen TheBaseType. Alle anderen Entitäten werden von dieser abgeleitet, um automatisch eine DateCreated-Eigenschaft zu erben. Es ist verlockend, diese Entwurfsmethode zu wählen. Aber die Entity Framework-Schemaregeln verlangen auch, dass der Basistyp die Schlüsseleigenschaft von jeder abgeleiteten Entität besitzt. Für mich ist das bereits ein Warnsignal, das auf eine nicht ordnungsgemäße Verwendung der Vererbung hindeutet.

All Classes Inherit from TheBaseType
Abbildung 1: Alle Klassen erben von TheBaseType

Es ist nicht so, dass speziell Entity Framework ein Problem mit diesem Entwurf aufweist, vielmehr handelt es sich um einen Entwurfsfehler im Modell selbst. In diesem Fall bestimmt die Vererbung, dass die Customer-Entität eine TheBaseType-Entität ist. Was würde passieren, wenn wir den Namen dieser Basisentität in „LoggingInfo“ änderten und dann die Anweisung „Customer is a LoggingInfo“ wiederholten? Der Irrtum bei der Anweisung wird mit dem neuen Klassennamen deutlicher. Stellen Sie einen Vergleich mit „Customer is a Person“ an. Vielleicht habe ich Sie jetzt davon überzeugt, dies in Modellen zu vermeiden. Falls das nicht so ist oder Sie bereits an diesem Modell festhalten müssen, fahren wir fort.

Der Model First-Workflow definiert standardmäßig ein Datenbankschema mit 1:1-Beziehungen zwischen der Basistabelle und allen Tabellen, die die abgeleiteten Typen darstellen. Dies ist die bereits erwähnte TPT-Hierarchie.

Wenn Sie ein paar einfache Abfragen ausführen, fällt Ihnen vielleicht gar kein Problem auf. Das gilt insbesondere, wenn Sie – wie ich – kein Datenbankadministrator oder eine andere Art Datenbankguru sind.

Diese LINQ to Entities-Abfrage ruft zum Beispiel die DateCreated-Eigenschaft für einen bestimmten Kunden ab:

context.TheBaseTypes.OfType<Customer>()
  .Where(b => b.Id == 3)
  .Select(c => c.DateCreated)
  .FirstOrDefault();

Das Ergebnis der Abfrage ist der folgende TSQL-Code, der in der Datenbank ausgeführt wird:

    SELECT TOP (1)
    [Extent1].[DateCreated] AS [DateCreated]
    FROM  [dbo].[TheBaseTypes] AS [Extent1]
    INNER JOIN [dbo].[TheBaseTypes_Customer] 
    AS [Extent2] ON [Extent1].[Id] = [Extent2].[Id]
    WHERE 3 = [Extent1].[Id]

Die Abfrage ist vollkommen korrekt.

Eine Abfrage einer vollständigen Entität ist ein wenig komplexer, da eine geschachtelte Abfrage ausgeführt werden muss. Die Basisabfrage ruft alle Felder ab, die die Verbindung zwischen der TheBaseTypes-Tabelle und der Tabelle mit dem abgeleiteten Typ darstellen. Eine Abfrage dieser Ergebnisse überträgt anschließend die Felder, die zurückgegeben werden sollen, an Entity Framework, um den Typ aufzufüllen. Ein Beispiel für eine Abfrage, um ein einzelnes Produkt abzurufen:

 

context.TheBaseTypes.OfType<Product>().FirstOrDefault();

Abbildung 2 zeigt den TSQL-Code, der auf dem Server ausgeführt wird.

Abbildung 2: Teillisting einer geschachtelten TSQL-Abfrage beim Bestimmen eines Typs

    SELECT
    [Limit1].[Id] AS [Id],
    [Limit1].[C1] AS [C1],
    [Limit1].[DateCreated] AS [DateCreated],
    [Limit1].[ProductID] AS [ProductID],
    [Limit1].[Name] AS [Name],
    [...continued list of fields required for Product class...]
    FROM ( SELECT TOP (1)
          [Extent1].[Id] AS [Id],
          [Extent1].[DateCreated] AS [DateCreated],
          [Extent2].[ProductID] AS [ProductID],
          [Extent2].[Name] AS [Name],
          [Extent2].[ProductNumber] AS [ProductNumber],
          [...continued list of fields from Products table aka "Extent2" ...],
          [Extent2].[ProductPhoto_Id] AS [ProductPhoto_Id],
          '0X0X' AS [C1]
          FROM  [dbo].[TheBaseTypes] AS [Extent1]
          INNER JOIN [dbo].[TheBaseTypes_Product] 
          AS [Extent2] ON [Extent1].[Id] = [Extent2].[Id]
    )  AS [Limit1]

Immer noch eine ganz passable Abfrage. Wenn Sie eine Profilerstellung für Ihre Abfragen bis zu diesem Punkt ausführen, stellen Sie vielleicht keine durch den Modellentwurf verursachten Probleme fest.

Aber was ist mit der nächsten „einfachen“ Abfrage, die alle heute erstellen Objekte unabhängig vom Typ ermitteln soll? Die Abfrage könnte Customers, Products, Orders, Employees oder jeden beliebigen anderen Typ im Modell zurückgeben, der von der Basis abgeleitet wird. Aufgrund des Modellentwurfs scheint dies eine sinnvolle Anforderung zu sein, deren Ausdruck durch das Modell in Kombination mit LINQ to Entities erleichtert wird (DateCreated ist als Datentyp in der Datenbank gespeichert, sodass ich in meinen Beispielabfragen keinen Vergleich mit DateTime-Feldern berücksichtigen muss):

var today= DateTime.Now.Date;
context.TheBaseTypes
  .Where(b => b.DateCreated == today)  .ToList();

Der Abfrageausdruck in LINQ to Entities ist kurz und einfach. Aber lassen Sie sich davon nicht täuschen. Die Anforderung hat es in sich. Entity Framework und Ihre Datenbank müssen Instanzen beliebigen Typs, sei es Customer oder Product oder Employee, zurückgeben, die heute erstellt wurden. Zuerst muss Entity Framework jede Tabelle abfragen, die den abgeleiteten Entitäten zugeordnet ist, und jede mit der einzelnen, zugehörigen TheBaseTypes-Tabelle mit dem DateCreated-Feld verknüpfen. In meiner Umgebung wird hierzu eine 3.200-Zeilen-Abfrage erstellt (bei guter Formatierung der Abfrage durch EFProfiler). Das Erstellen dieser Abfrage durch Entity Framework und ebenso das Ausführen durch die Datenbank kann einige Zeit dauern.

Nach meiner Erfahrung ist für eine solche Abfrage ohnehin ein Unternehmenstool zur Analyse zu verwenden. Aber nehmen wir an, Sie haben das Modell und möchten diese Informationen aus Ihrer Anwendung erhalten, zum Beispiel für einen administrativen Berichterstellungsbereich in einer Anwendung, die Sie erstellen. Ich habe beobachtet, wie Entwickler versucht haben, diesen Typ Abfrage in ihre Anwendungen zu implementieren, und ich rate immer noch zu einer Lösung außerhalb von Entity Framework. Erstellen Sie die Logik in der Datenbank als Ansicht oder gespeicherte Prozedur, und rufen Sie diese aus Entity Framework auf. So muss nicht Entity Framework die Abfrage für Sie erstellen. Sogar als Datenbankprozedur ist diese spezielle Logik nicht einfach zu erstellen. Es hat aber Vorteile. Erstens erstellen Sie wahrscheinlich eine Abfrage mit besserer Leistung. Zweiten benötigt Entity Framework keine Zeit, um die Abfrage zu verarbeiten. Drittens muss Ihre Anwendung keine Abfrage mit 3.300 oder mehr Zeilen senden. Allerdings besteht das Problem weniger in Entity Framework als dem gesamten Modell, das Ihnen in den Weg gerät. Das wird immer deutlicher, je mehr Sie sich mit dem Problem befassen und versuchen, es innerhalb der Datenbank oder mithilfe von Entity Framework und .NET-Codierungslogik zu lösen.

Wenn Sie Abfragen vom Basistyp und abfragespezifische Typen vermeiden können, sind Ihre Abfragen viel einfacher. Im folgenden Beispiel wird die vorherige Abfrage für einen bestimmten Typ ausgedrückt:

context.TheBaseTypes.TypeOf<Product>()
  .Where(b => b.DateCreated == today)
  .ToList();

Das TSQL-Ergebnis ist eine einfache 25-Zeilen-Abfrage, da Entity Framework nicht jeden Typ im Modell abfragen muss. Mit der DbContext-API müssen Sie nicht einmal TypeOf verwenden, um abgeleitete Typen abzufragen. Sie können DbSet-Eigenschaften für die abgeleiteten Typen erstellen. Ich könnte die Abfrage daher sogar noch einfacher machen:

context.Products
  .Where(b => b.DateCreated == today)
  .ToList();

Für dieses Modell würde ich tatsächlich das TheBaseTypes-DbSet vollständig aus meiner Context-Klasse entfernen und jeden an direkten Abfragen dieses Basistyps hindern.

Protokollieren ohne das problematische Modell

Bisher habe ich ein Hierarchieszenario behandelt, von dem ich Entwicklern bei der Erstellung von Modellen mit Entity Framework dringend abrate: die Verwendung einer zugeordneten Entität als Basis, von der auch jede einzelne Entität im Modell abgeleitet wird. Manchmal finde ich Szenarios vor, wo es einfach zu spät ist, das Modell zu ändern. Aber in anderen Fällen kann ich den Kunden helfen, diesen Weg vollständig zu vermeiden, oder die Entwicklung steht noch am Anfang, sodass wir das Modell ändern können.

Das Ziel ist, häufig verfolgte Daten wie beispielsweise Protokollierungsdaten bereitzustellen, für alle Typen im Modell. Wie lässt sich das besser erreichen?

Die erste Überlegung ist häufig, die Vererbung beizubehalten, aber den Hierarchietyp zu ändern. In Model First ist TPT der Standard. Sie können diesen aber mit Entity Designer Generation Power Pack (in Visual Studio Gallery über den Erweiterungs-Manager verfügbar) in Tabelle pro Hierarchie (TPH) ändern. Code First verwendet standardmäßig TPH, wenn Sie Vererbung in den Klassen definieren. Sie werden aber schnell feststellen, dass dies überhaupt keine Lösung ist. Warum ist das so? TPH bedeutet, dass die vollständige Hierarchie in einer einzelnen Tabelle enthalten ist. Mit anderen Worten, Ihre Datenbank würde aus nur einer Tabelle bestehen. Ich glaube, ich muss Sie nicht durch weitere Erklärungen überzeugen, dass dies nicht der beste Weg ist.

Bei der Frage, ob Customer wirklich ein Typ von LoggingInfo ist, habe ich es bereits erwähnt: Für das bestimmte Szenario, auf das ich mich konzentriert habe – das Problem der Verfolgung von häufigen Daten zu lösen – kommen wir durch die Vermeidung von Vererbung am besten zum Ziel. Ich empfehle, stattdessen die Verwendung einer Schnittstelle oder von komplexen Typen zu prüfen, um die Felder in die einzelnen Tabellen einzubetten. Wenn Sie bereits die Datenbank beibehalten müssen, die eine getrennte Tabelle erstellt hat, verwenden Sie eine Beziehung.

Um dies zu zeigen, wechsele ich zu einem Modell auf Basis von Klassen, die Code First anstelle von EDMX verwenden. Sie können die gleichen Muster aber auch mit einem EDMX-Modell und dem Designer erzielen.

Im ersten Fall verwende ich eine Schnittstelle:

public interface ITheBaseType
{
  DateTime DateCreated { get; set; }
}

Jede Klasse implementiert die Schnittstelle. Die Klasse hat ihre eigene Schlüsseleigenschaft und umfasst eine DateCreated-Eigenschaft. Zum Beispiel die Product-Klasse:

public class Product : ITheBaseType
{
  public int ProductID { get; set; }
  // ...other properties...
  public DateTime DateCreated { get; set; }
}

In der Datenbank hat jede Tabelle ihre eigene DateCreated-Eigenschaft. Wenn ich die vorige Abfrage für Product wiederhole, wird daher eine unkomplizierte Abfrage erstellt:

context.Products
.Where(b => b.DateCreated == today)
.ToList();

Da alle Felder in dieser Tabelle enthalten sind, benötige ich keine geschachtelte Abfrage mehr:

    SELECT TOP (1) [Extent1].[Id]                     AS [Id],
                   [Extent1].[ProductID]              AS [ProductID],
                   [Extent1].[Name]                   AS [Name],
                   [Extent1].[ProductNumber]          AS [ProductNumber],
                   ...more fields from Products table...
                   [Extent1].[ProductPhoto_Id]        AS [ProductPhoto_Id],
                   [Extent1].[DateCreated]            AS [DateCreated]
    FROM   [dbo].[Products] AS [Extent1]
    WHERE  [Extent1].[DateCreated] = '2012-05-25T00:00:00.00'

Wenn Sie lieber einen komplexen Typ definieren und diesen in jeder der Klassen wiederverwenden möchten, können die Typen zum Beispiel folgendermaßen aussehen:

public class Logging
{
  public DateTime DateCreated { get; set; }
}
public class Product{
  public int ProductID { get; set; }
  // ...other properties...
  public Logging Logging { get; set; } 
}

Die Logging-Klasse hat kein Schlüsselfeld (wie Id oder LoggingId). Die Code First-Konventionen unterstellen hier einen komplexen Typ, und bei einer Verwendung zur Definition von Eigenschaften in anderen Klassen, wie ich es mit Product getan habe, erfolgt eine entsprechende Verarbeitung.

Die Products-Tabelle in der Datenbank enthält eine von Code First erstellte Spalte namens Logging_DateCreated, und die Product.Logging.DateCreated-Eigenschaft wird dieser Spalte zugeordnet. Das Hinzufügen der Logging-Eigenschaft zur Customer-Klasse würde dieselbe Auswirkung haben. Die Customers-Tabelle hat ebenfalls ihre eigene Logging_DateCreated-Eigenschaft, die wieder zu Customer.Logging.DateCreated zugeordnet wird.

Um dieses DateCreated-Feld zu referenzieren, ist es im Code erforderlich, dass Sie durch die Logging-Eigenschaft navigieren. Hier habe ich dieselbe Abfrage wie vorher neu geschrieben, damit sie mit den neuen Typen funktioniert:

context.Products.Where(b => b.Logging.DateCreated == DateTime.Now).ToList();

Der resultierende SQL-Code stimmt mit dem Schnittstellenbeispiel bis auf den Feldnamen überein. Statt DateCreated lautet der Feldname jetzt Logging_DateCreated. Es ist eine kurze Abfrage, die nur die Products-Tabelle abfragt.

Einer der Vorteile der Vererbung von der Klasse im Originalmodell ist, dass es einfach ist, Logik zum Auffüllen der Felder aus der Basisklasse zu schreiben, zum Beispiel während SaveChanges. Für den komplexen Typ oder die Schnittstelle können Sie aber ebenso einfach Logik erstellen, sodass ich für diese neuen Muster keine Nachteile sehe. Abbildung 3 zeigt ein einfaches Beispiel dafür, wie die DateCreated-Eigenschaft während SaveChanges für neue Entitäten festgelegt wird. Weitere Informationen zu dieser Technik finden Sie in der zweiten Ausgabe und der DbContext-Ausgabe meiner Buchreihe „Programming Entity Framework“.

Abbildung 3: Festlegen der DateCreated-Eigenschaft der Schnittstelle während SaveChanges

public override int SaveChanges()
{
  foreach (var entity in this.ChangeTracker.Entries()
    .Where(e =>
    e.State == EntityState.Added))
  {
    ApplyLoggingData(entity);
  }
  return base.SaveChanges();
}
private static void ApplyLoggingData(DbEntityEntry entityEntry)
{
  var logger = entityEntry.Entity as ITheBaseType;
  if (logger == null) return;
  logger.DateCreated = System.DateTime.Now;
}

Einige Änderungen in Entity Framework 5

Entity Framework 5 enthält für Abfragen, die von TPT-Hierarchien generiert werden, einige Verbesserungen. Diese sind hilfreich, verringern aber nicht die erwähnten Probleme. Nehmen wir zum Beispiel die Abfrage vom Anfang, die zu 3.300 Zeilen SQL auf einem Computer mit einer Microsoft .NET Framework 4.5-Installation (ohne weitere Änderungen an der Lösung) führte. Wenn ich diese Abfrage erneut ausführe, wird eine Query generiert, die auf 2.100 Zeilen SQL reduziert wurde. Zu den größten Unterschieden gehört, dass für Entity Framework 5 zum Erstellen der Abfrage keine UNIONs erforderlich sind. Ich bin kein Datenbankadministrator, aber meines Wissens wirkt sich eine solche Verbesserung nicht auf die Leistung der Abfrage in der Datenbank aus. Weitere Informationen über diese Änderung an TPT-Abfragen finden Sie in meinem Blogbeitrag „Entity Framework CTP für Juni 2011: Abfrageverbesserungen bei der TPT-Vererbung“ unter bit.ly/MDSQuB.

Nicht jede Vererbung ist schlecht

Ein einzelner Basistyp für alle Entitäten im Modell ist ein extremes Beispiel für fehlgeschlagene Modellierung und Vererbung. Es gibt viele sinnvolle Anwendungsfälle für eine Vererbungshierarchie im Modell, zum Beispiel, wenn Sie „Customer is a Person“ beschreiben möchten. Es ist auch wichtig, dass LINQ to Entities nur eines der verfügbaren Tools ist. In dem Szenario, das mein Kunde mir zeigte, hatte ein raffinierter Datenbankentwickler die Abfrage für die Basistypenfelder neu als datenbankgespeicherte Prozedur konstruiert. Diese reduzierte eine Aktivität von mehreren Sekunden auf eine, die nur 9 Millisekunden dauerte. Wir alle applaudierten ihm. Dennoch hoffen wir weiterhin, dass sie für die nächste Version der Software das Model neu entwerfen und eine Feineinstellung der Datenbank vornehmen können. In der Zwischenzeit kann der Kunde mit Entity Framework weiterhin die unproblematischen Abfragen generieren und außerdem die von mir zur Verfügung gestellten Tools verwenden, um Anwendung und Datenbank zu optimieren und einige hervorragende Leistungsverbesserungen zu erzielen.

Julie Lerman ist Microsoft MVP, .NET-Mentor und Unternehmensberaterin und lebt in den Bergen von Vermont. Sie hält bei User Groups und Konferenzen in der ganzen Welt Vorträge zum Thema Datenzugriff und anderen Microsoft .NET-Themen. Julie Lerman führt unter thedatafarm.com/blog einen Blog. Sie ist die Verfasserin 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.

Unser Dank gilt dem folgenden technischen Experten für die Durchsicht dieses Artikels: Diego Vega