Rheinwerk Computing < openbook > Rheinwerk Computing - Professionelle Bücher. Auch für Einsteiger.
Professionelle Bücher. Auch für Einsteiger.

Inhaltsverzeichnis
Vorwort zur 6. Auflage
1 Allgemeine Einführung in .NET
2 Grundlagen der Sprache C#
3 Das Klassendesign
4 Vererbung, Polymorphie und Interfaces
5 Delegates und Ereignisse
6 Strukturen und Enumerationen
7 Fehlerbehandlung und Debugging
8 Auflistungsklassen (Collections)
9 Generics – Generische Datentypen
10 Weitere C#-Sprachfeatures
11 LINQ
12 Arbeiten mit Dateien und Streams
13 Binäre Serialisierung
14 XML
15 Multithreading und die Task Parallel Library (TPL)
16 Einige wichtige .NET-Klassen
17 Projektmanagement und Visual Studio 2012
18 Einführung in die WPF und XAML
19 WPF-Layout-Container
20 Fenster in der WPF
21 WPF-Steuerelemente
22 Elementbindungen
23 Konzepte von WPF
24 Datenbindung
25 Weitere Möglichkeiten der Datenbindung
26 Dependency Properties
27 Ereignisse in der WPF
28 WPF-Commands
29 Benutzerdefinierte Controls
30 2D-Grafik
31 ADO.NET – Verbindungsorientierte Objekte
32 ADO.NET – Das Command-Objekt
33 ADO.NET – Der SqlDataAdapter
34 ADO.NET – Daten im lokalen Speicher
35 ADO.NET – Aktualisieren der Datenbank
36 Stark typisierte DataSets
37 Einführung in das ADO.NET Entity Framework
38 Datenabfragen des Entity Data Models (EDM)
39 Entitätsaktualisierung und Zustandsverwaltung
40 Konflikte behandeln
41 Plain Old CLR Objects (POCOs)
Stichwort

Download:
- Beispiele, ca. 62,4 MB

Jetzt Buch bestellen
Ihre Meinung?

Spacer
Visual C# 2012 von Andreas Kühnel
Das umfassende Handbuch
Buch: Visual C# 2012

Visual C# 2012
Rheinwerk Computing
1402 S., 6., aktualisierte und erweiterte Auflage 2013, geb., mit DVD
49,90 Euro, ISBN 978-3-8362-1997-6
Pfeil 38 Datenabfragen des Entity Data Models (EDM)
Pfeil 38.1 Abfragen mit LINQ to Entities
Pfeil 38.1.1 Allgemeine Begriffe in LINQ
Pfeil 38.1.2 Einfache Abfragen
Pfeil 38.1.3 Navigieren in Abfragen
Pfeil 38.1.4 Aggregatmethoden
Pfeil 38.1.5 Joins in LINQ definieren
Pfeil 38.1.6 In Beziehung stehende Daten laden
Pfeil 38.2 Abfragen mit Entity SQL
Pfeil 38.2.1 Ein erstes Beispiel mit Entity SQL
Pfeil 38.2.2 Die fundamentalen Regeln der Entity-SQL-Syntax
Pfeil 38.2.3 Filtern mit Entity SQL
Pfeil 38.2.4 Parametrisierte Abfragen
Pfeil 38.3 Der EntityClient-Provider
Pfeil 38.3.1 Verbindungen mit »EntityConnection«
Pfeil 38.3.2 Die Klasse »EntityCommand«
Pfeil 38.4 Abfrage-Generator-Methoden (QueryBuilder-Methoden)
Pfeil 38.5 SQL-Direktabfragen

38 Datenabfragen des Entity Data Models (EDM)Zur nächsten Überschrift

Im letzten Kapitel haben Sie alles Wesentliche rund um das Konzept des Entity Frameworks erfahren. Sie kennen die Strukturen und auch alle wichtigen und grundlegenden Komponenten. In diesem Kapitel wollen wir uns mit den gegen das Entity Data Model (EDM) abgesetzten Datenabfragen beschäftigen und deren Möglichkeiten ausloten.

Ganz am Anfang sei vorab Folgendes klargestellt. Grundsätzlich stehen Ihnen dazu drei verschiedene Techniken zur Verfügung:

  • LINQ to Entities
  • Entity SQL
  • der EntityClient-Provider

Abfragen mit LINQ to Entities sind verhältnismäßig einfach zu formulieren, wenn man sich mit LINQ und den dazugehörigen Spracherweiterungen in C# oder VB.NET vertraut gemacht hat. LINQ kann überall dort eingesetzt werden, wo generell Datenmengen abgefragt, gefiltert oder sortiert werden sollen, und hat sich zu einem regelrechten Standard etabliert. In den meisten Fällen wird LINQ to Entities die erste Wahl sein.

Entity SQL wurde entwickelt, bevor LINQ offiziell das Licht der Welt erblickte. Dass das Entity Framework trotz Einführung von LINQ to Entities auch Entity SQL unterstützt, hat einen simplen Grund: Nicht alle .NET-Sprachen unterstützen LINQ, wie es C# tut. Entwickler, die auf eine .NET-fähige Sprache setzen, die LINQ nicht unterstützt, haben daher mit Entity SQL trotzdem die Möglichkeit, die Vorzüge des Entity Frameworks zu nutzen. Allerdings sei angemerkt, dass Entity SQL trotz der Ähnlichkeit zu SQL (bzw. T-SQL) nicht ganz einfach ist und ein Umdenken erforderlich macht.

Weniger bekannt und auch nicht häufig im praktischen Alltag ist der EntityClient-Provider. Dieser ähnelt in vielfacher Hinsicht den ADO.NET-Providern SqlClient oder OracleClient. Der wesentliche Unterschied zwischen LINQ to Entities und dem EntityClient-Provider besteht darin, wie die Daten dem Client zur Verfügung gestellt werden. Während LINQ to Entities Objekte »materialisiert«, ist das Resultat von EntityClient eine Ansammlung von Spalten und Zeilen, ähnlich dem der anderen ADO.NET-Provider.

Ich werde Ihnen in diesem Kapitel alle drei Techniken vorstellen. Dabei werden wir uns vorrangig mit LINQ to Entities beschäftigen, weil es zumindest beim Einsatz von C# eine herausragende Position einnimmt.


Rheinwerk Computing - Zum Seitenanfang

38.1 Abfragen mit LINQ to EntitiesZur nächsten ÜberschriftZur vorigen Überschrift


Rheinwerk Computing - Zum Seitenanfang

38.1.1 Allgemeine Begriffe in LINQZur nächsten ÜberschriftZur vorigen Überschrift

In LINQ werden Begriffe verwendet, die Sie am Anfang schon einmal gehört haben sollten, da wir sie in diesem Kapitel häufiger verwenden werden: Operatoren und Sequenzen.

  • Operatoren: Methoden, die speziell auf LINQ zugeschnitten sind, werden als Operatoren bezeichnet. Für LINQ gibt es über 100 Operatoren, die teilweise auch noch überladen sind. Mit Operatoren lassen sich Entitätsmengen (auch Sequenzen genannt, siehe weiter unten) filtern, sortieren und gruppieren. Die Möglichkeiten, die uns durch die Operatoren geboten werden, sollten eigentlich keine Wünsche mehr offenlassen – und wenn doch, können Sie natürlich auch eine eigene, spezifische LINQ-Methode schreiben. LINQ-Abfragen werden samt ihren Operatoren in ein SQL-Statement übersetzt, das von der Datenbank verstanden wird.
  • Sequenzen: Elementmengen, die sich im Speicher befinden, werden im Allgemeinen als Auflistungen bezeichnet. Im Zusammenhang mit LINQ wird auch der Begriff Sequenz benutzt. Während herkömmliche Auflistungen – wie erwähnt – immer vollständig im Speicher liegen, werden die Elemente einer Sequenz immer erst dann ermittelt, wenn sie benötigt werden. Eine Sequenz ist immer ein Objekt, das die Schnittstelle IEnumerable<T> implementiert.

Rheinwerk Computing - Zum Seitenanfang

38.1.2 Einfache AbfragenZur nächsten ÜberschriftZur vorigen Überschrift

Lassen Sie uns mit einer Konsolenanwendung beginnen, der wir ein Entity Data Model (EDM) mit dem Bezeichner Northwind.edmx hinzufügen. Das EDM soll die beiden Tabellen Products und Categories der Northwind-Datenbank beschreiben. Wenn Sie die singuläre und pluralisierende Namensgebung berücksichtigt haben (siehe Abbildung 38.3), werden in der Datei Northwind.Designer.cs die drei Klassen NorthwindEntities, Product und Category erzeugt. Dabei ist NorthwindEntities die Klasse, die von ObjectContext abgeleitet ist, während Product und Category die Entitäten beschreiben, die von der Basis EntityObject abgeleitet sind. Mit diesen drei Klassen werden wir in diesem Kapitel durchweg arbeiten.

Die denkbar einfachste Abfrage ist die, die uns alle Artikel der Tabelle Products zurückliefert. Dazu schreiben wir den folgenden Code in der Methode Main:

using (NorthwindEntities context = new NorthwindEntities())
{
var products = context.Products;
foreach (var prod in products)
Console.WriteLine("{0,-5}{1}", prod.ProductID, prod.ProductName);
}

Listing 38.1 Die denkbar einfachste LINQ-Abfrage

Zuerst wird eine Instanz der Klasse NorthwindEntities erzeugt. Sie sollten dazu die using-Anweisung verwenden, damit der Objektkontext nach seiner Verwendung sofort ordentlich mit Dispose geschlossen wird. Der Objektkontext ist, neben der eigentlichen Abfrage, auch dafür verantwortlich, dass die Verbindung zur Datenbank aufgebaut wird.

Das NorthwindEntities-Objekt veröffentlicht mit Categories und Products zwei Eigenschaften, die jeweils die Menge aller angeforderten Entitäten zurückliefern, also entweder eine Sequenz aller Artikel oder aller Kategorien. Beide Eigenschaften sind vom Typ ObjectSet<T> (oder präziser vom Typ ObjectSet<Product> und ObjectSet<Category>). Die generische Klasse ObjectSet<T> gehört zum Namespace System.Data.Objects und liefert eine typisierte Menge von Entitäten zurück, in unserem Fall alle Produkte. Daher ließe sich das Abfragestatement auch wie folgt formulieren:

ObjectSet<Product> products = context.Products;

Da das Ergebnis einer LINQ-Abfrage vom Typ IEnumerable<T> ist, wäre auch die Formulierung

IEnumerable<Product> products = context.Products;

möglich. Sollten Sie jedoch später im Programmcode auf eine spezifische Methode der Klasse ObjectSet<T> zugreifen wollen, wäre eine vorhergehende Typkonvertierung notwendig.

Im Haltemodus von Visual Studio können Sie sich sehr einfach den Typ der Rückgabe anzeigen lassen, wenn Sie die Maus über den Bezeichner products ziehen (siehe Abbildung 38.1).

Abbildung

Abbildung 38.1 Anzeige des Rückgabedatentyps des Listings 38.1

Ausgeführt wird die LINQ-Abfrage nicht an der Position, wo sie definiert ist, sondern in dem Moment, wenn zum ersten Mal die Ergebnismenge verwendet wird. In Listing 38.1 ist das beim ersten Durchlauf der foreach-Schleife der Fall. Dabei wird die LINQ-Abfrage in eine SQL-Abfrage übersetzt, die von der Datenbank verstanden wird. Wir können uns das SQL-Statement im SQL Server ansehen, wenn wir aus dem SQL Server Management Studio heraus den SQL Server Profiler starten und ein neues Ablaufverfolgungsprotokoll erstellen. Anschließend starten wir die Laufzeit von Visual Studio und können uns nun im Protokoll die tatsächlich gegen die Northwind-Datenbank abgesetzte SQL-Abfrage ansehen. Sie lautet:

SELECT 
[Extent1].[ProductID] AS [ProductID],
[Extent1].[ProductName] AS [ProductName],
[Extent1].[SupplierID] AS [SupplierID],
[Extent1].[CategoryID] AS [CategoryID],
[Extent1].[QuantityPerUnit] AS [QuantityPerUnit],
[Extent1].[UnitPrice] AS [UnitPrice],
[Extent1].[UnitsInStock] AS [UnitsInStock],
[Extent1].[UnitsOnOrder] AS [UnitsOnOrder],
[Extent1].[ReorderLevel] AS [ReorderLevel],
[Extent1].[Discontinued] AS [Discontinued]
FROM [dbo].[Products] AS [Extent1]

Das SQL-Statement sieht natürlich anders aus als die LINQ-Abfrage und entspricht eher dem, was ein T-SQL-Entwickler kennt und benutzt.

Der SQL Server Profiler bietet den Vorteil, alle mit einem SQL-Statement zusammenhängenden Ereignisse zu protokollieren. Sollten Sie sich jedoch nur für das abgesetzte SQL-Statement interessieren, können Sie auch die Methode ToTraceString der Klasse ObjectSet<T> aufrufen, z. B.:

Console.WriteLine(products.ToTraceString());

Die Verbindungszeichenfolge

Sobald mit den Daten einer Abfrage Operationen durchgeführt werden sollen, baut der Objektkontext automatisch eine Verbindung zur Datenbank auf. Hierzu holt sich der Objektkontext alle notwendigen Informationen aus einer Verbindungszeichenfolge, die in der Konfigurationsdatei definiert ist. Die Verbindungszeichenfolgen sind im Grunde genommen von ADO.NET her bekannt, allerdings im Entity Framework etwas komplexer.

Nachfolgend sehen Sie die Verbindungszeichenfolge unseres Entity Data Models.

<connectionStrings>
<add name="NorthwindEntities"
connectionString="metadata=res://*/Northwind.csdl|
res://*/Northwind.ssdl|
res://*/Northwind.msl;
provider=System.Data.SqlClient;
provider connection string=&quot;
data source=.;
initial catalog=Northwind;
integrated security=True;
multipleactiveresultsets=True;
App=EntityFramework&quot;"
providerName="System.Data.EntityClient" />
</connectionStrings>

Neben den allgemein üblichen Informationen wie der Angabe des Providers und der klassischen Verbindungszeichenfolge werden durch das metadata-Attribut auch die Dateien angegeben, die die Beschreibungen für das EDM enthalten (CSDL, MSL und SSDL). Da hier eine Verschachtelung von zwei Verbindungszeichenfolgen vorliegt, wird die innere Verbindungszeichenfolge mit &quot eingeschlossen, womit Anführungszeichen beschrieben werden.

Projektionen

Das Beispiel des Listings 38.1 stellt die einfachste Form einer Abfrage dar und liefert alle Daten, die sich in der Tabelle Products befinden. In SQL-Syntax entspricht das der Abfrage SELECT * FROM Products. In der Regel wird man aber Abfragen formulieren, die nur ganz bestimmte Eigenschaften der Entität in die Sequenz aufnehmen. Hierbei spricht man von einer Projektion. Greifen wir auf unser Beispiel aus dem letzten Abschnitt zurück und beschränken uns nun auf die Ausgabe der drei Eigenschaften ProductID, ProductName und UnitPrice. Zusätzlich sollen die abgefragten Daten gefiltert werden, so dass nur die Artikel, deren Preis größer oder gleich 50 ist, die Ergebnismenge bilden. Dazu ändern wir unsere LINQ-Abfrage wie folgt ab:

using (NorthwindEntities context = new NorthwindEntities())
{
var products = from p in context.Products
where p.UnitPrice >= 50
select new { p.ProductID, p.ProductName, p.UnitPrice };
foreach (var prod in products)
Console.WriteLine("{0,-5}{1,-35}{2}",
prod.ProductID, prod.ProductName, prod.UnitPrice);
}

Listing 38.2 Projektion und Filterung (Abfragesyntax)

Sie können für diese Abfrage, die in der Abfragesyntax formuliert worden ist, auch die gleichwertige Methodensyntax verwenden:

var products = context.Products
.Where(p => p.UnitPrice >= 50)
.Select(p => new { p.ProductID, p.ProductName, p.UnitPrice});

Während die Abfragesyntax leichter zu lesen ist, erinnert die Methodensyntax eher an eine Programmiersprache. Welche Syntax Sie selbst bevorzugen, ist völlig unerheblich, da das Resultat der Abfrage in jedem Fall dasselbe ist.

Interessant ist für uns an dieser Stelle weniger das Ergebnis der Abfrage. Vielmehr interessiert uns der Datentyp der Ergebnismenge. Schließlich projizieren wir nur einen Teil der tatsächlichen Spalten der Tabelle Products in unsere Ergebnismenge. Um den Typ in Erfahrung zu bringen, legen wir einen Haltepunkt in der Entwicklungsumgebung fest, z. B. in der foreach-Zeile. Starten Sie danach die Anwendung, und gehen Sie mit dem Mauszeiger auf die Variable products. Sie werden feststellen, dass die Ergebnismenge nun vom Typ ObjectQuery<T> ist, bei dem der generische Typparameter einen anonymen Typ beschreibt (siehe Abbildung 38.2).

Abbildung

Abbildung 38.2 »ObjectQuery<T>« als Rückgabetyp einer Selektion

Die beiden Klassen ObjectQuery<T> und ObjectSet<T> sind sich sehr ähnlich. Allerdings ist ObjectSet<T> spezialisierter als ObjectQuery<T>, da es sich um eine typisierte Entitätenmenge handelt, während es sich bei ObjectQuery<T> nur um eine typisierte Abfrage, also unter Umständen auch mit einer definierten Projektion, handelt. Die Ähnlichkeit spiegelt sich auch in den Klassendefinitionen wider: Der Typ ObjectSet<T> ist von ObjectQuery<T> abgeleitet. Gegenüber der Basis weist ObjectSet<T> zusätzliche Funktionalitäten auf, beispielsweise für das Hinzufügen oder Löschen von Objekten.

Einzelne Entitäten abrufen

Unsere bisherigen Abfragen lieferten als Ergebnis Objekte, die die Schnittstelle IEnumerable<T> implementieren. Dabei handelt es sich um Sequenzen vom Typ ObjectSet<T> oder ObjectQuery<T>. Um die einzelnen Entitäten auszuwerten, musste die Ergebnismenge in einer Schleife durchlaufen werden.

Manchmal reicht es aus, wenn das Resultat einer LINQ-Abfrage nur eine bestimmte Entität zurückliefert. Beispielsweise könnte es sich dabei um den Artikel mit der ProductID = 22 handeln. Für diese Fälle stellt uns das Entity Framework vier Methoden zur Verfügung:

  • Single
  • SingleOrDefault
  • First
  • FirstOrDefault

First bzw. FirstOrDefault bieten sich an, wenn aus einer Sequenz nur das erste Element zurückgegeben werden soll. Single bzw. SingleOrDefault sind dann geeignet, wenn nur ein Element als Resultat der Abfrage erwartet werden kann.

Es bleibt noch zu klären, was der Unterschied zwischen Single und SingleOrDefault bzw. First und FirstOrDefault ist. Die Erklärung ist sehr einfach: Wird kein passendes Element gefunden, lösen Single und First eine Ausnahme vom Typ InvalidOperationException aus, SingleOrDefault und FirstOrDefault liefern in dieser Situation hingegen den Rückgabewert null.

Im Zusammenhang mit den beiden Methoden Single und SingleOrDefault gibt es noch eine weitere Situation, die zu der genannten Ausnahme führt. Das ist nämlich genau dann der Fall, wenn in der Ergebnismenge mehrere Resultate stehen, während beide Methoden nur ein Resultat erwarten. Sie sollten dann First oder FirstOrDefault benutzen.

Abfragen, die Sequenzen als Resultat liefern, werden per Vorgabe erst in dem Moment ausgeführt, wenn auf die Elemente der Sequenz zugegriffen wird. Ganz anders verhalten sich die vier in diesem Abschnitt angesprochenen Methoden, die nur ein Element in der Ergebnismenge haben: Sie werden sofort ausgeführt, das Objekt steht also sofort zur Verfügung.

Mit diesen Kenntnissen ausgestattet ist es nun sehr einfach, den Artikel mit der ProductID 22 abzurufen.

using (NorthwindEntities context = new NorthwindEntities())
{
var query = (from prod in context.Products
where prod.ProductID == 22
select prod).SingleOrDefault();
if (query != null)
Console.WriteLine("{0,-20}", query.ProductName);
else
Console.WriteLine("Kein entsprechendes Element gefunden.");
}

Listing 38.3 Die Methode »SingleOrDefault«

Sie können die Abfrage auch noch einfacher definieren, denn SingleOrDefault (aber auch Single, First und FirstOrDefault) ist in der Weise überladen, dass die Filterbedingung als Argument des Methodenaufrufs angegeben werden kann, z. B.:

var query = (from prod in context.Products  
select prod).SingleOrDefault(p => p.ProductID == 22);

Abfragen mit Paging

Vielleicht wünschen Sie, dass Ihre Abfrage nur einen bestimmten Teilbereich der Tabelle in das Abfrageresultat projiziert. Dann müssen Sie die Technik des Pagings benutzen. Umgesetzt wird das Paging in LINQ to Entities mit den beiden Operatoren Skip und Take. An den Operator Skip übergeben Sie dabei die Anzahl der Elemente, die übersprungen werden sollen, Take erwartet die Anzahl der zurückzugebenden Elemente.

Das folgende Listing zeigt den Einsatz der beiden Operatoren. Dabei muss berücksichtigt werden, dass Skip nur auf sortierte Mengen angesetzt werden kann und die Sortierung ausschlaggebend dafür ist, welche Elemente in die Ergebnismenge aufgenommen werden.

using (NorthwindEntities context = new NorthwindEntities())
{
Console.Write("Wie viele DS überspringen?");
int skip = Convert.ToInt32(Console.ReadLine());
Console.Write("Wie viele DS anzeigen?");
int count = Convert.ToInt32(Console.ReadLine());
var query = (from prod in context.Products
orderby prod.ProductID
select prod).Skip(skip).Take(count);
foreach (var item in query)
Console.WriteLine("{0,-4}{1}", item.ProductID, item.ProductName);
}

Listing 38.4 Das Paging mit »Skip« und »Take« umsetzen

Operatoren mit sofortiger Ausführung

Sowohl Single/SingleOrDefault als auch First/FirstOrDefault werden sofort ausgeführt. Aber auch eine Abfrage, die mehrere Objekte als Resultat liefert, lässt sich sofort ausführen. Die Operatoren, die dazu in der Lage sind, können Sie der Tabelle 38.1 entnehmen.

Tabelle 38.1 Operatoren mit sofortiger Ausführung

Operator Beschreibung

ToList

Erstellt aus einem IEnumerable<T>-Objekt ein List<T>-Objekt.

ToDictionary

Erstellt aus einem IEnumerable<T>-Objekt ein Dictionary<TKey, TValue>-Objekt.

ToLookup

Erstellt aus einem IEnumerable<T>-Objekt ein Lookup<TKey, TElement>-Objekt.

ToArray

Erstellt aus einem IEnumerable<T>-Objekt ein Array.

Dazu auch noch ein Listing, das den Einsatz des ToList-Operators zeigt:

using (NorthwindEntities context = new NorthwindEntities())
{
List<Product> query = (from prod in context.Products
where prod.UnitPrice < 10
select prod).ToList();
foreach (Product item in query)
Console.WriteLine("{0,-35}{1}", item.ProductName, item.UnitPrice);
}

Listing 38.5 Der »ToList«-Operator

Anzumerken sei an dieser Stelle, dass auch die Aggregat-Operatoren von LINQ wie beispielsweise Count oder Max zu einer sofortigen Ausführung der Abfrage führen. Weiter unten werden wir auf diese Operatoren noch zu sprechen kommen.


Rheinwerk Computing - Zum Seitenanfang

38.1.3 Navigieren in AbfragenZur nächsten ÜberschriftZur vorigen Überschrift

Unser EDM bildet die beiden Tabellen Products und Categories ab, zwischen denen in der Datenbank eine 1:n-Beziehung besteht: Ein Produkt wird genau einer (oder keiner) Kategorie zugeordnet, während mehrere Produkte zu einer Kategorie gehören können.

Im ADO.NET Entity Framework haben Sie zwei Möglichkeiten, um die Beziehung zwischen zwei Entitäten zu nutzen:

  • die Navigationseigenschaften
  • die Fremdschlüsselspalte von abhängigen Entitäten

Navigationseigenschaften ermöglichen es, sehr einfach in Beziehung stehende Entitäten abzurufen. Bezogen auf unser EDM wird die Beziehung zwischen den Entitäten Product und Category mit den beiden Navigationseigenschaften Products (in der Entität Category) und Category (in der Entität Product) umgesetzt. Die Umsetzung der Fremdschlüsselspalte in eine Eigenschaft der Entität (in Product handelt es sich um die Eigenschaft CategoryID) lässt sich ebenfalls zur Abbildung einer Beziehung nutzen.

In diesem Moment stellt sich die Frage, warum uns zwei Varianten angeboten werden.

Um diese Frage zu beantworten, müssen wir in der Historie des ADO.NET Entity Frameworks ein wenig zurückblicken. In der ersten Version 3.5 enthielten die Entitäten nur Navigationseigenschaften. Es war dies die strikte Einhaltung eines objektorientierten Ansatzes und dessen Umsetzung im konzeptionellen Modell, in dem auch die Beziehung durch ein Element abgebildet wird.

Nicht in allen Fällen haben sich die Navigationseigenschaften als vorteilhaft erwiesen. Als typisches Beispiel sei hier die Datenbindung erwähnt. Hier war es nur mit verhältnismäßig hohem Programmieraufwand möglich, aus dem Resultat der Navigationseigenschaft den Fremdschlüsselwert zu ermitteln. Das Entwicklerteam sah sich daher gezwungen, den abhängigen Entitäten eine Eigenschaft hinzuzufügen, die den Fremdschlüssel repräsentiert.

Die Fremdschlüsselspalten werden ab EF 4.0 automatisch in den betreffenden Entitäten berücksichtigt. Möchten Sie auf diese Spalten verzichten, haben Sie bei der Definition des Entity Data Models dazu die Möglichkeit, wenn Sie die Auswahl im entsprechenden Optionsschalter des Dialogs (siehe auch Abbildung 37.3) abwählen.

Navigation von der »n-Seite« zur »1-Seite« einer Zuordnung

Jeder Product-Entität wird genau eine Category-Entität zugeordnet. Uns interessiert zunächst im folgenden Beispiel die Kategorie, die jedem Produkt zugeordnet ist. Die Ergebnismenge soll dazu den Produkt- und den Kategoriebezeichner enthalten.

using (NorthwindEntities context = new NorthwindEntities())
{
var query = context.Products
.Select(p => new { p.ProductName, p.Category.CategoryName});
foreach (var item in query)
Console.WriteLine("{0,-35}{1}", item.ProductName, item.CategoryName);
}

Listing 38.6 Einsatz der Navigationsmethode »Category«

Sollten Sie sich mehr für die Abfragesyntax begeistern können, hier auch noch die Abfrage in dieser syntaktischen Variante:

var query = from p in context.Products
select new { p.ProductName, p.Category.CategoryName };

Wir rufen in beiden Fällen die Navigationseigenschaft Category der Product-Entität auf. Die Navigationseigenschaft liefert uns die Referenz auf die dem Produkt zugeordnete Category-Entität, von der wir die Eigenschaft CategoryName abrufen.

Das Ergebnis der Abfrage ist ein anonymer Typ, der sich aus der Eigenschaft ProductName der Entität Product und der Eigenschaft CategoryName der Entität Category zusammensetzt. Die Sequenz ist vom Typ ObjectQuery<T>.

Sollte Ihnen die Ausgabe nicht gefallen, lässt sich die Ergebnisliste auch sortieren oder filtern. Auch dabei unterstützt uns die Navigationseigenschaft Category der Entität Product. Im folgenden Listing wird primär nach CategoryName sortiert, anschließend innerhalb der Kategorie nach ProductName.

using (NorthwindEntities context = new NorthwindEntities())
{
var query = from prod in context.Products
orderby prod.Category.CategoryName, prod.ProductName

select new { prod.ProductName, prod.Category.CategoryName };
foreach(var item in query)
Console.WriteLine("{0,-35}{1}", item.CategoryName, item.ProductName);
}

Listing 38.7 Sortieren der Ergebnismenge

Die Abfrage nun auch noch in Methodensyntax:

var query = context.Products
.OrderBy(p => p.Category.CategoryName)
.ThenBy(p => p.ProductName)

.Select(p => new { p.ProductName, p.Category.CategoryName });

Navigation von der »1-Seite« zur »n-Seite« einer Zuordnung

Die Navigation von der 1-Seite einer Zuordnung zu der n-Seite der Zuordnung sieht etwas anders aus. Eine solche Navigation wird in unserem Entity Data Model durch die Eigenschaft Products der Entität Category beschrieben: Jeder Kategorie sind viele Produkte zugeordnet (oder auch keins). Also liefert uns die Navigationseigenschaft Products nicht maximal ein Objekt zurück, sondern unter Umständen sehr viele.

Betrachten Sie dazu das folgende Listing 38.8, in dem alle Produkte abgerufen werden, die entweder der Kategorie Beverages (CategoryID=1) oder der Kategorie Seafood (CategoryID=8) zugeordnet sind.

using (NorthwindEntities context = new NorthwindEntities())
{
var query = from cat in context.Categories
where cat.CategoryID == 1 || cat.CategoryID == 8
select new { cat.CategoryName, Artikel = cat.Products };
foreach (var item in query)
{
Console.WriteLine("{0}", item.CategoryName);
foreach (var prod in item.Artikel)
Console.WriteLine("... {0}", prod.ProductName);
Console.WriteLine();
}
}

Listing 38.8 Einsatz der Navigationseigenschaft »Products«

Die Ergebnismenge setzt sich aus dem Bezeichner der Kategorie und einer Liste der zu der betreffenden Kategorie gehörenden Produkte zusammen, die durch die Eigenschaft Artikel beschrieben wird. Diese Eigenschaft ist vom Typ EntityCollection<T> und kann mit einer foreach-Schleife durchlaufen werden.

Eingebettete Abfragen

Im Listing 38.8 ist das von der Datenbank gelieferte Datenvolumen zur Materialisierung sehr groß, weil alle Produkte mit allen ihren Eigenschaften in die Ergebnismenge geschrieben werden. Interessieren uns aber nicht alle Eigenschaften, sollten wir nur die für uns wesentlichen abrufen. Dazu ist eine Selektion bezogen auf die von der Navigationseigenschaft zurückgelieferten Produkte notwendig. Unsere Abfrage wollen wir daher im nächsten Schritt verbessern und nur die beiden Eigenschaften ProductName und UnitPrice der Product-Entität in die Ergebnismenge aufnehmen. Aus diesem Grund ersetzen wir in der LINQ-Abfrage den Teilausdruck

Artikel = cat.Products

durch eine Projektion, in der die beiden erwähnten Eigenschaften aufgenommen werden. Dazu formulieren wir eine innere, eingebettete Abfrage, wie im folgenden Codefragment gezeigt wird:

using (NorthwindEntities context = new NorthwindEntities())
{
var query = from cat in context.Categories
where cat.CategoryID == 1 || cat.CategoryID == 8
select new { cat.CategoryName,
Artikel = from prod in cat.Products
select new { prod.ProductName,
prod.UnitPrice }

};
foreach (var item in query)
{
Console.WriteLine(item.CategoryName);
foreach (var item2 in item.Artikel)
Console.WriteLine("...{0,-35}{1}",item2.ProductName, item2.UnitPrice);
}
}

Listing 38.9 Navigationseigenschaft mit Selektion

Nun wird die Eigenschaft Artikel in der Ergebnismenge nicht mehr durch ein Objekt vom Typ EntityCollection<T>, sondern durch eine Liste (Typ: List<T>) anonymer Typen beschrieben. Jedes der Listenelemente wird aus den Eigenschaften ProductName und UnitPrice der Entität Product gebildet.

Vielleicht haben Sie auch noch eine andere Idee, das Vorhaben zu realisieren, und schreiben die folgende LINQ-Abfrage:

var query = from prod in context.Products
where prod.CategoryID == 1 || prod.CategoryID == 8
orderby prod.CategoryID
select new { prod.Category.CategoryName,
prod.ProductName, prod.UnitPrice };

Beachten Sie, dass nun die Abfrage gewissermaßen umgedreht worden ist: Es wird als Ausgangsmenge der LINQ-Abfrage nicht mehr Categories, sondern Products verwendet. Zuerst werden die Produkte anhand der CategoryID-Eigenschaft gefiltert, anschließend nach Kategorien sortiert. Die Sequenz wird durch einen anonymen Typ gebildet, der alle erforderlichen Eigenschaften enthält, nämlich CategoryName, ProductName und UnitPrice.

Diese LINQ-Abfrage führt offensichtlich zum gleichen Resultat. Dennoch gibt es einen Unterschied: Sobald einer Kategorie mehrere Produkte zugeordnet werden (was in unserer Datenquelle tatsächlich der Fall ist), ist der Kategoriebezeichner (CategoryName) mehrfach in der Ergebnismenge enthalten. Das war im ersten Beispiel nicht der Fall.

Filtern einer »EntityCollection«

Stellen wir uns nun die folgende Aufgabe: Es sollen alle Kategorien ausgegeben werden, die von einem bestimmten Lieferanten bereitgestellt werden können. Nehmen wir an, es würde sich dabei um den Lieferanten Exotic Liquids handeln, der in der Tabelle Suppliers unter SupplierID=1 geführt wird. (Hinweis: Die Entität Product stellt mit der Eigenschaft SupplierID den Bezug zu dem Lieferanten her. Allerdings ist die Aufgabenstellung so einfach gehalten, dass wir unser EDM nicht durch eine Entität Supplier erweitern müssen.)

Ein erster, etwas naiver Ansatz der Formulierung der LINQ-Abfrage könnte wie folgt aussehen:

// ACHTUNG: Falsche LINQ-Abfrage
var query = from cat in context.Categories
where cat.Products.SupplierID == 1
select cat;

Der LINQ-Ausdruck wird zu einem Fehler führen, da der Teilausdruck cat.Products vom Typ EntityCollection<T> ist und nicht die Eigenschaft SupplierID veröffentlicht, die zur Filterung der Daten notwendig ist.

In dieser Situation hilft uns die Methode Any weiter. Any stellt fest, ob ein Element in einer Menge vom Typ IEnumerable<T> eine bestimmte Bedingung erfüllt. In unserem Fall handelt es sich um die durch cat.Products gebildete Menge vom Typ EntityCollection<T>, die auch die erforderliche Schnittstelle implementiert. Die Lösung der Aufgabestellung mit Any beschreibt das folgende Listing.

using (NorthwindEntities context = new NorthwindEntities())
{
var query = from cat in context.Categories
where cat.Products.Any(prod => prod.SupplierID == 1)
select cat;
foreach (var item in query)
Console.WriteLine(item.CategoryName);
}

Listing 38.10 Filtern einer »EntityCollection«

Any entnimmt aus der EntityCollection ein Element nach dem anderen und untersucht, ob die Bedingung SupplierID=1 erfüllt ist oder nicht. Alle Kategorien, die mindestens ein Produkt enthalten, das die gestellte Bedingung erfüllt, gehören zur Ergebnismenge der LINQ-Abfrage.


Rheinwerk Computing - Zum Seitenanfang

38.1.4 AggregatmethodenZur nächsten ÜberschriftZur vorigen Überschrift

Aggregate, Average, Count, LongCount, Min, Max und Sum sind LINQ-Aggregatoperatoren und ermöglichen einfache mathematische Operationen mit den Elementen einer Sequenz. Wie weiter oben schon erwähnt, stellen alle Aggregatmethoden das Resultat der Abfrage sofort zur Verfügung.

Im Folgenden wollen wir uns den Einsatz der LINQ-Abfrageoperatoren an Beispielen ansehen. Dabei möchte ich Ihnen exemplarisch für alle anderen die beiden Aggregatmethoden Count und Max vorstellen.

Der »Count«-Operator

Starten wir mit einer sehr einfachen Abfrage, die uns die Gesamtanzahl aller Produkte angibt:

int count = context.Products.Count();
Console.WriteLine("Anzahl der Produkte: {0}", count);

Hier kommt der Operator Count zum Einsatz, der einen Integer als Rückgabewert hat. Sollte die Menge sehr groß sein, d. h. den Wertebereich des Integers überschreiten, würde sich alternativ LongCount anbieten, denn dieser Operator hat den Rückgabedatentyp long.

Zum tieferen Verständnis sollten wir uns die Definition von Count ansehen, wobei es sich selbstverständlich um eine Erweiterungsmethode handelt:

public static int Count<T>(this IEnumerable<T> source)

Der Parameter deutet an, dass Count die Typen erweitert, die die Schnittstelle IEnumerable<T> implementieren. Damit rechtfertigt sich auch, dass wir Count auf context.Products aufrufen, weil der Teilausdruck context.Products vom Typ ObjectSet<T> ist und somit das genannte Interface implementiert.

Count ist überladen. Wir können auch eine Bedingung formulieren, unter deren Berücksichtigung wir ein gefiltertes Resultat haben wollen. Die Bedingung wird als Lambda-Ausdruck formuliert. Die Definition der Überladung sieht wie folgt aus:

public static int Count<T>(this IEnumerable<T> source, 
Func<T, bool> predicate)

Um Ihnen ein Beispiel zur Count-Überladung zu zeigen, nehmen wir an, dass wir wissen wollen, wie viele Produkte »teuer« sind, sagen wir, einen Preis haben, der größer oder gleich 50 ist.

int count = context.Products.Count(p => p.UnitPrice >= 50);
Console.WriteLine("Anzahl der Produkte: {0}", count);

Ist doch überhaupt nicht schwer, oder?

Etwas mehr wird uns abverlangt, wenn wir wissen wollen, wie viele Produkte zu jeder Kategorie gehören. Natürlich hilft uns auch hier Count weiter.

using (NorthwindEntities context = new NorthwindEntities())
{
var query = from cat in context.Categories
select new { cat.CategoryName, Count = cat.Products.Count() };
foreach (var item in query)
Console.WriteLine("{0,-15} {1}", item.CategoryName, item.Count);
}

Listing 38.11 Weiterer Einsatz von »Count«

Zuerst wird das ObjectSet<Category> gebildet. In die Ergebnismenge wird anschließend der Kategoriebezeichner aufgenommen und der Wert, der die Anzahl der Produkte widerspiegelt. Dazu wird für jedes Element in der Ergebnismenge – also für jede Kategorie – die Navigationsmethode Products aufgerufen, die alle Produkte der entsprechenden Kategorie liefert. Mit Count zählen wir allerdings nur die Produkte und verzichten auf weitere Angaben.

Der »Max«-Operator

Der Max-Operator liefert einen Maximalwert aus einer Menge zurück, die die Schnittstelle IEnumerable<T> implementiert. Einen relativ einfachen Einsatz des Max-Operators zeigt das folgende Listing, das dazu dient, aus allen Produkten das mit dem höchsten Preis herauszufiltern.

using (NorthwindEntities context = new NorthwindEntities())
{
var query = from prod in context.Products
where prod.UnitPrice == context.Products.Max(p => p.UnitPrice)
select new { prod.ProductName, prod.UnitPrice };
foreach (var item in query)
Console.WriteLine("{0}, {1}", item.ProductName, item.UnitPrice);
}

Listing 38.12 Der Operator »Max«

In diesem Listing wird Max innerhalb der Filterung mit Where dazu benutzt, aus allen Produkten den Höchstpreis festzustellen. Mit diesem werden alle Produkte verglichen und bei Übereinstimmung in die Ergebnismenge eingetragen. In unserem Beispiel handelt es sich zwar um genau ein Produkt, aber es könnten durchaus auch mehrere sein.


Rheinwerk Computing - Zum Seitenanfang

38.1.5 Joins in LINQ definierenZur nächsten ÜberschriftZur vorigen Überschrift

Inner Joins

In Listing 38.8 haben wir die Navigationsmethoden dazu benutzt, uns alle Produkte der beiden Kategorien Beverages (CategoryID=1) und Seafood (CategoryID=8) ausgeben zu lassen. Wir können die LINQ-Abfrage auch ohne Einsatz der Navigationsmethoden formulieren, indem wir mit Join diese Assoziation nachbilden. Mit anderen Worten bilden wir die Beziehung zwischen den beiden Entitäten Product und Category nach, ohne die Navigationsmethoden zu bemühen.

Anders als im erwähnten Listings 38.8 wollen wir uns aber nicht auf die zwei Kategorien beschränken, sondern alle berücksichtigen.

using (NorthwindEntities context = new NorthwindEntities())
{
var query = from cat in context.Categories
join prod in context.Products
on cat.CategoryID equals prod.CategoryID
orderby cat.CategoryName
select new { cat.CategoryName, prod.ProductName };
foreach (var item in query)
Console.WriteLine("{0}...{1}", item.CategoryName, item.ProductName);
}

Listing 38.13 Inner Join

Auf diese Weise haben wir einen inneren Join erzeugt, der nur die Entitäten berücksichtigt, die eine gemeinsame CategoryID aufweisen. Alle Kategorien, die keine Entsprechung auf der Seite der Produkte finden, gehören nicht zum Ergebnis der Abfrage. Da es in der Northwind-Datenbank eine solche Kategorie nicht gibt, sollten Sie in der Tabelle Categories der Northwind-Datenbank eine weitere Kategorie hinzufügen, z. B. Magazines, und die LINQ-Abfrage noch einmal starten. Magazines wird nicht in der Ergebnisliste geführt, weil der neuen Kategorie kein Produkt zugeordnet worden ist.

Left Outer Joins mit LINQ

Ein Left Outer Join ist ein Join, in dem jedes Element aus der zuerst genannten Elementmenge auch dann aufgeführt wird, wenn in der zweiten Auflistung kein korrelierendes Element existiert. Um einen Left Outer Join mit LINQ zu formulieren, bilden Sie einen Group Join und rufen auf diesen DefaultIfEmpty auf. Das klingt kompliziert, daher dazu auch sofort ein Beispiel. Um tatsächlich auch ein Ergebnis zu finden, sollten Sie spätestens jetzt die Kategorie Magazines in der Tabelle Categories der Northwind-Datenbank eintragen.

using (NorthwindEntities context = new NorthwindEntities())
{
var results = from cat in context.Categories
join prod in context.Products
on cat.CategoryID equals prod.CategoryID
into listProd
from p in listProd.DefaultIfEmpty()
select new { CatName = cat.CategoryName,
ProdName = p.ProductName };
foreach (var item in results)
Console.WriteLine("{0,-15} ... {1}", item.CatName, item.ProdName);
}

Listing 38.14 Left Outer Join

Die Sequenz Categories ist hier die linke Auflistung, die Sequenz Products die rechte. Ein Group Join, der in LINQ mit into abgebildet wird, erzeugt eine hierarchische Ergebnismenge, in der die Elemente aus der rechten Sequenz dem entsprechenden Element aus der linken Sequenz zugeordnet werden. Ein Group Join kann man sich damit als eine Menge von Objektarrays vorstellen. In unserem Beispiel wird für jedes Element der linken Sequenz, also der Kategorien, mit into ein Array gebildet.

Wenn für ein Element der linken Sequenz keine korrespondierenden Elemente aus der rechten Sequenz zugeordnet werden können, erzeugt die join-Klausel ein leeres Array.

Anschließend muss jedes Element der linken Elementmenge in die Ergebnisliste aufgenommen werden, selbst wenn dieses Element keine Entsprechung in der rechten Auflistung hat. Dazu rufen Sie DefaultIfEmpty für jede Sequenz von übereinstimmenden Elementen aus dem Group Join auf. Die Ausgabe in der Konsole überzeugt: Tatsächlich finden wir die neue Kategorie Magazines wieder, obwohl keine entsprechenden Produkte zugeordnet werden können.

Vielleicht interessieren Sie sich nur für die Kategorien, denen noch keine Produkte zugeordnet worden sind. Basierend auf der LINQ-Abfrage des letzten Listings müssen wir nur eine kleine Ergänzung vornehmen und die Elemente aus der Ergebnisliste herausfiltern, die den Wert null haben, also:

var results = from cat in context.Categories join prod in context.Products
on cat.CategoryID equals prod.CategoryID
into listProd
from p in listProd.DefaultIfEmpty()
where p == null
[...]

Right Outer Joins mit LINQ

Unter einem Right Outer Join wird die Umkehrung des Left-Outer-Join-Prinzips verstanden: Es werden alle Elemente der zweiten Elementmenge in die Ergebnisliste aufgenommen – auch wenn sich kein entsprechendes Element in der ersten Elementliste befindet. Bezogen auf unser Listing 38.14 würde das bedeuten, dass wir alle Produkte in das Abfrageresultat schreiben, auch wenn das Produkt keiner Kategorie zugeordnet wird (das Feld CategoryID darf tatsächlich per Definition Null sein).

LINQ unterstützt von Hause aus keine Right Outer Joins. Auch wenn das im ersten Moment ernüchternd klingt, gibt es aber sehr wohl einen Weg, einen Right Outer Join nachzubilden: Wir müssen dazu nur einen Left Join definieren und die Reihenfolge der angeführten Elementlisten hinter from umdrehen.

using (NorthwindEntities context = new NorthwindEntities())
{
var results = from prod in context.Products
join cat in context.Categories
on prod.CategoryID equals cat.CategoryID
into listProd
from p in listProd.DefaultIfEmpty()
select new {
CatName = p.CategoryName,
ProdName = prod.ProductName
};
foreach (var item in results)
Console.WriteLine("{0,-35}{1}", item.ProdName, item.CatName);
}

Listing 38.15 Right Outer Joins

Wenn Sie dieses Beispiel ausprobieren möchten, sollten Sie zuvor der Tabelle Products einen weiteren Artikel hinzufügen, aber ohne eine Kategorie anzugeben. Dann können Sie erkennen, dass auch das neue Produkt in die Ergebnisliste aufgenommen worden ist.


Rheinwerk Computing - Zum Seitenanfang

38.1.6 In Beziehung stehende Daten ladenZur nächsten ÜberschriftZur vorigen Überschrift

Lazy Loading

In den bisherigen Beispielen lieferten uns die LINQ-Abfragen immer das gewünschte Ergebnis. Wir haben uns keine Gedanken darüber gemacht, wann und wie die Daten tatsächlich bereitgestellt werden – das Entity Framework hat das für uns im Hintergrund automatisch erledigt. Dennoch müssen wir uns darüber bewusst sein, dass auch hinsichtlich des Ladens der Daten aus der Datenbank Vorgänge ablaufen, die wir beeinflussen können, besser noch, die wir manchmal sogar beeinflussen müssen.

Zur Verdeutlichung der Problematik sehen Sie sich bitte das folgende Listing an. Die LINQ-Abfrage liefert eine Menge anonymer Typen, in der alle Kategorien sowie die Anzahl der Produkte der betreffenden Kategorie enthalten sind.

using (NorthwindEntities context = new NorthwindEntities())
{
var query = from cat in context.Categories
select new { cat, Anzahl = cat.Products.Count() };
foreach (var item in query)
Console.WriteLine("Kategorie: {0,-15} Anzahl der Produkte: {1}",
item.cat.CategoryName, item.Anzahl);
}

Listing 38.16 Anzahl der einer Kategorie zugeordneten Produkte

Dasselbe Resultat liefert auch die LINQ-Abfrage des folgenden Listings. Wir fragen wieder alle Kategorien ab und bilden ein Objekt vom Typ QuerySet<Category>. Innerhalb der foreach-Schleife wird bei der Ausgabe über die Navigationseigenschaft Products die Summe aller Produkte gebildet, die zu der betreffenden Kategorie gehören.

using (NorthwindEntities context = new NorthwindEntities())
{
var query = from cat in context.Categories
select cat;
foreach (var item in query)
Console.WriteLine("Kategorie: {0,-15}Anzahl der Produkte: {1}",
item.CategoryName, item.Products.Count());
}

Listing 38.17 Anzahl der einer Kategorie zugeordneten Produkte (Lazy Loading)

Obwohl ganz offensichtlich die Konsolenausgabe identisch ist, unterscheidet sich die Ausführung der beiden Listings ganz deutlich. Wir können uns davon überzeugen, wenn wir den SQL Server Profiler starten und uns den Ablauf der Ausführung protokollieren lassen. In den folgenden beiden Abbildungen sind sowohl für Listing 38.16 als auch für Listing 38.17 die vom SQL Server Profiler aufgezeichneten Protokolle dargestellt.

Abbildung

Abbildung 38.3 Ablaufprotokoll des Listings 38.16

Abbildung

Abbildung 38.4 Ablaufprotokoll des Listings 38.17

Es fällt auf, dass im Fall der Ausführung von Listing 38.16 nur eine Abfrage an den SQL Server geschickt wird, die dann alle erforderlichen Daten enthält.

Fragen wir jedoch zuerst nur alle Kategorien ab und rufen innerhalb der Schleife auch die Navigationseigenschaft auf, um die Summe aller Produkte zu ermitteln, wird für jede Kategorie eine neue Abfrage auf dem SQL Server ausgeführt. Das Verhalten, bei dem beim Zugriff auf eine Navigationseigenschaft automatisch Daten aus der Datenquelle geladen werden, wird als Lazy Loading oder Deferred Loading bezeichnet.

Lazy Loading ist die Standardvorgabe des Entity Frameworks und führt zu mehr oder wenigen vielen zusätzlichen Datenbankabfragen. Das Verhalten ist somit natürlich alles andere als effizient und kann wegen der damit verbundenen hohen Datenbankaktivität zu Problemen führen. Wenn Sie sich Abbildung 38.4 noch einmal ansehen, werden Sie zudem feststellen, dass alle Produktdaten abgerufen werden. Eine Filterung der zu ladenden Daten ist mit Lazy Loading nicht möglich. Dieser Problematik des Lazy Loadings sollten Sie sich bewusst sein und nach Möglichkeit darauf verzichten. Es gibt auch noch andere Möglichkeiten, um die in Beziehung stehenden Daten zu laden.

Lazy Loading unterdrücken

Lazy Loading ist per Vorgabe aktiviert und wird in den Konstruktoren der von ObjectContext abgeleiteten Klasse (in unseren Beispielen NorthwindEntities) festgelegt. Sie können es jedoch auf zwei verschiedene Weisen abschalten:

  • mit Programmcode
  • in der Entwicklungsumgebung

Die Deaktivierung erfolgt über die Eigenschaft ContextOptions des ObjectContext-Objekts. ContextOptions beschreibt selbst die Referenz auf ein Objekt vom Typ ObjectContextOptions, mit dem mehrere spezifische Eigenschaften des ObjectContext-Objekts beschrieben werden. In unserem aktuellen Fall interessiert uns die Abschaltung des Lazy Loadings, die wir durch Setzen der Eigenschaft LazyLoadingEnabled auf false erreichen können.

using (NorthwindEntities context = new NorthwindEntities()) {
context.ContextOptions.LazyLoadingEnabled = false;
[...]
}

Zum Abschalten in der Entwicklungsumgebung markieren Sie das Entity Data Model im Designer, damit dessen Eigenschaften im Eigenschaftsfenster angezeigt werden. Danach stellen Sie die Eigenschaft Lazy Loading aktiviert auf false ein (siehe Abbildung 38.5).

Abbildung

Abbildung 38.5 Die Eigenschaft »Lazy Loading« des konzeptionellen Modells

Mit dem deaktivierten Lazy Loading sollten Sie noch einmal Listing 38.22 ausführen. Sie werden feststellen, dass nun die Daten der Produkte nicht mehr nachgeladen werden und somit auch der Count-Aufruf wirkungslos verpufft: Die Anzeige lautet für jede Kategorie »Anzahl der Produkte: 0«.

Explizites Laden verbundener Objekte

Mit der Deaktivierung des Lazy Loadings haben wir uns zunächst einmal der Möglichkeit beraubt, die Anzahl der zu einer Kategorie gehörenden Produkte zu ermitteln. Dennoch lassen sich die Produkte für jede Kategorie explizit laden. Dazu rufen wir die Methode Load auf Products in der Schleife auf. Load ist eine Methode der Klasse EntityCollection<T>.

using (NorthwindEntities context = new NorthwindEntities())
{
context.ContextOptions.LazyLoadingEnabled = false;
var query = from cat in context.Categories
select cat;
foreach (var item in query)
{
item.Products.Load();
Console.WriteLine("{0,-20} 'Anzahl: {1}",
item.CategoryName, item.Products.Count);
}
}

Listing 38.18 Explizites Laden mit der Methode »Load«

Wenn Sie sich das Ablaufprotokoll im SQL Server Profiler ansehen, werden Sie keinen Unterschied zum Lazy Loading feststellen, denn für jede Kategorie wird weiterhin ein Server-Roundtrip erforderlich. Dennoch hat der explizite Aufruf von Load einen Vorteil gegenüber dem Lazy Loading: Sie können nämlich steuern, zu welcher Kategorie die Anzahl der Produkte angezeigt werden soll. Dadurch wird sichergestellt, dass eine Abfrage nicht ohne eine explizite Anforderung der verknüpften Entität ausgeführt wird. Das ist ein großer Vorteil im Vergleich zum standardmäßigen Lazy Loading.

Das folgende Listing zeigt, wie der Code dazu geschrieben werden muss.

using (NorthwindEntities context = new NorthwindEntities())
{
context.ContextOptions.LazyLoadingEnabled = false;
var query = from cat in context.Categories
select cat;
foreach (var item in query)
{
Console.WriteLine(item.CategoryName);
if (item.CategoryName == "Beverages") {
item.Products.Load();
Console.WriteLine("Anzahl: {0}", item.Products.Count());
}
Console.WriteLine();
}
}

Listing 38.19 Explizites Laden mit der Methode »Load«

Eager Loading

In Fällen, in denen von vornherein bekannt ist, dass nicht nur die Kategorien, sondern darüber hinaus auch viele Produkte und deren Details von Interesse sind, ist es eine gute Lösung, die Produkte direkt von der LINQ-Anfrage zurückgeben zu lassen. Dass man das mit einem Select in einer Projektion erreichen kann, haben Sie schon erfahren. Die Methode Include, die auf ein ObjectQuery-Objekt aufgerufen wird, ist eine andere Variante und wird als Eager Loading bezeichnet.

Include hat gegenüber einer Projektion den Vorteil, dass das Abfrageergebnis Entitäten liefert und nicht nur anonyme Typen. Andererseits ermöglicht Include keine Filterung, was mit einer Selektion wiederum möglich ist. Include wird entweder auf Objekte der Klassen ObjectQuery oder der Klasse ObjectSet aufgerufen. Im Zusammenhang mit Projektionen kann Include nicht verwendet werden.

Kommen wir nun zu unserem Beispiel von oben zurück und rufen die Produkte als Entitäten ab. Dazu übergeben wird den Bezeichner der Navigationseigenschaft Products als Zeichenfolge an die Methode Include. Um auf die Product-Entitäten zuzugreifen, können wir in der Ausgabe die Navigationseigenschaft nutzen, ohne dass es zu einem Server-Roundtrip kommt.

using (NorthwindEntities context = new NorthwindEntities())
{
var query = from cat in context.Categories.Include("Products")
select cat;
foreach (var item in query)
{
Console.WriteLine(item.CategoryName);
foreach (Product prod in item.Products)
Console.WriteLine(" {0}", prod.ProductName);
}
}

Listing 38.20 Eager Loading mit »Include«

Mit Load und Include lassen sich explizit in Beziehung stehende Daten nachladen. Sie werden immer wieder in die Situation kommen, eine Entscheidung zugunsten der einen oder anderen Variante treffen zu müssen. Load hat, wie oben erwähnt, den großen Nachteil, immer wieder Daten aus der Datenbank nachladen zu müssen. Der Datenbankserver wird damit unter Umständen erheblich belastet. Load genießt aber andererseits den Vorteil, dass man nur dann Daten einer Beziehung nachladen kann, wenn sie tatsächlich benötigt werden. Demgegenüber begnügt sich Include mit einem Aufruf, lädt aber alle Daten, ob Sie sie benötigen oder nicht. Die Entscheidung, ob Include oder Load, kommt der Entscheidung »Pest oder Cholera« gleich. Einen allgemeingültigen Tipp kann man nicht geben, es ist im Einzelfall zu entscheiden, welcher Variante man den Vorzug einräumt.



Ihre Meinung

Wie hat Ihnen das Openbook gefallen? Wir freuen uns immer über Ihre Rückmeldung. Schreiben Sie uns gerne Ihr Feedback als E-Mail an kommunikation@rheinwerk-verlag.de.

<< zurück
  Zum Rheinwerk-Shop
Zum Rheinwerk-Shop: Visual C# 2012

Visual C# 2012
Jetzt Buch bestellen


 Ihre Meinung?
Wie hat Ihnen das Openbook gefallen?
Ihre Meinung

 Buchempfehlungen
Zum Rheinwerk-Shop: Professionell entwickeln mit Visual C# 2012






 Professionell
 entwickeln mit
 Visual C# 2012


Zum Rheinwerk-Shop: Windows Presentation Foundation






 Windows Presentation
 Foundation


Zum Rheinwerk-Shop: Schrödinger programmiert C++






 Schrödinger
 programmiert C++


Zum Rheinwerk-Shop: C++ Handbuch






 C++ Handbuch


Zum Rheinwerk-Shop: C/C++






 C/C++


 Lieferung
Versandkostenfrei bestellen in Deutschland, Österreich und der Schweiz
InfoInfo





Copyright © Rheinwerk Verlag GmbH 2013
Für Ihren privaten Gebrauch dürfen Sie die Online-Version natürlich ausdrucken. Ansonsten unterliegt das Openbook denselben Bestimmungen, wie die gebundene Ausgabe: Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt.
Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung, Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen.


Nutzungsbestimmungen | Datenschutz | Impressum

Rheinwerk Verlag GmbH, Rheinwerkallee 4, 53227 Bonn, Tel.: 0228.42150.0, Fax 0228.42150.77, service@rheinwerk-verlag.de

Cookie-Einstellungen ändern