41.3 Änderungen verfolgen
41.3.1 Die Methode »DetectChanges«

Verwenden wir POCO-Klassen in unserer Anwendung, liefert uns das Abfrageergebnis abhängig von der Abfrage mehr oder weniger viele POCO-Objekte. Für jedes dieser Objekte erzeugt der Objektkontext ein ObjectStateEntry-Objekt. Das Verhalten unterscheidet sich also nicht von dem, wenn wir Entitäten verwenden, die von EntityObject abgeleitet sind. Trotzdem gibt es einen wichtigen Unterschied: EntityObject-basierte Objekte kommunizieren mit dem Objektkontext und teilen ihm automatisch mit, wenn sich eine Eigenschaft verändert hat. Der Objektkontext ist daher in der Lage, den Zustand der Objekte zu verfolgen und das zugehörige ObjectStateEntry-Objekt mit dem Objekt zu synchronisieren.
POCO-Objekte hingegen leiten nicht die Klasse EntityObject ab und sind auch nicht in der Lage, dem Objektkontext Informationen über Änderungen mitzuteilen. Daher ist die Zustandsverfolgung seitens des Objektkontextes zunächst einmal nicht möglich. Besondere Umstände verlangen besondere Maßnahmen. Es muss daher eine andere Möglichkeit geben, die es dem Objektkontext ermöglicht, die Daten eines POCO-Objekts mit denen des entsprechenden ObjectStateEntry-Objekts zu synchronisieren. Genau diese Aufgabe übernimmt die Methode DetectChanges des Objektkontextes. Die Arbeitsweise von DetectChanges zeigt das folgende Listing.
using (NorthwindContext context = new NorthwindContext())
{
Product p = (from prod in context.Products
where prod.CategoryID == 1
select prod).First();
ObjectStateEntry ose = context.ObjectStateManager.GetObjectStateEntry(p);
Console.WriteLine(ose.State);
// Änderung einer Eigenschaft
p.ProductName = "Möhren";
Console.WriteLine(ose.State);
// Änderung synchronisieren
context.DetectChanges();
Console.WriteLine(ose.State);
}
Listing 41.7 Einsatz der Methode »DetectChanges«
Wir fragen zuerst eine Datenzeile ab und lassen uns deren Zustand über das zugeordnete ObjectStateEntry-Objekt ausgeben. Er ist Unchanged. Danach ändern wir eine Eigenschaft und lassen uns erneut den Zustand der Entität ausgeben. Er ist immer noch Unchanged, obwohl sich offensichtlich eine Eigenschaft geändert hat. Das ObjectStateEntry-Objekt spiegelt in diesem Moment noch nicht den tatsächlichen Zustand der Entität wider.
Im dritten Schritt rufen wir die Methode DetectChanges auf. Erst danach wird das ObjectStateEntry-Objekt der geänderten POCO-Entität als Modified gekennzeichnet. Somit ist bewiesen, dass die Methode DetectChanges in der Lage ist, das POCO-Objekt mit seinem zugeordneten ObjectStateEntry zu synchronisieren.
Natürlich werden Sie die Änderung auch in die Datenbank schreiben wollen. Wie Sie sich erinnern, ist dafür die Methode SaveChanges des Objektkontextes zuständig. Sie müssen jetzt nicht glauben, dass aufgrund der zuvor gemachten Ausführungen die Methode DetectChanges vor dem SaveChanges-Aufruf ausgeführt werden muss, denn das geschieht implizit in der Methode SaveChanges. Sie können sich davon überzeugen, wenn Sie das folgende Listing testen.
using(NorthwindContext context = new NorthwindContext())
{
Product product = (from prod in context.Products
where prod.CategoryID == 1
select prod).First();
// Änderung einer Eigenschaft
product.ProductName = "Möhren";
context.SaveChanges();
}
Listing 41.8 Speichern einer geänderten POCO-Entität
Die Änderung wird tatsächlich in die Tabelle Products der Datenbank Northwind geschrieben, wie das Öffnen der Tabelle beispielsweise im SQL Server Management Studio beweist.
Sie finden das gesamte Projekt auf der Buch-DVD unter ...\Beispiele\Kapitel 41\POCO_ Sample2.
41.3.2 In Beziehung stehende POCOs aktualisieren

In einem Entity Data Modell, das auf Entitäten basiert, die von EntityObject abgeleitet sind, werden Änderungen auf beiden Seiten einer Beziehung automatisch verfolgt. Nehmen wir an, wir würden ein neues Produkt erstellen und es der EntityCollection der Kategorie zuordnen, zu der das neue Produkt gehört. Automatisch wird dann auch die EntityReference des neuen Produkts mit der entsprechenden Kategorie verknüpft.
Im folgenden Listing wird das demonstriert. Zur Laufzeit wird an der Konsole Beverages ausgegeben, also die Kategorie, zu der das Produkt gehört. Beachten Sie, dass in diesem Beispiel EntityObject-basierte Entitäten benutzt werden und keine POCOs.
using (NorthwindEntities context = new NorthwindEntities())
{
var cat = (from c in context.Categories.Include("Products")
select c).First(c => c.CategoryID == 1);
Product prod = new Product { ProductName = "Gurke",
Discontinued = false };
cat.Products.Add(prod);
Console.WriteLine(prod.Category.CategoryName);
}
Listing 41.9 Aktualisierung in Beziehung stehender Entitäten (keine POCOs)
Lassen Sie uns nun denselben Code in einem Entity Data Model ausführen, in dem die Entitäten durch POCOs beschrieben werden. Sie werden feststellen, dass in der Zeile der Konsolenausgabe eine Ausnahme vom Typ NullReferenceException ausgelöst wird, weil die Eigenschaft Category des neuen Produkts den Wert null aufweist. Das ist auch weiter nicht verwunderlich, da in den POCO-Klassen keine automatische Unterstützung der Beziehung codiert ist, die dafür sorgt, dass die Eigenschaft Category auf eine gültige Kategorie verweist – nämlich die, zu der das neue Produkt gehört.
Zur Lösung unseres Problems bieten sich drei Ansätze an:
- die Methode DetectChanges
- die Bereitstellung spezifischer Methoden in den POCO-Klassen
- Proxy-Objekte für POCO-Entitäten
Wir wollen uns diese drei Ansätze etwas genauer ansehen.
Variante 1: Die Methode »DetectChanges«
Die einfachste Lösung ist sicherlich die des Aufrufs der Methode DetectChanges, auf die ich weiter oben schon eingegangen bin. Bezogen auf das Beispiel im vorangehenden Listing müssen wir DetectChanges aufrufen, nachdem das neue Produkt der Liste aller Produkte hinzugefügt worden ist.
using (NorthwindContext context = new NorthwindContext())
{
...
cat.Products.Add(prod);
context.DetectChanges();
Console.WriteLine(prod.Category.CategoryName);
}
Listing 41.10 Objektkontext und POCO-Entität synchronisieren
Die Methode DetectChanges zwingt den Objektkontext dazu, die ObjectStateEntry-Objekte, die bekanntlich der Zustandsverfolgung dienen, zu aktualisieren. Gewissermaßen besorgt sich der Objektkontext damit einen Schnappschuss des aktuellen Zustands der skalaren Eigenschaften einer POCO-Entität. Darüber hinaus sorgt die Methode auch dafür, dass die gegenseitige Abhängigkeit der beteiligten POCO-Entitäten berücksichtigt wird – in unserem Beispiel also die der Category- und Product-Entität.
Es scheint verlockend, sich immer für diesen einfachen Ansatz zu entscheiden. Jedoch sollten Sie dabei immer im Auge behalten, dass die Operation von DetectChanges auf jede im Objektkontext befindliche Entität ausgeführt wird.
Variante 2: Spezifische Methoden in den POCO-Klassen
Einen besseren Ansatz verfolgt die folgende Variante. Dabei werden in den beiden POCO-Klassen Ergänzungen vorgenommen, die die Beziehung zwischen Category und Product hinsichtlich der gegenseitigen Abhängigkeit widerspiegeln.
Sehen wir uns zuerst die Änderung in der Klasse Category an. Hier wird eine benutzerdefinierte Methode bereitgestellt, die zwei Aufgaben bewerkstelligen muss:
- Sie muss das neue Produkt zur Liste aller Produkte der betreffenden Kategorie hinzufügen.
- Sie muss dem neuen Produkt mitteilen, zu welcher Kategorie es gehört.
In Listing 41.11 heißt die Methode AddProduct, kann aber auch beliebig anders benannt werden. Als Argument erwartet sie die Referenz auf das neue Produkt.
public class Category {
public int CategoryID { get; set; }
public string CategoryName { get; set; }
public string Description { get; set; }
public byte[] Picture { get; set; }
public virtual ICollection<Product> Products { get; set; }
public void AddProduct(Product prod)
{
if (Products == null) Products = new List<Product>();
if (!Products.Contains(prod)) Products.Add(prod);
if (prod.Category != this) prod.Category = this;
}
}
Listing 41.11 Anpassung der POCO-Klasse »Category«
Wichtig ist in der Methode AddProduct die letzte Prüfung, bei der untersucht wird, ob das neue Produkt bereits eine Referenz auf die entsprechende Kategorie hat. Vergessen Sie die Prüfung, befinden Sie sich (unter Berücksichtigung der noch zu ändernden Product-Klasse) in einer Endlosschleife.
In der POCO-Klasse Product müssen wir die automatische Eigenschaft Category auflösen, weil im set-Zweig eine zusätzliche Anweisung notwendig ist. In dieser rufen wir die AddProduct-Methode der Category-Referenz auf und übergeben ihr das neue Produkt.
public class Product {
public int ProductID { get; set; }
public string ProductName { get; set; }
public int SupplierID { get; set; }
public string QuantityPerUnit { get; set; }
public decimal UnitPrice { get; set; }
public short UnitsInStock { get; set; }
public short UnitsOnOrder { get; set; }
public short ReorderLevel { get; set; }
public bool Discontinued { get; set; }
public int CategoryID { get; set; }
private Category _Category;
public virtual Category Category
{
get { return _Category; }
set {
_Category = value;
_Category.AddProduct(this);
}
}
}
Listing 41.12 Anpassung der POCO-Klasse »Product«
Die Methode AddProduct muss in unserer Testanwendung natürlich berücksichtigt werden, wenn wir ein neues Produkt hinzufügen wollen.
using(NorthwindContext context = new NorthwindContext())
{
var cat = (from c in context.Categories.Include("Products")
select c).First(c => c.CategoryID == 1);
Product prod = new Product { ProductName = "Gurke",
Discontinued = false };
cat.AddProduct(prod);
Console.WriteLine(prod.Category.CategoryName);
}
Listing 41.13 Testen der POCO-Klassen der Listings 41.10 und 41.11
Sie finden das gesamte Projekt dieses Abschnitts auf der Buch-DVD unter ...\Beispiele\Kapitel 41\POCO_Sample3.
Variante 3: Proxy-Objekte für POCO-Entitäten
Auf sehr einfache Weise können Sie POCO-Objekte erstellen, die sich nicht von den üblichen EntityObject-Objekten unterscheiden. Dazu müssen Sie nur in der POCO-Klasse ausnahmslos jede Eigenschaft virtual kennzeichnen. Erfüllen Ihre POCO-Klassen diese Bedingung, wird das Entity Framework um die POCO-Klassen automatisch einen Wrapper vom Typ DynamicProxy erzeugen, der von der POCO-Klasse abgeleitet ist. Die Proxy-Klasse ist in der Lage, zur Laufzeit zahlreiche Features zur Verfügung zu stellen, die von EntityObject her bekannt sind. Dazu gehört neben dem Lazy Loading auch die für uns so wichtige Zustandsverwaltung. Der aktuelle Zustand kann mit der Ergänzung sofort über das ObjectStateEntry-Objekt abgefragt werden, und der Aufruf der Methode DetectChanges erübrigt sich.
public class Category {
public virtual int CategoryID { get; set; }
public virtual string CategoryName { get; set; }
public virtual string Description { get; set; }
public virtual byte[] Picture { get; set; }
public virtual ICollection<Product> Products { get; set; }
}
public class Product {
public virtual int ProductID { get; set; }
public virtual string ProductName { get; set; }
public virtual int SupplierID { get; set; }
public virtual string QuantityPerUnit { get; set; }
public virtual decimal UnitPrice { get; set; }
public virtual short UnitsInStock { get; set; }
public virtual short UnitsOnOrder { get; set; }
public virtual short ReorderLevel { get; set; }
public virtual bool Discontinued { get; set; }
public virtual int CategoryID { get; set; }
public virtual Category Category {get; set;}
}
Listing 41.14 Vorbereitung der POCO-Klassen zur Erstellung von Proxy-Objekten
Mit diesen Änderungen wird der Beispielcode aus Listing 41.6 auch dann einwandfrei funktionieren, wenn wir auf den Aufruf von DetectChanges verzichten.
using (NorthwindContext context = new NorthwindContext())
{
var cat = (from c in context.Categories.Include("Products")
select c).First(c => c.CategoryID == 1);
Product prod = new Product { ProductName = "Gurke",
Discontinued = false };
cat.Products.Add(prod);
Console.WriteLine(prod.Category.CategoryName);
}
Listing 41.15 Testen der Proxy-Objekte
Sie finden das gesamte Projekt dieses Abschnitts auf der Buch-DVD unter ...\Beispiele\Kapitel 41\POCO_Sample4.
Interessant ist es, wenn wir die Proxy-Klasse etwas genauer unter die Lupe nehmen. Dazu legen wir einen Haltepunkt in der Anweisung fest, in der die Konsolenausgabe codiert ist. Abbildung 41.2 zeigt den für uns wesentlichen Ausschnitt im Lokal-Fenster.
Abbildung 41.2 POCO-Objekt im Debugger
Wir müssen feststellen, dass es sich bei dem Objekt cat nicht einfach nur um ein Objekt vom Typ Category handelt. Tatsächlich wird der Typ des Objekts durch eine Kombination aus dem tatsächlichen Typ und einem Hash gebildet. Die Navigationseigenschaft Products hingegen wird auf den Typ EntityCollection<T> abgebildet.
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.