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

Inhaltsverzeichnis
Vorwort zur 5. Auflage
1 Allgemeine Einführung in .NET
2 Grundlagen der Sprache C#
3 Klassendesign
4 Vererbung, Polymorphie und Interfaces
5 Delegates und Ereignisse
6 Weitere .NET-Datentypen
7 Weitere Möglichkeiten von C#
8 Auflistungsklassen (Collections)
9 Fehlerbehandlung und Debugging
10 LINQ to Objects
11 Multithreading und die Task Parallel Library (TPL)
12 Arbeiten mit Dateien und Streams
13 Binäre Serialisierung
14 Einige wichtige .NET-Klassen
15 Projektmanagement und Visual Studio 2010
16 XML
17 WPF – Die Grundlagen
18 WPF-Containerelemente
19 WPF-Steuerelemente
20 Konzepte der WPF
21 Datenbindung
22 2D-Grafik
23 ADO.NET – verbindungsorientierte Objekte
24 ADO.NET – Das Command-Objekt
25 ADO.NET – Der SqlDataAdapter
26 ADO.NET – Daten im lokalen Speicher
27 ADO.NET – Aktualisieren der Datenbank
28 Stark typisierte DataSets
29 LINQ to SQL
30 Weitergabe von Anwendungen
Stichwort

Buch bestellen
Ihre Meinung?

Spacer
<< zurück
Visual C# 2010 von Andreas Kühnel
Das umfassende Handbuch
Buch: Visual C# 2010

Visual C# 2010
geb., mit DVD
1295 S., 49,90 Euro
Rheinwerk Computing
ISBN 978-3-8362-1552-7
Pfeil 29 LINQ to SQL
Pfeil 29.1 Allgemeine Grundlagen
Pfeil 29.2 Objektzuordnung mit Entitätsklassen
Pfeil 29.3 Mapping von Objekten
Pfeil 29.3.1 Das »Table«-Attribut
Pfeil 29.3.2 Das »Column«-Attribut
Pfeil 29.4 Verknüpfungen zwischen Entitäten
Pfeil 29.4.1 Der Typ »EntityRef<T>«
Pfeil 29.4.2 Verzögertes Laden
Pfeil 29.4.3 Der Typ »EntitySet<T>«
Pfeil 29.4.4 Ein weiteres Beispiel
Pfeil 29.4.5 Sofortiges Laden der Daten
Pfeil 29.5 Tools zur Erzeugung von Entitätsklassen
Pfeil 29.6 Die Klasse »DataContext«
Pfeil 29.6.1 Verbindungsaufbau
Pfeil 29.6.2 Daten abfragen
Pfeil 29.6.3 Von einer LINQ-Abfrage erzeugtes SQL-Statement ausgeben
Pfeil 29.6.4 Aktualisieren der Daten
Pfeil 29.6.5 Konflikte behandeln
Pfeil 29.7 Der LINQ to SQL-Designer (O/R-Designer)
Pfeil 29.7.1 Handhabung des O/R-Designers
Pfeil 29.7.2 Die abgeleitete »DataContext«-Klasse
Pfeil 29.7.3 Entitätsklassen


Galileo Computing - Zum Seitenanfang

29.6 Die Klasse »DataContext« Zur nächsten ÜberschriftZur vorigen Überschrift

Die schon öfter erwähnte Klasse DataContext bildet die Schnittstelle zwischen der relationalen Datenbank und der Anwendung. Eine Instanz von DataContext hat zuerst immer die Aufgabe, die Verbindung zur Datenbank aufzubauen. Darüber hinaus werden die benötigten Daten aus der Datenbank gelesen und in den dafür vorgesehenen Entitätsobjekten abgelegt. Damit sind aber noch nicht alle Aufgaben beschrieben. Das DataContext-Objekt verfolgt die Änderung an den Daten im Speicher und übermittelt diese bei Bedarf an die Datenbank. Zusammenfassend kann man sagen, dass die DataContext-Instanz den Einstiegspunkt in LINQ to SQL bildet und die Kommunikation zwischen der Datenbank und der Anwendung abwickelt.


Galileo Computing - Zum Seitenanfang

29.6.1 Verbindungsaufbau Zur nächsten ÜberschriftZur vorigen Überschrift

Bei der Instanziierung der Klasse DataContext müssen Sie dem Konstruktor die Datenbankverbindung als Argument übergeben. Dafür eignet sich sowohl ein SqlConnection-Objekt als auch eine Zeichenfolge, die der der Klasse SqlConnection gleicht, zum Beispiel:


string con = "Data Source=.\sqlexpress;Initial Catalog=Northwind;
              Integrated Security=True";
DataContext context = new DataContext(con);

DataContext besitzt zwar auch eine Eigenschaft Connection, diese ist aber schreibgeschützt.


Galileo Computing - Zum Seitenanfang

29.6.2 Daten abfragen Zur nächsten ÜberschriftZur vorigen Überschrift

Das DataContext-Objekt hat die Fähigkeit, mit der Methode GetTable<TEntity> Daten von der Datenbank abzufragen. Bei dem Abfrageergebnis handelt es sich um eine Collection vom Typ Table<TEntity>. Möchten Sie zum Beispiel die Tabelle der Produkte abfragen, müsste die Anweisung


Table<Product> products = context.GetTable<Product>();

lauten. Das setzt natürlich voraus, dass in der Anwendung die Entitätsklasse Product definiert ist.

Der Rückgabetyp Table<Product> ist vergleichbar mit einer Collection des Typs List<Products>, ist aber auf die Belange von LINQ to SQL hin optimiert. Er enthält nach Absetzen der LINQ-Abfrage alle gemappten Datensätze, die vom Typ Product sind.

Auf die Ergebnisliste products können wir alle Standard-Abfrageoperationen anwenden. Die folgende Anweisung fragt alle Artikelbezeichner und deren Preis in der Datenbank ab:


// alle Datensätze abrufen
var query = from prod in products
            select new { prod.ProductName, prod.UnitPrice };
foreach (var item in query)
  Console.WriteLine("{0,-35}{1}",item.ProductName, item.UnitPrice);

Tatsächlich ausgeführt wird die Abfrage erst in dem Moment, wenn die Daten in der Schleife angefordert werden.


Galileo Computing - Zum Seitenanfang

29.6.3 Von einer LINQ-Abfrage erzeugtes SQL-Statement ausgeben Zur nächsten ÜberschriftZur vorigen Überschrift

Um die Datenbank abzufragen, verwendet LINQ to SQL die Definition der LINQ-Abfrage, um daraus ein für die Datenbank verständliches SQL-Statement zu erzeugen. Sie können sich dieses anzeigen lassen, wenn Sie mit der Methode GetCommand unter Angabe der Abfrage die CommandText-Eigenschaft aufrufen, beispielsweise so:


Console.WriteLine(context.GetCommand(query).CommandText);

Das Ergebnis für unsere Abfrage wird wie folgt lauten:


SELECT [t0].[ProductName], [t0].[UnitPrice]
FROM [dbo].[Products] AS [t0]

Dass nicht alle Felder abgefragt werden, liegt daran, dass wir nur die Felder ProductName und UnitPrice in das Ergebnis projiziert haben (siehe das Codefragment im vorigen Abschnitt). LINQ to SQL bewertet also unsere Anforderung an die Datenbank und liefert nur die wirklich benötigten Ergebnisse. Für Sie bedeutet das aber auch, dass Sie sich keine Gedanken darüber machen müssen, welche Datenbankfelder Sie in der Entitätsklasse abbilden müssen. Sprechen nicht wichtige Gründe dagegen, können Sie bedenkenlos alle abbilden. Auf die Performance und den Netzwerkverkehr hat das keinen Einfluss.

Zwei weitere Möglichkeiten, um das von LINQ to SQL erzeugte SQL-Statement abzurufen, möchte ich Ihnen nicht vorenthalten. Die erste benutzt die Eigenschaft Log des DataContext-Objekts. Diese Eigenschaft leitet die Ausgabe an einen TextWriter um, bei dem es sich beispielsweise um Console.Out handeln könnte:


context.Log = Console.Out; 

Sie können auch mittels Reflection das SQL-Statement als Zeichenfolge abrufen, wie die folgende Anweisung zeigt. Den Namenspace System.Reflection sollten Sie vorher importieren.


string strSQL = context.GetType().
    GetMethod("GetChangeText", BindingFlags.Instance |   
    BindingFlags.NonPublic).Invoke(context, null) as string;


Galileo Computing - Zum Seitenanfang

29.6.4 Aktualisieren der Daten Zur nächsten ÜberschriftZur vorigen Überschrift

Unter Verwendung des DataContext-Objekts können wir Datensätze ändern, löschen und hinzufügen. Das DataContext-Objekt verfolgt alle Änderungen und kann durch einen einzigen Methodenaufruf die Datenbank aktualisieren.

Daten ändern

Sehen wir uns am Anfang ein Beispiel an, in dem wir alle Artikel der Products-Tabelle, deren Preis höher als 50 EUR ist, um 10% billiger anbieten. Dazu besorgen wir uns zuerst eine Liste aller Artikel, die der Bedingung UnitPrice > 50 genügen. Das soll uns jedoch noch nicht genügen. Wir wollen uns die geänderten Datensätze auch an der Konsole ausgeben lassen, ehe wir sie in die Datenbank schreiben.


// ----------------------------------------------------------------
// ...\Beispiele\Kapitel 29\Aktualisieren-Mit_LINQ_to_SQL\Editieren
// ----------------------------------------------------------------
string con = "...";
DataContext context = new DataContext(con);
Table<Product> products = context.GetTable<Product>();
// alle Datensätze abrufen
var query = from prod in products
            where prod.UnitPrice > 50
            select prod;
foreach (Product prod in query)
  Console.WriteLine("{0,-35}{1}", prod.ProductName, prod.UnitPrice);
Console.WriteLine();
// Preis reduzieren
foreach (var item in query)
  item.UnitPrice = item.UnitPrice * 0.9m; 
// Abrufen der geänderten Datensätze aus der 'products'-Liste
ChangeSet set = context.GetChangeSet();
var liste = set.Updates;
Console.WriteLine("Geänderte Datensätze");
foreach(Product prod in liste)
  Console.WriteLine("{0,-35}{1}", prod.ProductName, prod.UnitPrice); 
// Datenbank aktualisieren
context.SubmitChanges();
Console.WriteLine("Datenbank ist aktualisiert ...");

Die Datensätze, die wir ändern wollen, müssen in der Ergebnisliste der LINQ-Abfrage stehen. In einer Schleife wird die Liste Element für Element durchlaufen und der Preis jedes Produkts verringert. Der Code sollte auch ohne weitere Erklärungen verständlich sein.

Das DataContext-Objekt verfolgt die Änderungen an den Datensätzen. Die protokollierten Änderungen können wir mit der Methode GetChangeSet abrufen, die ein Objekt vom Typ ChangeSet zurückliefert. Das ChangeSet-Objekt ist ein Container, der drei Listen beherbergt: die der geänderten Datensätze, die der gelöschten und die der neu hinzugefügten. Auf diese Listen, die vom Typ List<Object> sind, haben wir Zugriff über die Eigenschaften Updates, Deletes und Inserts. Mit


ChangeSet set = context.GetChangeSet();
var liste = set.Updates;

wird im Code die Liste aller von der Änderung betroffenen Datensätze abgerufen und in der Variablen liste gespeichert. Da wir wissen, dass die Liste nur Product-Entitäten enthält, können wir in der foreach-Schleife die Artikel mit ihrem neuen Preis abfragen.

Zu diesem Zeitpunkt sind nur die Daten im lokalen Speicher von der Änderung betroffen. Um auch die Datenbank zu aktualisieren, müssen Sie nur die SubmitChanges-Methode des DataContext-Objekts aufrufen:


context.SubmitChanges();

Das Aktualisierungsverhalten des DataContext-Objekts erinnert sehr an das Verhalten eines DataSet, das zunächst ebenfalls erst alle Änderungen intern speichert.

Im Beispiel sind von der Änderung des Preises mehrere Entitäten gleichzeitig betroffen. Soll nur ein Datensatz editiert werden, hilft die Erweiterungsmethode Single weiter. Im folgenden Beispiel wird ein Datensatz unter Angabe des Primärschlüssels ausgewählt:


var query = products.Single(prod => prod.ProductID == 17);

Single hat aber einen Nachteil. Wird der gesuchte Datensatz in der Datenbank nicht gefunden, wird eine Exception ausgelöst, die Sie mit einer try/catch-Fehlerbehandlung behandeln müssen. Verwenden Sie anstelle von Single die nahezu gleichwertige Methode SingleOrDefault, können Sie auf die Fehlerbehandlung verzichten. SingleOrDefault löst keine Ausnahme aus, wenn der Datensatz nicht gefunden wird, sondern liefert null zurück.


var query = products.SingleOrDefault(prod => prod.ProductID == 171789);
if (query != null) {
  Console.Write("'{0}' ändern in ", query.ProductName);
  query.ProductName = Console.ReadLine();
  context.SubmitChanges();
  Console.WriteLine("Datenbank ist aktualisiert ...");
}
else {
  Console.WriteLine("Datensatz nicht gefunden.");
}

Beachten Sie beim Einsatz der Methoden Single bzw. SingleOrDefault, dass der Rückgabewert vom Typ Product ist. Sie können damit direkt auf die Spalten zugreifen und deren Inhalt, wie im Codefragment gezeigt, ändern.

Datensatz hinzufügen

Um einen neuen Datensatz in die Datenbank zu schreiben, benötigen wir zuerst ein Entitätsobjekt zur Übergabe der neuen Daten an das DataContext-Objekt. Anschließend weisen wir dem Objekt die neuen Werte zu und fügen es der Liste aller Entitäten hinzu. Dazu rufen Sie die Methode InsertOnSubmit auf der Entitätenliste products auf. Die Methode SubmitChanges fügt den neuen Datensatz der Datenbank hinzu.

Den Programmcode, der erforderlich ist, um ein neues Entitätsobjekt zu erzeugen und als Datensatz in der Datenbank zu speichern, können Sie dem folgenden Beispielprogramm entnehmen:


// ------------------------------------------------------------------
// ...\Beispiele\Kapitel 29\Aktualisieren-Mit_LINQ_to_SQL\Hinzufügen
// ------------------------------------------------------------------
string con = "...";
DataContext context = new DataContext(con);
Table<Product> products = context.GetTable<Product>();
// neue 'Product'-Entität
Product newProduct = new Product();
newProduct.ProductName = "Kartoffelsalat";
newProduct.Discontinued = true;
// zur Liste hinzufügen
products.InsertOnSubmit(newProduct);
// Datenbank aktualisieren
context.SubmitChanges();
Console.WriteLine("Datensatz hinzugefügt.");
Console.ReadLine();

Datensatz löschen

Jetzt fehlt uns nur noch das Löschen eines Datensatzes. Es können nur Datensätze gelöscht werden, die in der vom DataContext-Objekt gefüllten Entitätsliste enthalten sind. Das Prinzip des Löschens ähnelt dem des Änderns. Zuerst muss der zu löschende Datensatz ermittelt werden. Hierzu bieten sich wieder die beiden Methoden Single und SingleOrDefault an. Die Methode DeleteOnSubmit, die auf der Liste der Product-Entitäten aufgerufen wird, markiert den Datensatz als zur Löschung anstehend. SubmitChanges löscht den Datensatz endgültig in der Datenbank.


// ----------------------------------------------------------------
// ...\Beispiele\Kapitel 29\Aktualisieren-Mit_LINQ_to_SQL\Löschen
// ----------------------------------------------------------------
string con = "...";
DataContext context = new DataContext(con);
Table<Product> products = context.GetTable<Product>();
// zu löschenden Datensatz suchen
var prod = products
       .SingleOrDefault(delProd => delProd.ProductID == 13);
if (prod != null) {
  // Datensatz markieren
  products.DeleteOnSubmit(prod);
  // Datenbank aktualisieren
  context.SubmitChanges();
  Console.WriteLine("Datensatz gelöscht.");
}
else {
  Console.WriteLine("Datensatz nicht gefunden.");
}
Console.ReadLine();


Galileo Computing - Zum Seitenanfang

29.6.5 Konflikte behandeln topZur vorigen Überschrift

Schreiben wir eine Anwendung für einen Benutzer, brauchen wir uns nicht allzu viele Gedanken um eventuell auftretende Konflikte zu machen. Aber der Alltag sieht meist anders aus, denn die Systeme werden von mehreren Usern gleichzeitig benutzt. In diesen Situationen muss damit gerechnet werden, dass zwei oder noch mehr Benutzer gleichzeitig denselben Datensatz bearbeiten. Da LINQ to SQL genauso wie auch ADO.NET mit verbindungslosen Daten arbeitet, müssen Sie als Entwickler die potenziell möglichen Konfliktszenarien berücksichtigen.

Beim Zurückschreiben gilt per Vorgabe von LINQ to SQL das First-In-Wins-Szenario. Mit anderen Worten: Der erste Benutzer, der einen geänderten Datensatz in der Datenbank aktualisiert, hat Erfolg. Der andere Benutzer wird bei dem Versuch scheitern, seine eigene Änderung an demselben Datensatz in die Datenbank zu schreiben. Er wird zwar darüber informiert, dass die Daten zwischenzeitlich in der Datenbank geändert wurden, aber der beabsichtigte Speichervorgang kann nicht ausgeführt werden. Dieses Konfliktverhalten wird auch als Optimistic Concurrency bezeichnet.

Konfliktverhalten steuern

Bereits beim Bereitstellen der Entitätsklassen haben wir bisher, ohne es zu wissen, Optimistic Concurrency eingestellt. Rufen wir die Methode SubmitChanges auf dem DataContext-Objekt auf, wird automatisch ein passendes SQL-Statement erzeugt, in dem alle Spalten der SELECT-Abfrage in der WHERE-Klausel zur Identifizierung des Datensatzes in der Datenbanktabelle angegeben werden.

Nehmen wir an, Sie würden die Tabelle Products durch die Entitätsklasse Product abbilden. In der Klasse seien nur die drei Spalten ProductID, ProductName und UnitPrice beschrieben. Ändern Sie an einem der gemappten Datensätze im Feld ProductName den Wert, würde LINQ to SQL das folgende parametrisierte UPDATE-Statement erzeugen:


UPDATE [dbo].[Products]
SET [ProductName] = @p3
WHERE ([ProductID] = @p0) AND 
      ([ProductName] = @p1) AND 
      ([UnitPrice] = @p2)

Die Parameter werden mit den entsprechenden Daten des betroffenen Datensatzes gefüllt, wobei die Parameter in der WHERE-Klausel die ursprünglichen Originalwerte aus der Datenbank beschreiben. Wird in der Datenbank kein Datensatz gefunden, der den durch WHERE beschriebenen Kriterien entspricht, kann auch keine Aktualisierung erfolgen, was als Konflikt bewertet wird. Anschließend wird eine ChangeConflictException ausgelöst.

Sie können das Aktualisierungsverhalten beliebig an Ihre eigenen Anforderungen anpassen. Dazu müssen Sie das Attribut Column, das mit jeder Spalte der von der Entitätsklasse beschriebenen Tabelle verknüpft ist, um den Parameter UpdateCheck ergänzen, beispielsweise:


[Column(Storage="_ProductName", 
        DbType="NVarChar(40) NOT NULL", 
        CanBeNull=false, 
        UpdateCheck=UpdateCheck.WhenChanged)]
public string ProductName {
  ...
}

Dieser Parameter kann drei Werte annehmen, die durch die Enumeration UpdateCheck beschrieben werden. Die möglichen Werte können Sie Tabelle 29.3 entnehmen.


Tabelle 29.3 Die Mitglieder der Enumeration »UpdateCheck«

Konstante Beschreibung
Always

Die Spalte wird immer in die WHERE-Klausel einbezogen.

Never

Die Spalte wird nie in die WHERE-Klausel einbezogen.

WhenChanged

Die Spalte wird nur bei einer Änderung in die WHERE-Klausel einbezogen.


Wird der Parameter UpdateCheck nicht angegeben, gilt die Vorgabe UpdateCheck.Always. Alle zur Entität gehörenden Spalten werden dann zur Bestimmung der zu aktualisierenden Datenzeile herangezogen. Daraus resultiert das zuvor beschriebene First-In-Wins-Szenario. In einem Last-In-Wins-Szenario wird zur Identifizierung des betroffenen Datensatzes ausschließlich die Primärschlüsselspalte angegeben. Das erreichen Sie, indem Sie für alle anderen Spalten den Parameter auf den Wert UpdateCheck.Never einstellen.

Sie können übrigens auch die mit einem Primärschlüssel gekennzeichnete Spalte mit UpdateCheck.Never kennzeichnen. Allerdings wird diese Einstellung keine Auswirkungen haben, da diese Spalte grundsätzlich immer im Filter der WHERE-Klausel verwendet wird.

Auf Konflikte reagieren

LINQ to SQL weist eine hohe Flexibilität auf, wenn es darum geht, Einfluss darauf auszuüben, wann ein Konflikt auftreten soll. Trotz der vielen Beeinflussungsmöglichkeiten kann in einer Mehrbenutzerumgebung ein Konflikt nicht vermieden werden, wenn zwei User Änderungen am gleichen Datensatz vornehmen. Der Benutzer, der als zweiter versucht, den Datensatz in der Datenbank zu aktualisieren, wird mit einer ChangeConflictException konfrontiert, die behandelt werden muss.

Abhängig von der vom DataContext-Objekt protokollierten Anzahl an Aktualisierungen können beim Aufruf der Methode SubmitChanges durchaus mehrere Datensätze einen Konflikt auslösen. Rufen Sie SubmitChanges parameterlos auf, wird die Aktualisierung beim ersten Parallelitätskonflikt abgebrochen. Eine Überladung der Methode gestattet es jedoch, alle aufgetretenen Parallelitätskonflikte zu sammeln. Übergeben Sie SubmitChanges dazu das Argument ConflictMode.ContinueOnConflict, z. B. so:


context.SubmitChanges(ConflictMode.ContinueOnConflict)

ConflictMode ist eine Enumeration, die nur zwei Konstanten beschreibt. Die zweite, FailOnFirstConflict, ist die Vorgabe, wenn Sie keine spezifische Angabe machen.


Tabelle 29.4 Mitglieder der Enumeration »ConflictMode«

Konstante Beschreibung
FailOnFirstConflict

Gibt an, dass weitere Versuche zur Aktualisierung der Datenbank sofort abgebrochen werden sollen, wenn der erste Konflikt gefunden wird.

ContinueOnConflict

Gibt an, dass alle Aktualisierungen für die Datenbank geprüft und dass Parallelitätskonflikte zusammengefasst und am Ende der Aktualisierungsoperation zurückgegeben werden sollen.


Tritt ein Konflikt auf, haben Sie prinzipiell zwei Lösungsmöglichkeiten:

  • Sie lösen das aufgetretene Problem pauschal, ohne dass Sie den Benutzer mit Detailinformationen zum Konflikt versorgen.
  • Sie zeigen dem Benutzer detailliert den Grund des Konflikts an. Der Anwender kann dann seinerseits entscheiden, wie im Einzelfall auf den Konflikt reagiert werden muss.

Beide Varianten wollen wir nun untersuchen.

Allgemeine Konfliktlösung

Das DataContext-Objekt verfolgt nicht nur die Änderungen an den Entitätsobjekten, es hilft uns auch bei einem oder mehreren aufgetretenen Konflikten weiter. Diese werden vom DataContext in einer Collection gesammelt, auf die wir Zugriff über die Eigenschaft ChangeConflicts haben. Um festzulegen, wie Parallelitätskonflikte gelöst werden sollen, bietet uns die Collection die Methode ResolveAll an, der wir ein Argument vom Typ der Enumeration RefreshMode übergeben müssen, also zum Beispiel:


context.ChangeConflicts.ResolveAll(RefreshMode.KeepChanges);

In der Enumeration RefreshMode sind drei Konstanten definiert, die Sie Tabelle 29.5 entnehmen können.


Tabelle 29.5 Mitglieder der Enumeration »RefreshMode«

Konstante Beschreibung
KeepCurrentValues

Kombiniert die neuen Werte in der Datenbank mit den aktuellen Werten.

KeepChanges

Die aktuellen Werte werden beibehalten, die neuen Werte in der Datenbank ignoriert.

OverwriteCurrentValues

Die neuen Werte in der Datenbank überschreiben die aktuellen Werte.


Möchten Sie die Änderungen, die ein zweiter Benutzer zuvor vorgenommen hat, mit den Änderungen des aktuellen Benutzers kombinieren, stellen Sie die Option RefreshMode.KeepCurrentValues ein. Das hört sich zwar verlockend an, hat aber einen Nachteil: Haben beide Anwender dieselbe Spalte geändert, wird der Wert des Anwenders, der zuerst aktualisiert hat, einfach überschrieben.

Mit RefreshMode.OverwriteCurrentValues werden die Werte des konfliktverursachenden Datensatzes aus der Datenbank gelesen. Diese überschreiben die aktuellen Werte. Damit hat der Anwender, der mit dem Konflikt konfrontiert wird, einerseits die neuesten Daten vorliegen, muss aber andererseits seine eigenen Änderungen noch einmal überarbeiten.

RefreshMode.KeepChanges ist die brutale Art, auf einen Konflikt zu reagieren. Änderungen, die ein zweiter Anwender zuvor vorgenommen hat, werden einfach ignoriert und durch die eigenen beim folgenden SubmitChanges-Aufruf überschrieben.


try {
 context.SubmitChanges(ConflictMode.ContinueOnConflict);
}
catch (ChangeConflictException ex) {
  context.ChangeConflicts.ResolveAll(RefreshMode.KeepChanges);
  context.SubmitChanges();
}

Detaillierte Konfliktbeschreibung

Eine gute Lösung eines Parallekitätskonflikts setzt voraus, dass dem Anwender alle notwendigen Detailangaben dazu zur Verfügung gestellt werden. Dazu gehört der Name der Tabelle, in der der Konflikt verursacht worden ist, ebenso wie die betroffene Spalte. Darüber hinaus müssen auch drei Feldinformationen vorliegen:

  • der neue Wert in der Datenbank
  • der aktuelle Wert, der sich als Konfliktverursacher erwiesen hat
  • der ursprüngliche Originalwert, von dem ausgehend das Feld des Datensatzes aktualisiert worden ist

An alle benötigten Informationen zu kommen, stellt kein Problem dar. Dazu müssen wir die ChangeConflicts-Collection durchlaufen. Alle dort registrierten Konflikte werden durch Objekte vom Typ ObjectChangeConflict beschrieben. Da ein Konflikt seine Ursache auch in mehreren Feldern eines Datensatzes haben kann, werden diese Felder in einer Collection gesammelt, auf die wir Zugriff über die Eigenschaft MemberConflicts des ObjectChangeConflict-Objekts haben.

Da wir nun in Form einer Referenz genau das Feld in Erfahrung gebracht haben, das für den Konflikt verantwortlich ist, können wir alle notwendigen Informationen zusammentragen. Das wollen wir uns nun an einem konkreten Beispiel ansehen. In der Entitätsklasse werden die drei Spalten ProductID, ProductName und UnitPrice beschrieben. Für alle drei Spalten ist der Parameter UpdateCheck des Attributs Column auf seinen Standardwert Always festgelegt.


// -------------------------------------------------------
// ...\Beispiele\Kapitel 29\Konfliktanalyse
// -------------------------------------------------------
string con = "...";
DataContext context = new DataContext(con);
Table<Product> products = context.GetTable<Product>();
var prod = products.SingleOrDefault(delProd =>  
                                    delProd.ProductID == 1); 
// Feldwert aktualisieren
prod.ProductName = "Senf";
prod.UnitPrice = 115;
Console.WriteLine("Jetzt 2. User simulieren ...");
Console.ReadLine();
try {
  // Versuch der Datenbankaktualisierung
  context.SubmitChanges(ConflictMode.ContinueOnConflict);
}
catch (ChangeConflictException ex) {
  // Detailinformationen abrufen und anzeigen
  var detail = from conflict in context.ChangeConflicts
               from member in conflict.MemberConflicts
               select new {
                      Membername = member.Member.Name,
                      CurrentValue = member.CurrentValue,
                      Originalvalue = member.OriginalValue,
                      DataBaseValue = member.DatabaseValue
               };
  foreach (var item in detail) {
    Console.WriteLine("Feldname:      {0}", item.Membername);
    Console.WriteLine("CurrentValue:  {0}", item.CurrentValue);
    Console.WriteLine("Originalvalue: {0}", item.Originalvalue);
    Console.WriteLine("DataBaseValue: {0}", item.DataBaseValue);
  }
}
Console.ReadLine();

Sie können das Programmbeispiel testen, indem Sie die Anwendung starten und der Aufforderung an der Konsole folgen, einen zweiten Benutzer zu simulieren. Dazu ist das SQL Server Management Studio sehr gut geeignet. Ändern Sie hier in der ersten Datenzeile der Tabelle Products den Wert der beiden Spalten ProductName und UnitPrice. Wechseln Sie danach wieder an die Konsole, und setzen Sie durch Drücken der Taste Enter -Taste die Ausführung fort. Danach sehen Sie, dass beide geänderten Spalten für den Konflikt verantwortlich sind. Zudem zeigt die Auswertung neben dem alten Originalwert und dem aktuellen Wert auch den neuen Wert in der Datenbank an.

Abbildung 29.1 Ausgabe des Beispielprogramms »Konfliktanalyse«

Sehen wir uns nun den catch-Zweig etwas genauer an. Mit


var detail = from conflict in context.ChangeConflicts

besorgen wir uns zuerst die Liste aller aufgetretenen Konflikte. Dabei handelt es sich um Objekte vom Typ ObjectChangeConflict. Jedes dieser Objekte beschreibt eine eigene Collection, die MemberChangedConflict-Objekte verwaltet. Diese Liste wird mit


from member in conflict.MemberConflicts

abgerufen. Über die Eigenschaft Member des MemberChangedConflict-Objekts können wir die für uns interessanten Details erfahren und einem anonymen Typ übergeben. Dazu gehören der Name der konfliktauslösenden Spalte sowie der aktuelle Wert, der Originalwert und der in der Datenbank gespeicherte neue Wert.


select new {
              Membername = member.Member.Name,
              CurrentValue = member.CurrentValue,
              Originalvalue = member.OriginalValue,
              DataBaseValue = member.DatabaseValue
           };

Im Beispielprogramm werden die gelieferten Informationen nur dazu benutzt, sie an der Konsole anzuzeigen. Sie lassen sich aber auch so verwenden, dass der Benutzer selbst darüber entscheiden kann, welche Daten tatsächlich in die Datenbank geschrieben werden sollen.



Ihr Kommentar

Wie hat Ihnen das <openbook> gefallen? Wir freuen uns immer über Ihre freundlichen und kritischen Rückmeldungen. >> Zum Feedback-Formular
<< zurück
  Zum Katalog
Zum Katalog: Visual C# 2010

Visual C# 2010
Jetzt bestellen


 Ihre Meinung?
Wie hat Ihnen das <openbook> gefallen?
Ihre Meinung

 Buchempfehlungen
Zum Katalog: Professionell entwickeln mit Visual C# 2012






 Professionell
 entwickeln mit
 Visual C# 2012


Zum Katalog: Windows Presentation Foundation






 Windows Presentation
 Foundation


Zum Katalog: Schrödinger programmiert C++






 Schrödinger
 programmiert C++


Zum Katalog: C++ Handbuch






 C++ Handbuch


Zum Katalog: C/C++






 C/C++


 Shopping
Versandkostenfrei bestellen in Deutschland und Österreich
InfoInfo




Copyright © Rheinwerk Verlag GmbH 2010
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