39 Entitätsaktualisierung und Zustandsverwaltung
Der Typ ObjectContext spielt im Entity Data Model eine eminent wichtige Rolle. Im vorherigen Kapitel haben wir ein Objekt dieses Typs dazu benutzt, einen Kontext zu beschreiben, um Abfragen gegen eine Datenbank abzusetzen. Die Resultate der Abfragen wurden materialisiert oder, mit anderen Worten, als Objekte an die Laufzeitumgebung zurückgeliefert.
Allein mit der Abfrage von Daten werden Sie sich auf die Dauer nicht zufrieden geben. Sie wollen die Daten oder besser gesagt Entitäten sicherlich auch verändern oder gar löschen oder neue Entitäten erzeugen. Diese Änderungen sollen natürlich auch in der Datenquelle gespeichert werden. Das wird der Schwerpunkt in diesem Kapitel sein. Darüber hinaus werden wir einen Blick in den Hintergrund des Entity Frameworks werfen und verstehen, warum neben dem ObjectContext und den Entitäten im Datencache weitere Objekte eine wichtige Rolle spielen.
39.1 Aktualisieren von Entitäten
39.1.1 Entitäten ändern
Änderungen an einer Entität vorzunehmen ist sehr einfach. Zuerst gilt es, sich die Referenz auf die zu editierende Entität zu besorgen. Dazu kann man ein ObjectQuery-Objekt entsprechend filtern oder das gewünschte Objekt direkt abfragen.
using (NorthwindEntities context = new NorthwindEntities())
{
var prod =(context.Products
.Select(p => p)).SingleOrDefault(p => p.ProductID == 1);
if (prod != null) {
prod.ProductName = "Aachener Printen";
prod.UnitsInStock = 0;
// Änderung in die Datenbank schreiben
context.SaveChanges();
Console.WriteLine("Datenbank aktualisiert...");
}
else
Console.WriteLine("Der Artikel wurde nicht gefunden.");
}
Listing 39.1 Änderung einer Entität mit anschließender Speicherung
Ein bestimmter Datensatz eines Produkts kann aus der Datenbank mit der Methode Single oder SingleOrDefault abgefragt werden. Beiden Methoden kann die Filterbedingung als Methodenargument übergeben werden (eine Alternative zur Where-Erweiterungsmethode). Single und SingleOrDefault unterscheiden sich hinsichtlich der Reaktion, wenn das entsprechende Produkt in der Datenbank nicht gefunden wird: Single löst eine Ausnahme aus, während SingleOrDefault den Rückgabewert null liefert.
In der LINQ-Abfrage von Listing 39.1 ist keine Selektion angegeben. Damit handelt es sich bei dem Resultat der LINQ-Abfrage um den Typ Product. Im Code werden die beiden Eigenschaften ProductName und UnitsInStock der Entität geändert.
Mit der Methode SaveChanges des Objektkontextes wird die an der Entität vorgenommene Änderung in die Datenbank geschrieben. Dabei wird die Änderung in ein passendes SQL-UPDATE-Statement umgesetzt. Sie können sich dieses ansehen, wenn Sie im SQL Server Management Studio das Tool SQL Server Profiler starten und ein neues Ablaufprotokoll starten. Sie werden dann das folgende SQL-Statement finden:
exec sp_executesql N'update [dbo].[Products]
set [ProductName] = @0, [UnitsInStock] = @1
where ([ProductID] = @2)
',N'@0 nvarchar(40),@1 smallint,@2 int',@0=N'Aachener Printen',@1=0,@2=1
Erwähnenswert ist, dass nur die Spalten im UPDATE-Statement angegeben sind, die auch tatsächlich einen neuen Wert aufweisen. In unserer Abfrage handelt es sich um ProductName und UnitsInStock. Die WHERE-Klausel hingegen enthält nur die Angabe des Primärschlüssels des betroffenen Datensatzes.
Mehrere Entitäten editieren
Mit SaveChanges werden alle Änderungen, die an einer Entität vorgenommen worden sind, in die Datenbank geschrieben. Nun möchten wir mehrere Entitäten gleichzeitig ändern. Dazu stellen wir uns vor, wir hätten die Absicht, den Lagerbestand aller Artikel, von denen aktuell 20 Stück oder weniger im Lager vorrätig sind, durch eine Nachbestellung auf 100 zu erhöhen. Natürlich könnten wir eine LINQ-Abfrage schreiben, die direkt die Produkte abfragt, die unserer Bedingung genügen. Lassen Sie uns aber annehmen, alle Produkte der Tabelle Products würden sich bereits im Datencache befinden, so dass wir eine weitere LINQ-Abfrage gegen den Datencache absetzen.
using (NorthwindEntities context = new NorthwindEntities())
{
// alle Produkte abfragen
var query = context.Products.Select(p => p).ToList();
// Produkte herausfiltern, deren Lagerbestand <= 20 ist
var prods = query
.Where(p => p.UnitsInStock <= 20)
.Select(p => p);
// Lagerbestand erhöhen
foreach (var item in prods)
item.UnitsInStock = 100;
int count = context.SaveChanges();
Console.WriteLine("Datenbank aktualisiert ({0} Datensätze).", count)
}
Listing 39.2 Lagerbestand mehrerer Artikel gleichzeitig erhöhen
Im ersten Schritt besorgen wir uns mit der Methode ToList alle Artikel. Die Methode sorgt dafür, dass die Abfrage sofort gegen die Datenbank abgesetzt und die Ergebnismenge gebildet wird. Anschließend bilden wir die Menge der Produkte, die unserer Bedingung hinsichtlich des Lagerbestands entsprechen, und erhöhen diesen auf 100.
Die Methode SaveChanges hat einen Rückgabewert, der darüber Auskunft gibt, wie viele Entitäten von der Änderung betroffen sind. Diesen Wert lassen wir uns an der Konsole zur Information ausgeben.
39.1.2 Hinzufügen neuer Entitäten
Um eine neue Entität zu erzeugen und diese so weit vorzubereiten, dass sie in die Datenbank geschrieben werden kann, sind zwei Schritte notwendig:
- Im ersten Schritt muss die neue Entität erstellt werden. Dazu wird entweder der Konstruktor aufgerufen oder die CreateXyz-Methode, die von jeder Entitätsklasse bereitgestellt wird.
- Ist eine neue Entität erzeugt, hat sie noch keinen Bezug zum Objektkontext und liegt noch verwaist im Heap. Daher ist die neue Entität im zweiten Schritt dem Objektkontext bekannt zu geben, damit dieser zu einem späteren Zeitpunkt das Objekt in die Datenbank schreiben kann.
Erzeugen einer neuen Entität
Sehen wir uns zuerst den ersten Schritt an, die Erzeugung eines neuen Entitätsobjekts. Dazu können Sie den parameterlosen Konstruktor der Entitätsklassen aufrufen und weisen den Eigenschaften die gewünschten Werte zu, beispielsweise:
Product product = new Product();
product.UnitsInStock = 0;
product.ProductName = "Schokolade";
product.Discontinued = false;
Neben dem parameterlosen Standardkonstruktor ist in den Entitätsklassen auch eine statische Methode definiert (Factory-Methode), die eine neue Entität der entsprechenden Entitätsklasse erzeugt und deren Referenz über den Rückgabewert bereitstellt. In der Klasse Product lautet diese Methode CreateProduct, in der Klasse Category analog dazu CreateCategory.
Das folgende Codefragment zeigt exemplarisch die vom Assistenten generierte Methode CreateProduct in der Klasse Product.
public static Product CreateProduct(global::System.Int32 productID,
global::System.String productName,
global::System.Boolean discontinued)
{
Product product = new Product();
product.ProductID = productID;
product.ProductName = productName;
product.Discontinued = discontinued;
return product;
}
Die Factory-Methoden haben im Gegensatz zum Standardkonstruktor den Vorteil, eine Parameterliste zu definieren, in der neben dem Primärschlüssel auch die Eigenschaften berücksichtigt werden, die nicht null sein dürfen. Für die Entität vom Typ Product sind das die Eigenschaften ProductName und Discontinued. Daher erwartet die Factory-Methode Werte für diese beiden Eigenschaften, z. B.:
Product newProduct = Product.CreateProduct(12, "Wurst", false);
Die Factory-Methode garantiert also, dass eine Entität erzeugt wird, die von der Datenbank bei der späteren Aktualisierung in jedem Fall akzeptiert wird, während beim Einsatz des Konstruktors den Eigenschaften explizit ein Wert zugewiesen werden muss.
Hinzufügen zum Objektkontext mit der Methode »AddToXxx«
Nach dem Erzeugen einer neuen Entität muss diese dem Objektkontext übergeben werden, da nur die Entitäten beim Aufruf der Methode SaveChanges erfasst werden, die dem Objektkontext bekannt sind.
Für jede im Entity Data Model definierte Entitätsklasse stellt das Entity Framework dem ObjectContext dazu eine spezielle Methode bereit. In unserem Beispiel beschreibt das Entity Data Model (EDM) die beiden Entitäten Categories und Products. Infolgedessen stellt der Objektkontext die beiden Methoden
- AddToCategories(Category category)
- AddToProducts(Product product)
bereit. Die neue Entität wird dabei der Methode als Argument übergeben.
Listing 39.3 zeigt im Zusammenhang das Erzeugen eines neuen Artikels, das anschließende Hinzufügen zum Objektkontext und die Aktualisierung der Datenbank.
using (NorthwindEntities context = new NorthwindEntities())
{
Product product = new Product();
product.ProductName = "Schokolade";
product.Discontinued = false;
// oder: Product product = Product.CreateProduct(99, "Kuchen", false);
context.AddToProducts(product);
context.SaveChanges();
Console.WriteLine("Datenbank aktualisiert...");
Console.WriteLine("Neue ID: {0}", product.ProductID);
}
Listing 39.3 Erzeugen einer neuen Entität mit anschließender DB-Aktualisierung
Im ersten Augenblick scheint der Code des Listings nichts Besonderes zu bieten. Spektakulär ist aber beim genaueren Hinsehen die Ausgabe am Ende des Listings. Zur Erinnerung: Der Primärschlüssel der Tabelle Products wird durch einen Autoinkrementwert beschrieben, der natürlich von der Datenbank erzeugt wird. An der Konsole wird nach der Aktualisierung tatsächlich der Wert angezeigt, den die Datenbank für den neuen Datensatz generiert hat. Demnach wird beim erfolgreichen Hinzufügen einer neuen Entität der zugeteilte Primärschlüssel automatisch vom ObjectContext übernommen. Ein sehr nettes Feature, das uns einiges an Programmcode erspart.
Hinzufügen zum Objektkontext mit der Methode »AddObject«
Es gibt mit der Methode AddObject noch eine zweite Variante, ein neues Objekt dem Objektkontext zu übergeben. Die Methode wird von zwei Klassen bereitgestellt:
- ObjectContext
- ObjectSet
Beide Varianten unterscheiden sich nur in der Anzahl der erwarteten Argumente. Während AddObject beim Aufruf auf die Instanz des Objektkontextes die Angabe des Bezeichners der Entitätenmenge und der Referenz auf die hinzuzufügende Entität erwartet, begnügt sich die Methode beim Aufruf auf ObjectSet nur mit der Angabe der neuen Entität.
Um ein neues Produkt, dessen Referenz product lautet, dem Objektkontext bekannt zu geben, können Sie demnach entweder die Anweisung
context.Products.AddObject(product);
oder
context.AddObject("Products", product);
codieren.
Wird ein Datensatz mit einem bereits existierenden Primärschlüssel zur Datenbank hinzugefügt, wird eine InvalidOperationException ausgelöst. Das kann jedoch im Zusammenhang mit den beiden Entitäten Product und Category nicht passieren, da hier die Primärschlüssel durch Autoinkrementwerte beschrieben werden.
Hinzufügen zu Master- und Detailtabelle
Im praktischen Alltag werden häufig Master- und Detailtabelle gleichzeitig ergänzt. Angenommen, es soll der Tabelle Products ein Produkt hinzugefügt werden, das einer Kategorie zugeordnet werden muss, die noch nicht von der Tabelle Categories beschrieben wird. In üblichen Szenarien wird man zuerst die neue Kategorie in die Tabelle Categories eintragen und dann den neuen Schlüsselwert abrufen. Mit diesem kann anschließend auch das Produkt in die Tabelle Products eingetragen werden.
Das Entity Framework ist wesentlich intelligenter, denn SaveChanges macht das alles selbstständig. Das sehen wir uns auch sofort an einem Beispiel an.
using (NorthwindEntities context = new NorthwindEntities())
{
Category newCat = Category.CreateCategory(-1, "Backwaren");
Product newProduct = new Product();
newProduct.ProductID = 0;
newProduct.ProductName = "Jubel's Eierkuchen";
newProduct.Discontinued = false;
newProduct.Category = newCat;
context.AddToCategories(newCat);
context.SaveChanges();
Console.WriteLine("Datenbank aktualisiert ...");
}
Listing 39.4 Gleichzeitige Aktualisierung von Master- und Detailtabelle
Im Listing werden zuerst eine neue Kategorie und ein neuer Artikel erzeugt. Die Referenz auf die neue Kategorie wird der Eigenschaft Category des Artikels zugewiesen, und danach wird die neue Kategorie mit der Methode AddToCategories dem Objektkontext bekannt gemacht. Weil der neue Artikel seinerseits selbst mit der neuen Kategorie in Beziehung steht, ordnet er sich ebenfalls automatisch dem Objektkontext zu.
Der Objektkontext weiß nun, dass zwei Entitäten hinzugefügt worden sind, die miteinander in Beziehung stehen, und aktualisiert beim Aufruf von SaveChanges in der erforderlichen Reihenfolge: Zuerst fügt er die neue Kategorie zur Datenbank hinzu, anschließend das neue Produkt.
Davon können wir uns im SQL Server Profiler überzeugen. Es werden der Reihe nach die entsprechenden SQL-UPDATE-Statements abgesetzt. Als Erstes wird
exec sp_executesql N'insert [dbo].[Categories]([CategoryName],
[Description], [Picture])
values (@0, null, null)
select [CategoryID]
from [dbo].[Categories]
where @@ROWCOUNT > 0 and [CategoryID] = scope_identity()',
N'@0 nvarchar(15)',@0=N'Backwaren'
abgesetzt, danach
exec sp_executesql N'insert [dbo].[Products]([ProductName], ...)
values (@0, null, @1, null, null, null, null, null, @2)
select [ProductID]
from [dbo].[Products]
where @@ROWCOUNT > 0 and [ProductID] = scope_identity()',N'@0 nvarchar(40),@1 int,@2
bit',@0=N'Jubel''s Eierkuchen',@1=10,@2=0
Auch hier werden automatisch die neuen, von der Datenbank generierten Primärschlüssel bezogen.
39.1.3 Löschen einer Entität
Kommen wir nun zur dritten Aktualisierungsmöglichkeit, dem Löschen. Die Methode dazu lautet DeleteObject, die Sie entweder auf das ObjectContext-Objekt aufrufen können oder auf das ObjectSet. Damit wären die beiden folgenden Anweisungen möglich, falls product die Referenz auf die zum Löschen anstehende Entität beschreibt:
context.DeleteObject(product);
context.Products.DeleteObject(product);
Natürlich wollen wir uns auch dazu ein komplettes Listing ansehen.
using (NorthwindEntities context = new NorthwindEntities())
{
var prod = context.Products
.Select(p => p).First(p => p.ProductID == 78);
context.DeleteObject(prod);
context.SaveChanges();
Console.WriteLine("Artikel gelöscht.");
}
Listing 39.5 Löschen einer Entität
Nach dem Aufruf von SaveChanges ist das betreffende Produkt in der Datenbank gelöscht.
Sollten Sie das Beispiel des Listings 39.5 erfolgreich ausführen wollen, sollten Sie berücksichtigen, dass Sie vorher auch einen neuen Artikel mit der ProductID = 78 hinzugefügt haben, denn ursprünglich weist die Tabelle Products nur Produkte bis zum Schlüsselwert 77 auf.
Löschen von in Beziehung stehenden Daten
Beim Löschen eines Datensatzes kann es passieren, dass gegen die referenzielle Integrität verstoßen wird, weil der zu löschende Datensatz in Beziehung zu mindestens einem weiteren Datensatz in einer anderen Tabelle steht und die Beziehung eine Löschweitergabe nicht erlaubt. In diesem Fall wird eine Ausnahme vom Typ UpdateException ausgelöst. Das würde beispielsweise bei dem Versuch passieren, aus der Northwind-Datenbank einen Artikel zu löschen, der Bestandteil einer Bestellung und somit in der Tabelle Order_Details eingetragen ist.
Um trotzdem das Ziel zu erreichen und den Artikel zu löschen, ist es notwendig, auch die in Beziehung stehenden Einträge aus der Tabelle Order_Details zu löschen. Dazu wäre zuerst ein entsprechendes Entity Data Model Voraussetzung, in dem zumindest die beiden Entitäten Product und Order_Detail enthalten sind. Sie können dazu das bereits vorhandene EDM um die erforderliche zusätzliche Entität ergänzen, indem Sie im Bereich des Designers mit der rechten Maustaste das Kontextmenü öffnen und hier Modell aus der Datenbank aktualisieren... auswählen. Bereits im nächsten Schritt können Sie das EDM um die Entität Order_Details ergänzen (siehe Abbildung 39.1).
Abbildung 39.1 Entity Data Model (EDM) für das Listing 39.6
Den Ablauf zum Löschen des Artikels sehen Sie im Listing 39.6.
using (NorthwindEntities context = new NorthwindEntities())
{
var prod = context.Products.First(p => p.ProductID == 2);
var orders = context.Order_Details
.Where(order => order.ProductID == prod.ProductID);
context.DeleteObject(prod);
foreach (var item in orders)
context.DeleteObject(item);
context.SaveChanges();
Console.WriteLine("Artikel (samt der Bestellungen) gelöscht.");
}
Listing 39.6 Löschen von in Beziehung stehenden Daten
Zuerst wird das zu löschende Produkt in den Objektkontext geladen, und mit dessen ID-Wert werden die entsprechenden Bestellungen abgerufen. Normalerweise würde man zuerst die Bestellungen löschen und anschließend das Produkt, aber beim Entity Framework spielt die Löschreihenfolge keine Rolle: Der Objektkontext sorgt für die richtige Löschreihenfolge beim Ausführen der Methode SaveChanges. Sie können sich davon wieder im Ablaufverfolgungsprotokoll des SQL Server Profilers überzeugen. Demnach werden zuerst alle Bestellungen mit
exec sp_executesql N'delete [dbo].[Order Details]
where (([OrderID] = @0) and ([ProductID] = @1))',
N'@0 int,@1 int',@0=11077,@1=2
gelöscht. Anschließend kommt es zum Löschen des entsprechenden Artikels.
exec sp_executesql N'delete [dbo].[Products]
where ([ProductID] = @0)',N'@0 int',@0=2
Ist die Beziehung mit Löschweitergabe zwischen zwei Tabellen definiert, ist es sehr einfach, diese zu nutzen. Angenommen, in der Northwind-Datenbank wäre das bei der Beziehung zwischen den beiden Tabellen Products und Order_Details der Fall. Dann würde es genügen, die betreffende Product-Entität im Objektkontext zu löschen und die Methode SaveChanges aufzurufen.
var product = context.Products.First(prod => prod.ProductID == 32);
context.DeleteObject(product);
context.SaveChanges();
In der Datenbank würden automatisch alle entsprechenden Einträge in der Tabelle Order_Details ebenfalls gelöscht.
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.