10. Grundlagen

In diesem Abschnitt soll es vor allem um architektonische und konzeptionelle Aspekte rund um das Entity Framework gehen.

Primär geht es darum, modellbasiert zu entwickeln, nicht Datenbankschema-basiert. Mit dem Einsatz von ADO.NET verwenden Sie viel Zeit auf die Abfrage von Daten aus einer Datenbank: das Durchlaufen der Ergebnisse und Herausfiltern der relevanten Felder sowie deren Inhalte. Mit dem Entity Framework ist das anders. Hier erfolgt die Abfrage nicht gegen ein Datenbankschema. Sie entwickeln Ihre Software gegen ein der Geschäftslogik entsprechendes konzeptionelles Objektmodell.

Die Daten aus der Datenbank werden automatisch in die Geschäftsobjekte übertragen. Änderungen werden gespeichert, indem die geänderten Objekte gespeichert werden, das Entity Framework erledigt den Rest automatisch. So werden Sie als Entwickler von der Aufgabe “befreit”, die Eigenschaften der Geschäftsobjekte in Reihen und Spalten der Datenbank zu konvertieren.

10.1 Entitäten

Der wichtigste Bestandteil des Entity Frameworks sind Entitäten. Es ist der Kern, um den sich die weitere Funktionalität anordnet. Das Entitäten-Modell beschreibt die Geschäftsobjekte, wie sie von der Geschäftsprogrammlogik verwendet werden sollen. Es gehört, im Gegensatz zu dem logischen Datenmodell, nicht zur verwendeten Datenbank.

Wichtige Begriffe im Zusammenhang mit dem Entity Framework sind:

  • Entity Set
    Eine Sammlung zusammengehörender Entities, denen ein Entity Key gemeinsam ist. Das bildet eine Tabelle im relationalen Modell ab.
  • Entity Container
    Gruppierung aus Entity Set, Beziehungen und Funktionen. Dies muss mindestens einmal im Modell vorhanden sein und bildet eine Datenbank im relationalen Modell ab. Es wird auch als Kontext (Context) bezeichnet.
  • Association Type
    Eine Beziehung von einem Entity Typy zu einem anderen. Die Abbildung erfolgt durch eine Eigenschaft (“Navigation Property” genannt). Sie hat eine Multiplizität: 1, 0..1, * und bildet Relationen im relationalen Modell ab.
  • Datentypen
    Dies ist die Abbildung des relationalen Modells auf Typen des Objektmodells. Es werden viele .NET-Datentypen wie Boolean, String, Int32, Guid, Byte[] und DateTime direkt abgebildet. Komplexere Typen werden durch Klassen abgebildet.
Abbildung: Schematische Darstellung eines Entity Containers
Abbildung: Schematische Darstellung eines Entity Containers

10.2 Bausteine

Die folgende Abbildung zeigt, wie sich das Entity Framework in die Applikation einordnet.

Abbildung: Einordnung des Entity Frameworks in die Applikation
Abbildung: Einordnung des Entity Frameworks in die Applikation

Taucht man tiefer ein, so werden die einzelnen Dienste sichtbar.

Abbildung: Innerer Aufbau des Entity Frameworks
Abbildung: Innerer Aufbau des Entity Frameworks

Wichtig ist der Umgang mit den bereits erwähnten Schnittstellen IEnumerable<T> und IQueryable<T>.

IEnumerable<T>

Diese Schnittstelle unterstützt Iterationen, also speziell den Zugriff mittels foreach. Basis ist die Methode GetEnumerator, die einen Enumerator mit den folgenden Mitgliedern zurückgibt:

  • Reset
  • MoveNext
  • Current<T>

Objekte von diesem Type sind immer Speicherobjekte. Man spricht im Entity Framework davon, dass diese “materialisiert” sind. Abfragen auf solchen Objekten werden nunmehr im Speicher und nicht mehr auf der Datenbank ausgeführt.

IQueryable<T>

Diese Schnittstelle liefert IEnumerable<T> und zusätzlich folgende Eigenschaften:

  • Type vom Typ ElementType
  • Expression vom Typ Expression
  • IQueryProvider vom Typ Provider

Abfragen stehen in Expression und werden vom Provider ausgeführt. Damit ist dies der Schlüssel, um die Abfrage weiter zu verfeinern (Expression wird erweitert) und dann auf der Datenbank ausgeführt zu werden.

Soll die Abfrage auf der Datenbank ausgeführt und dann materialisiert werden, wird folgendes geschrieben:

1 MyDataSource.Where(d => d.Name.Contains("Test").ToList()

Soll die Datenbank komplett abgerufen und dann materialisiert und die Abfrage danach im Speicher ausgeführt werden, wird folgendes geschrieben:

1 MyDataSource.ToList().Where(d => d.Name.Contains("Test")

Der Aufruf von ToList() erzwingt die Materialisierung und gibt (unter anderem) IEnumerable<T>zurück.

Abbildung: Vergleich IEnumerable und IQueryable
Abbildung: Vergleich IEnumerable und IQueryable

10.3 Kontext

Der Kontext ist eine zusätzliche Schicht, welche mit Hilfe des Entity Readers C#-Objekte zur Verfügung stellt. Die Dienste des Entity Frameworks unterstützen Entity SQL oder LINQ-To-Entities durch Implementierung der entsprechenden Schnittstellen mittels Providern. Die Provider sorgen dafür, dass datenbankunabhängig gearbeitet wird.

Die Klasse DbContext ist der zentrale Bestandteil der Dienste. Diese Klasse stellt Funktionen für die Interaktion mit CLR-Objekten zur Verfügung. Abfragen werden in Form eines Ausdrucksbaums (Befehlsstruktur) gesendet. Als Rückgabewert kommt ein Auszählungstyp der Schnittstelle IQueryable<T> zurück.

Die Abfragesprache Entity-SQL ist eine eigens für das Entity Framework entwickelte Abfragesprache, welche T-SQL sehr ähnlich ist. Sie erlaubt SQL-Anweisungen die vor allem dann nützlich sind, wenn der Zugriff mit LINQ nicht gelingt oder Funktionen erreicht werden sollen, die der LINQ-Provider nicht kennt. Entity-SQL stellt eine generische, von der verwendeten Datenquelle unabhängige, abstrakte Abfrage zur Verfügung. Ausgeführt wird diese Abfrage unter Verwendung von datenquellenspezifischen Abfrageprovidern, beispielsweise ADO.NET. Zwar kann EntitySQL direkt verwendet werden, jedoch ist es oft leichter den LINQ-Provider zu verwenden, weil LINQ für Abfragen auf Objekten und Strukturen optimiert wurde.

Dieses Verfahren wird dann LINQ-To-Entities genannt. Der Datenclient LINQ-To-Entities greift direkt auf die clientseitigen Komponenten des Entity Frameworks zurück. Dabei wird der Objektdienst als LINQ-Provider verwendet. Die LINQ-Abfrage wird in einen Ausdrucksbaum gewandelt, welcher an den Entity-Datenprovider übergeben wird. Hier erfolgt eine weitere Anpassung in einen ADO.NET kompatiblen Ausdrucksbaum. Der ADO.NET-Provider erzeugt das entsprechende SQL für die Abfrage und liefert das Ergebnis an den Provider. Im letzten Schritt wandelt der LINQ-Provider die im EntityReader enthaltenen Informationen in Objekte, auf die über die Schnittstelle IQueryable<T> zugegriffen werden kann.

10.4 Eigenschaften

Entity Framework hat einige grundlegend Eigenschaften, die bei jedem Zugriff berücksichtigt werden sollten.

Lazy Loading

Lazy Loading heißt, das Daten nur dann geladen werden, wenn Sie auch gebraucht werden. In der aktuellen Version unterstützt Entity Framework dieses Verfahren nicht.

Eager Loading

Hier wird das Laden explizit gesteuert. Nutzen Sie folgenden Aufruf:

1 var usersWithClaim = Users.Include(u => u.Claims);

Weitere Ladevorgänge werden mit .ThenInclude ausgeführt.

Explicit Loading

Hier wird der gesamte Objektgraph geladen:

1 context.Entry(Person)       
2        .Reference(p => p.Bookings)       
3        .Load()

Smart Loading

Hier erfolgt kein Laden, wenn nur eine Aggregation auf den Objekten erfolgt:

1 context.Entry(Person)
2        .Collection(p => p.Bookings)
3        .Query()      
4        .Count()

Dies lässt sich mit dem expliziten Laden kombinieren:

1 context.Entry(sess)
2        .Collection(b => b.Speakers)
3        .Query()
4        .Where(p => p.Name.Contains("EF")
5        .Load();

10.5 Umgang mit Multiplizität

Per Konvention werden Navigationseigenschaften (lies: Relationen) erstellt, wenn der Name auf “ID” endet. Eine explizite Deklaration ist mit dem Attribut [Key] möglich. Folgende Multiplizitäten gibt es:

  • 0..1: Eigenschaftentyp ist Entity Type
  • 1: Eigenschaftentyp ist Entity Type + [Required]
  • n: ICollection<Entity Type> + [Required]
  • 0..n: ICollection<Entity Type>

Assoziationen wirken sich auf das Ladeverhalten aus. Die “n”-Seite der Beziehungen wird durch drei Schnittstellen bestimmt:

  • IEnumerable<T>: Erlaubt nur Lesen, es gibt keine Änderungsverfolgung (Tracking) und kein Lazy Loading.
  • ICollection<T>: Mit Änderungsverfolgung (Add/Remove) und Lazy Loading
  • IList<T>: Zusätzlich mit Sortieren, Filtern und weiteren Abfragen mit LINQ to Objects

Fremdschlüssel können, wenn keine Navigationseigenschaften möglich sind, mit dem Attribut [ForeignKey] bestimmt:

1 public class Project { 
2 
3   public int ProjectId { get; set; } 
4   public string Name { get; set; }    
5   public int ManagerId { get; set; }    
6   
7   [ForeignKey("ManagerId")]    
8   public Person Manager { get; set; }
9 } 

10.6 Umgang mit dem Kontext

Die wichtigsten Funktionen sind:

  • SaveChanges: Speichern, auslösen des Datenbankzugriffs
  • ExecuteStoreQuery: Ausführen von Entity-SQL-Abfragen
  • Entry: Zugriff auf die Objekttabelle

SaveChanges

Mit SaveChanges erfolgt das Speichern, also das Auslösen des Datenbankzugriffs. Die Funktion gibt es synchron und asynchron als SaveChangesAsync. Wenn Sie global das Speicherverhalten anpassen möchten, überschreiben Sie die Methode und fügen eigene Aktionen davor ein. Insbesondere besteht hier Zugriff auf den ChangeTracker, der alle vom Entity Framework überwachten Objekte enthält. Damit lassen sich typische Aktionen steuern:

  • Setzen eines Löschkennzeichens für gelöschte Objekte statt physischem Löschen
  • Setzen von globalen Feldern wie CreatedBy, CreatedAt, ModifiedBy, ModifiedAt

Der Aufruf ist darüber hinaus atomar und kapselt alle anstehenden SQL-Abfragen in eine Transaktion. Sie müssen sich nur dann um Transaktionen kümmern, wenn die betroffenen Aktionen in mehrere Aufrufe von SaveChanges zerfallen.

ExecuteStoreQuery

Hiermit erfolgt das Ausführen von Entity-SQL-Abfragen. Die Funktion gibt es synchron und asynchron als ExecuteStoreQueryAsync. Entity-SQL-Abfragen ist ein plattformunabhängiger SQL-Dialekt, der es erlaubt, die Abfragestruktur selbst zu schreiben. Dies ist sinnvoll, wenn es Entity Framework nicht gelingt, eine brauchbare SQL-Abfrage zu erstellen.

Entry und DbSet/IDbSet

Mit Entry erfolgt der Zugriff auf die Objekttabelle der vom Tracker behandelten Objekte. Objekte in der Objekttabelle haben einen definierten Zustand (State, vom Typ EntityState), der darüber entscheidet, welche Art von SQL beim Speichern erzeugt wird.

Objekte lassen sich folgendermaßen der Objekttabelle hinzufügen und in einen bestimmten Zustand versetzen:

1 using (var context = new SessionContext())
2 {
3   var session = new Session { Name = "EF Core 1.0" };
4   context.Entry(session).State = EntityState.Added;   
5   context.SaveChanges();
6 }

Mit Hilfe dieses Verfahrens ist es möglich, generische Repositories aufzubauen, die nicht auf bestimmte DbSet-Methoden angewiesen sind. Der gezeigte Code-Block entspricht folgender Vorgehensweise:

1 using (var context = new SessionContext())
2 {
3   var session = new Session { Name = "EF Core 1.0" };
4   context.Sessions.Add(session);   
5   context.SaveChanges();
6 }

Die Eigenschaft Sessions ist hier im Kontext folgendermaßen definiert:

1 public DbSet<Session> Sessions { get; set; }

Weitere Methoden des Kontexts

Einige weitere Methoden werden nicht immer benötigt, helfen aber dabei, eine leistungsfähigere API für den Datenbankzugriff aufzubauen.

  • Attach
    Hiermit erfolgt das Anhängen eines vorher abgehängten Objekts. Dieses muss in der Datenbank bereits existieren, hat aber möglicherweise Tracking verloren, weil ein Webdienst oder ein ähnlicher Serialisierungsvorgang benutzt worden ist.
  • Find
    Hiermit erfolgt die Suche nach einem Primärschlüssel. Als Parameter wird auch ein Array akzeptiert, falls es sich um einen zusammengesetzten Primärschlüssel handelt.
  • Remove
    Hier erfolgt das Entfernen (DELETE) aus der Objekttabelle und damit das Löschen beim nächsten Speichervorgang. Dies setzt voraus, dass das Objekt vorher geladen wurde. Das können Sie sich sparen, wenn folgendermaßen vorgegangen wird:
1    Ctx.Entry(cust).State = EntityState.Deleted; 
  • Local
    Dies erzeugt eine ObservableCollection<T>, deren Änderungen Teil des Tracking sind und bei SaveChanges beachtet werden
  • Create
    Erzeugt einen Proxy (sehen Sie sich dazu den Abschnitt Proxies an). Der Befehl fügt das Objekt nicht sofort ein; dies würde ein weiteres Add/Attach erfordern.
  • Include
    Dient der Erweiterung des Objektgraphen zum Abruf folgender Stufen verbundener Objekte. Gesteuert wird damit das Eager Loading. Ohne diese Maßnahme wird immer nur eine Stufe geladen und dies auch nur dann, wenn dies mit dem Schlüsselwort virtual bei der Navigationseingenschaft angezeigt wird.
  • SqlQuery
    Erlaubt das direkte Abfragen mit SQL. Das Ergebnis wird Teil des Trackings.

Der Status einer Entität

Der Status steuert das Tracking – das Verfolgen von Änderungen an Proxy-Objekten. Er kann folgende Zustände haben:

  • Added
  • Unchanged
  • Modified
  • Deleted
  • Detached

Dies kann man nutzen, um mit Hilfe der bereits gezeigten Methode Entry eine Funktion zu entwickeln, die gleichzeitig Aktualisieren und Erstellen kann. Als Kriterium wird der Primärschüssel benutzt.

 1 public void InsertOrUpdate(Session session)
 2 {
 3   using (var context = new SessionContext())
 4   {
 5     context.Entry(session).State = session.Id == 0 ?
 6                                    EntityState.Added :
 7                                    EntityState.Modified;
 8  
 9     context.SaveChanges();
10   }
11 }