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 27 ADO.NET – Aktualisieren der Datenbank
Pfeil 27.1 Aktualisieren mit dem »CommandBuilder«
Pfeil 27.1.1 Von »SqlCommandBuilder« generierte Aktualisierungsstatements
Pfeil 27.1.2 Konfliktsteuerung in einer Mehrbenutzerumgebung
Pfeil 27.1.3 Die Eigenschaft »ConflictOption« des »SqlCommandBuilder«
Pfeil 27.1.4 Die Eigenschaft »SetAllValues«
Pfeil 27.2 Manuell gesteuerte Aktualisierung
Pfeil 27.2.1 Eigene Aktualisierungslogik
Pfeil 27.2.2 Beispielprogramm
Pfeil 27.3 Konfliktanalyse
Pfeil 27.3.1 Benutzer über fehlgeschlagene Aktualisierungen informieren
Pfeil 27.3.2 Konfliktverursachende Datenzeilen bei der Datenbank abfragen
Pfeil 27.4 Neue Autoinkrementwerte abrufen


Galileo Computing - Zum Seitenanfang

27.3 Konfliktanalyse Zur nächsten ÜberschriftZur vorigen Überschrift

Stehen mehrere Datenzeilen zur Aktualisierung an, wird der DataAdapter versuchen, eine nach der anderen an die Datenbank zu senden. Wie im letzten Beispielprogramm zu sehen war, wird der DataAdapter eine DBConcurrencyException auslösen und die verbleibenden Änderungen nicht mehr an die Datenbank schicken. Das ist das Standardverhalten.

Sie können den DataAdapter anweisen, nach einem etwaigen Konflikt seine Aufgabe fortzusetzen und die verbleibenden Änderungen zu übermitteln. Dazu setzen Sie seine Eigenschaft ContinueUpdateOnError=true. Eine Exception wird in diesem Fall nicht ausgelöst. Stattdessen stehen Ihnen zwei andere Optionen zur Verfügung, mit den aufgetretenen Konflikten umzugehen:

  • Sie informieren den Benutzer lediglich darüber, welche Datenzeilen nicht aktualisiert werden konnten.
  • Sie implementieren über die reine Information zum fehlgeschlagenen Aktualisierungsversuch hinaus auch eine Konfliktlösung. Dazu benötigt der Benutzer alle zur Verfügung stehenden Informationen, unter anderem auch diejenige, wie der neue aktuelle Inhalt der konfliktverursachenden Datenzeile in der Datenbank lautet.

Beide Szenarien wollen wir uns nun ansehen.


Galileo Computing - Zum Seitenanfang

27.3.1 Benutzer über fehlgeschlagene Aktualisierungen informieren Zur nächsten ÜberschriftZur vorigen Überschrift

Wenn Sie vor dem Aufruf der Update-Methode die Eigenschaft ContinueUpdateOnError auf true festlegen, verursacht ein fehlgeschlagener Aktualisierungsversuch keine Ausnahme mehr. Stattdessen wird die Eigenschaft HasErrors des entsprechenden DataRow-Objekts auf true gesetzt, ebenso die gleichnamige Eigenschaft des DataSets und der DataTable. Eine DataRow hat eine Eigenschaft RowError. Diese enthält nach dem misslungenen Versuch eine Fehlermeldung.

Im folgenden Beispielprogramm wird der Einsatz der Eigenschaften ContinueUpdateOnError, HasErrors und RowError gezeigt. Das Beispiel setzt das Beispielprogramm des vorhergehenden Abschnitts fort und ergänzt nur noch die notwendigen Passagen.


// ------------------------------------------------------------------
// Beispiel: ...\Kapitel 27\HasErrorsSample
// ------------------------------------------------------------------
static void Main(string[] args) {
  ... 
  // Simulation eines Konflikts
  Console.Write("Konflikt simulieren ...");
  Console.ReadLine();
  // Datenbank aktualisieren
  da.ContinueUpdateOnError = true;
  da.Update(ds);
  if (ds.HasErrors) {
    string text = "Folgende Zeilen konnten nicht aktualisiert werden:";
    foreach (DataRow row in ds.Tables[0].Rows)
      if (row.HasErrors) {
        Console.WriteLine(text);
        Console.WriteLine("ID: {0}, Fehler: {1}", 
                           row["ProductID"], row.RowError);
      }
  }
  else
    Console.WriteLine("Die Aktualisierung war erfolgreich.");
  Console.ReadLine();
}

Nach dem Aufruf von Update auf dem DataAdapter wird zuerst mit


if (ds.HasErrors)

das DataSet dahingehend untersucht, ob tatsächlich ein Konflikt vorliegt. HasErrors ist false, wenn die Datenbank die Änderungen angenommen hat. true signalisiert hingegen, dass wir alle Datenzeilen in der Tabelle des DataSets durchlaufen müssen, um die konfliktverursachenden Zeilen zu finden. Der Code wird fündig, wenn er auf eine Datenzeile mit HasErrors=true trifft.


foreach (DataRow row in ds.Tables[0].Rows)
  if (row.HasErrors) {
   ...
  }

Jetzt können wir reagieren. Im einfachsten Fall lassen wir uns zumindest die ID des betreffenden Übeltäters ausgeben – so wie in diesem Beispiel. Sie können die Information natürlich auch dazu benutzen, dem Benutzer die Möglichkeit zu geben, die Konfliktursache zu beseitigen, denn der Verursacher ist ermittelt.


Galileo Computing - Zum Seitenanfang

27.3.2 Konfliktverursachende Datenzeilen bei der Datenbank abfragen topZur vorigen Überschrift

Meist genügt es nicht, nur zu wissen, wer der Konfliktverursacher ist. Es wird darüber hinaus auch eine Lösung angestrebt. Dies bedarf aber einer genaueren Analyse der Umstände, die zu einem Konflikt führen können. Dabei sind vier Situationen zu beachten:

  • Ein Anwender versucht, eine Datenzeile mit einem neuen Primärschlüssel hinzuzufügen, der bereits in der Tabelle existiert.
  • Ein Anwender versucht, einen Datensatz zu ändern, den ein zweiter Benutzer zuvor geändert hat.
  • Es wird versucht, eine Datenzeile zu ändern, die zwischenzeitlich gelöscht worden ist.
  • Es wird versucht, eine Datenzeile zu löschen, die bereits gelöscht ist. In der Regel wird man aber diesem Konflikt keine Beachtung schenken müssen.

Wird versucht, einen bereits vorhandenen Primärschlüssel für einen neuen Datensatz ein zweites Mal zu vergeben, scheint die Lösung des Problems noch recht einfach zu sein: Es muss nur ein anderer Primärschlüssel vergeben werden. Aber das könnte eine falsche Entscheidung sein. Können Sie denn sicherstellen, dass nicht zwei Anwender versuchen, den gleichen Datensatz zur Tabelle hinzuzufügen? Falls Sie diese Situation nicht berücksichtigen, liegen im schlimmsten Fall zwei identische Datensätze vor.

Um eine präzise Konfliktlösung der beiden anderen relevanten Konflikte zu ermöglichen, fehlen uns Informationen, die nur in der Datenbank zu finden sind. Was wir brauchen, ist eine neue Originalversion der konfliktverursachenden Datenzeile.

Der DataAdapter hilft uns an dieser Stelle weiter. Er löst nämlich für jede zu aktualisierende Datenzeile zwei Ereignisse aus, wenn anstehende Änderungen über die Methode Update an die Datenbank übermittelt werden:

  • RowUpdating
  • RowUpdated

RowUpdating wird ausgelöst, bevor eine Zeile übermittelt wird, RowUpdated tritt unmittelbar nach der Übermittlung auf.

Für unsere Lösung interessiert uns natürlich nur das Ereignis RowUpdated, dessen zweiter Parameter vom Typ SqlRowUpdatedEventArgs uns mit allen Informationen versorgt, die wir zur Konfliktanalyse und zur anschließenden Konfliktlösung benötigen. In Tabelle 27.2 sind die Eigenschaften des EventsArgs-Parameters des RowUpdated-Ereignisses aufgeführt.


Tabelle 27.2 Eigenschaften des »SqlRowUpdatedEventArgs«-Objekts

Eigenschaft Beschreibung
Command

Ruft das beim Aufruf von Update ausgeführte SqlCommand ab.

Errors

Ruft alle Fehler ab, die während der Ausführung generiert wurden.

RecordsAffected

Ruft die Anzahl der durch die Ausführung der SQL-Anweisung geänderten, eingefügten oder gelöschten Zeilen ab.

Row

Ruft die durch ein Update gesendete DataRow ab.

StatementType

Ruft den Typ der ausgeführten SQL-Anweisung ab.

Status

Ruft einen Wert der Enumeration UpdateStatus ab oder legt diesen fest.

TableMapping

Ruft das durch ein Update gesendete DataTableMapping ab.


Der Vollständigkeit halber folgt jetzt auch noch die Tabelle mit den Membern der Enumeration UpdateStatus, die von der Eigenschaft Status des SqlRowUpdatedEventArgs-Objekts offengelegt wird.


Tabelle 27.3 Member der Enumeration »UpdateStatus«

Member Beschreibung
Continue

Der DataAdapter soll mit der Verarbeitung von Zeilen fortfahren.

ErrorsOccured

Der Ereignishandler meldet, dass die Aktualisierung als Fehler behandelt werden soll.

SkipAllRemainingRows

Die aktuelle Zeile und alle restlichen Zeilen sollen nicht aktualisiert werden.

SkipCurrentRow

Die aktuelle Zeile soll nicht aktualisiert werden.


Wie können wir nun das Ereignis zu unserem Nutzen einsetzen?

Wir müssen zunächst herausfinden, ob das Update einer Datenzeile zu einem Konflikt geführt hat. Hierzu prüfen wir, ob die Eigenschaft Status des SqlRowUpdatedEventArgs-Objekts den Enumerationswert UpdateStatus.ErrorsOccured aufweist.


private void da_RowUpdated(object sender, SqlRowUpdatedEventArgs e) {
  if (e.Status == UpdateStatus.ErrorsOccurred) {
    ...
  }
}

Um zu einer Konfliktlösung zu kommen, werden die konfliktverursachenden Datenzeilen in ihrer neuen, aktuellen Version aus der Datenbank benötigt. Deshalb wird innerhalb des Ereignishandlers eine erneute Abfrage an die Datenbank geschickt und der Primärschlüssel der konfliktverursachenden Datenzeile als Filter benutzt. Da uns das EventArgs-Objekt in seiner Eigenschaft Row die Referenz auf die entsprechende Datenzeile mitteilt, stellt das kein Problem dar.

Sinnvollerweise stellt man ein eigenes DataSet-Objekt für alle abgefragten Datenzeilen zur Verfügung (hier: dsConflict), ebenso einen separaten SqlDataAdapter (hier: daConflict). Der Aufruf der Fill-Methode bewirkt, dass entweder genau eine Datenzeile das Ergebnis des Aufrufs bildet oder keine. Die Fill-Methode teilt uns über ihren Rückgabewert die Anzahl der Datensätze mit, die die Ergebnismenge bilden. Der Rückgabewert ist ganz entscheidend, um festzustellen, welche Ursache der Konflikt hat. Sehen wir uns nun zuerst das entsprechende Codefragment zu dem Gesagten an:


DataSet dsConflict = new DataSet();
SqlDataAdapter daConflict = new SqlDataAdapter();
...
private void da_RowUpdated(object sender, SqlRowUpdatedEventArgs e) {
  if (e.Status == UpdateStatus.ErrorsOccurred) {
    SqlCommand cmdConflict = new SqlCommand();
    string sql = "SELECT ProductID, ProductName, UnitPrice, UnitsInStock
                  FROM Products WHERE ProductID = " + e.Row["ProductID"];
    cmdConflict = new SqlCommand();
    cmdConflict.Connection = con;
    cmdConflict.CommandText = sql;
    daConflict.SelectCommand = cmdConflict;
    int result = daConflict.Fill(dsConflict);
  }
}

Grundsätzlich können beim Aktualisieren einer Datenzeile zwei verschiedene Exceptions auftreten:

  • SqlException
  • DBConcurrencyException

SqlException beschreibt Ausnahmen, die der SQL Server zurückgibt. Das wäre beispielsweise der Fall, wenn ein Datensatz mit einem Primärschlüssel hinzugefügt wird, der in der Tabelle bereits existiert.

DBConcurrencyException hingegen wird ausgelöst, wenn eine Parallelitätsverletzung vorliegt. Das ist der Fall, wenn die Anzahl der aktualisierten Datenzeilen 0 ist.

Um festzustellen, welche Ausnahme ausgelöst worden ist, brauchen Sie nur den Ausnahmetyp zu untersuchen, der in der Eigenschaft Errors des SqlRowUpdatedEventArgs-Objekts enthalten ist.


if (e.Errors.GetType() == typeof(SqlException)) {
  // Anweisung
}
else if (e.Errors.GetType() == typeof(DBConcurrencyException)) {
  // Anweisungen
}

Anschließend kommt es zur Auswertung der Anzahl der Datensätze, die in der Variablen result stehen. Es kann sich nur um die Zahl 0 oder 1 handeln, woraus weitere Rückschlüsse gezogen werden können.

Betrachten wir zuerst den Fall, dass Errors ein SqlException-Objekt enthält:


if (e.Errors.GetType() == typeof(SqlException)) {
  if (result == 1)
    Console.WriteLine("Der PS existiert bereits.");
}

Ist in diesem Fall der Inhalt von result eins, handelt es sich um den Versuch, einen neuen Datensatz mit einem Primärschlüssel hinzuzufügen, wobei der Primärschlüssel in der Originaltabelle bereits vergeben ist. Beschreibt result die Zahl 0, liegt ein anderer Datenbankfehler vor.

Handelt es sich um den Ausnahmetyp DBConcurrencyException, wurde der Versuch, die Änderung an einer Datenzeile in die Originaltabelle zu schreiben, abgelehnt. Die Parallelitätsverletzung kann zwei Ursachen haben:

  • Ein anderer Anwender hat den Datensatz zwischenzeitlich geändert. Der Inhalt der Variablen result muss in diesem Fall die Zahl 1 sein. Mit anderen Worten: Der Datensatz existiert noch.
  • Wird der Inhalt von result mit der Zahl 0 beschrieben, wurde der Datensatz von einem anderen Anwender gelöscht. Der entsprechende Primärschlüssel existiert nicht mehr.

Der else if-Zweig muss demnach wie folgt codiert werden:


...
else if (e.Errors.GetType() == typeof(DBConcurrencyException)) {
  // ist Anzahl=1 -> anderer Benutzer hat DS geändert
  if (result == 1)
    Console.WriteLine("Ein anderer User hat den Datensatz geändert.");
  else
    Console.WriteLine("Datensatz existiert nicht in der Datenbank.");
}

Das folgende Programm zeigt den Code im Zusammenhang. Ausgangspunkt ist wieder das Programm ManuelleAktualisierung, das entsprechend ergänzt wird. Neben der Implementierung des Ereignishandlers des RowUpdated-Ereignisses wird nach der Aktualisierung auch das DataSet mit den konfliktverursachenden Datenzeilen abgefragt und ausgegeben.


// ------------------------------------------------------------------
// Beispiel: ...\Kapitel 27\KonfliktAnalyse
// ------------------------------------------------------------------
class Program {
  static DataSet dsConflict = new DataSet();
  static SqlDataAdapter daConflict = new SqlDataAdapter();
  static SqlConnection con = new SqlConnection();
  static void Main(string[] args) {
    ...
    // Datenbank aktualisieren
    da.ContinueUpdateOnError = true;
    da.RowUpdated += new SqlRowUpdatedEventHandler(da_RowUpdated);
    da.Update(ds);
    // Konflikt-DataSet abrufen
    if (dsConflict.Tables.Count > 0) {
      Console.WriteLine("\n{0,-5}{1,-35}{2,-12}{3}", 
                   "ID", "ProductName", "UnitPrice", "UnitsInStock");
      Console.WriteLine(new string('-', 65));
      foreach (DataRow item in dsConflict.Tables[0].Rows)
        Console.WriteLine("{0,-5}{1,-35}{2,-12}{3}", 
                           item["ProductID"], item["ProductName"], 
                           item["UnitPrice"], item["UnitsInStock"]);
    }            
    Console.ReadLine();
  }
  static void da_RowUpdated(object sender, SqlRowUpdatedEventArgs e) {
    if (e.Status == UpdateStatus.ErrorsOccurred) {
      SqlCommand cmdConflict = new SqlCommand();
      string sql = "SELECT ProductID, ProductName, UnitPrice, UnitsInStock " +
"FROM Products WHERE ProductID = " + e.Row["ProductID"]; cmdConflict = new SqlCommand(); cmdConflict.Connection = con; cmdConflict.CommandText = sql; daConflict.SelectCommand = cmdConflict; int result = daConflict.Fill(dsConflict); if (e.Errors.GetType() == typeof(SqlException)) { // prüfen, ob es einen DS mit einem bestimmten PS gibt if (result == 1) Console.WriteLine("Der PS existiert bereits."); } else if (e.Errors.GetType() == typeof(DBConcurrencyException)) { // ist Anzahl=1 -> anderer Benutzer hat DS geändert if (result == 1) Console.WriteLine("Ein anderer User hat den Datensatz geändert."); else Console.WriteLine("Datensatz existiert nicht in der Datenbank."); } } } }



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