34.4 Änderungen in einer DataTable vornehmen
Sehen wir uns nun an, wie wir einer DataTable eine neue DataRow hinzufügen und eine vorhandene DataRow löschen oder editieren können. Um einen wichtigen Punkt gleich vorwegzunehmen: Jegliche Änderung betrifft zunächst nur das DataSet. Die Originaldatenbank weiß davon nichts. Erst zu einem späteren Zeitpunkt werden alle Änderungen zur Datenbank übermittelt. Wir behandeln daher in diesem Abschnitt nur die lokalen Aktualisierungen. In Kapitel 35 werden wir uns der Aktualisierung der Originaldatenquelle zuwenden.
34.4.1 Editieren einer DataRow
Es gibt drei Möglichkeiten, eine Zeile zu aktualisieren. Im einfachsten Fall weisen Sie der betreffenden Spalte nur den neuen Inhalt zu:
ds.Tables[0].Rows[3]["ProductName"] = "Kirschkuchen";
Die Änderung wird sofort in die angegebene Spalte der entsprechenden Datenzeile geschrieben.
Die zweite Möglichkeit puffert die Änderung. Dazu wird vor Beginn der Änderung die Methode BeginEdit auf die zu ändernde Datenzeile aufgerufen und die Änderung mit EndEdit bestätigt. Sie können die eingeleitete Änderung auch zurücksetzen und anstelle von EndEdit die Methode CancelEdit aufrufen. Die Zeile wird dann in den Zustand zurückversetzt, den sie vor BeginEdit hatte.
DataRow row = ds.Tables[0].Rows[3];
row.BeginEdit();
row["ProductName"] = "Kirschkuchen";
row.EndEdit();
// Alternativ: row.cancelEdit();
Listing 34.6 Ändern einer Datenzeile
Die Pufferung der Änderung ist nicht der einzige Unterschied zwischen den beiden Aktualisierungsmöglichkeiten. Die DataTable verfügt über mehrere Ereignisse, die nur im Zusammenhang mit BeginEdit und EndEdit ausgelöst werden. Es handelt sich hierbei um
Diese Ereignisse spielen eine Rolle, wenn Änderungen an einer Datenzeile oder Spalte überprüft werden müssen. Die Ereignisse werden nicht ausgelöst, wenn Sie CancelEdit aufrufen. Wenn wir uns später dem Zurückschreiben der Änderungen in die Originaldatenbank zuwenden, werden wir noch einmal auf diese Ereignisse zurückkommen.
Die dritte Möglichkeit bietet uns die Eigenschaft ItemArray, die ein Object-Array beschreibt. Mit dieser Eigenschaft können Sie den Inhalt einer Datenzeile abrufen oder verändern. ItemArray arbeitet mit einem Array, in dem jedes Element einer Spalte entspricht. Mit einer Codezeile können Sie mehrere Spaltenwerte abrufen und editieren. Ist in einer Zeile nur eine Teilmenge der verfügbaren Werte zu modifizieren, verwenden Sie null, um anzuzeigen, dass der Wert dieser Spalte nicht geändert werden soll.
Im folgenden Codefragment werden drei Spalten der Tabelle Products abgefragt. In der ersten Datenzeile soll mit der Eigenschaft ItemArray der Produktbezeichner modifiziert werden. Weil der Schlüsselwert nicht geändert wird, muss an der ersten Position null in das Objekt-Array geschrieben werden.
SqlCommand cmd = new SqlCommand();
cmd.Connection = con;
cmd.CommandText = "SELECT ProductID, ProductName, UnitPrice FROM Products";
DataSet ds = new DataSet();
SqlDataAdapter da = new SqlDataAdapter(cmd);
da.Fill(ds);
DataRow row = ds.Tables[0].Rows[0];
row.ItemArray = new Object[] {null, "Kirschkuchen"};
Listing 34.7 Änderungen mit »ItemArray«
Den Spaltenwert auf NULL festlegen
Möchten Sie den Wert einer Spalte auf NULL setzen, verwenden Sie die Klasse DBNull, die sich im Namespace System befindet. Mit der Eigenschaft Value legen Sie den Wert einer Spalte in einer DataRow auf NULL fest.
DataRow row = ds.Tables[0].Rows[4];
row["UnitPrice"] = DBNull.Value;
34.4.2 Löschen einer Datenzeile
Das Löschen einer Datenzeile ist sehr einfach: Sie rufen hierzu die Methode Delete der DataRow auf, die gelöscht werden soll.
Es ist falsch anzunehmen, dass die betreffende Datenzeile nun aus der DataTable entfernt wird. Sie ist immer noch vorhanden, allerdings kennzeichnet ADO.NET sie als gelöscht. Hintergrund der Markierung ist, dass das Löschen zunächst nur das aktuelle DataSet betrifft und zu einem späteren Zeitpunkt der Originaldatenbank mitgeteilt werden muss. Es wäre daher auch falsch, eine Datenzeile mit Remove oder RemoveAt aus der DataRowCollection der Tabelle zu entfernen, denn dann findet der Aktualisierungsprozess die Datenzeile nicht mehr.
34.4.3 Eine neue Datenzeile hinzufügen
Eine Datenzeile zu einer DataTable hinzuzufügen, ist auch nicht schwierig. Allerdings stellt die Klasse DataRow keinen öffentlichen Konstruktor zur Verfügung, denn woher sollte ein auf diese Weise konstruiertes DataRow-Objekt etwas von den Spalten wissen, durch die es beschrieben wird?
ADO.NET bietet Ihnen genauso wie zum Editieren einer Datenzeile drei Varianten an, um eine neue Datenzeile zu einer DataTable hinzuzufügen. Zunächst einmal sei die Methode NewRow der DataTable erwähnt. Eine so erzeugte neue Zeile enthält alle Informationen über die Spalten in der Tabelle. Werden im Schema keine Standardwerte vorgegeben, sind die Inhalte der Spalten auf NULL gesetzt. Haben Sie alle Einträge in der neuen Zeile vorgenommen, müssen Sie die neue Zeile der DataRowCollection anhängen, denn das leistet der Aufruf von NewRow nicht.
DataTable tbl = ds.Tables[0];
DataRow row = tbl.NewRow();
row["ProductName"] = "Erbsensuppe";
row["UnitPrice"] = 2;
row["SupplierID"] = 3;
[...]
tbl.Rows.Add(row);
Listing 34.8 Hinzufügen einer Datenzeile
Die zweite Möglichkeit, eine neue Datenzeile hinzuzufügen, bietet eine Überladung der Methode Add der DataRowCollection. Übergeben Sie dem Methodenaufruf die Spaltenwerte in der Reihenfolge, die der Reihenfolge der Spalten in der SELECT-Abfrage entspricht. Basierend auf der Auswahlabfrage
SELECT ProductName, Unitprice, UnitsInStock FROM Products
könnte eine neue Datenzeile wie folgt hinzugefügt werden:
ds.Tables[0].Rows.Add("Mehl", 20, 0);
Im Gegensatz zur Methode NewRow wird die neue Datenzeile automatisch der DataRowCollection hinzugefügt.
Die dritte Möglichkeit stellt die Methode LoadDataRow der DataTable dar. Diese Methode arbeitet ähnlich wie die zuvor gezeigte Add-Methode der DataRowCollection, verlangt aber die Angabe von zwei Parametern. Geben Sie im ersten Parameter ein Array von Werten an, dessen Elemente den Spalten in der Tabelle entsprechen. Tragen Sie im zweiten Parameter false ein. Hintergrund ist, dass die so gekennzeichnete Datenzeile als neue Datenzeile interpretiert wird. LoadDataRow eignet sich nämlich auch dazu, eine bestimmte Datenzeile zu suchen und zu modifizieren. Dann muss dem zweiten Parameter jedoch true übergeben werden.
ds.Tables[0].LoadDataRow(new object[] {"Mehl", 20, 0}, false);
34.4.4 Der Sonderfall: Autoinkrementspalten
Viele Tabellen in Datenbanken beschreiben das Primärschlüsselfeld mit Autoinkrementwerten. Das ist vorteilhaft, weil eine zentrale Logik immer eindeutige Ganzzahlen erzeugt. Fügen wir jedoch eine neue Datenzeile zu einer DataTable hinzu, die ein solches Schlüsselfeld definiert, haben wir keine Verbindung zur Originaldatenbank. Mit anderen Worten: Wir kennen den neuen Wert des Schlüsselfeldes nicht. Den erfahren wir erst, wenn wir die Datenbank aktualisiert haben und eine entsprechende Abfrage starten.
ADO.NET unterstützt uns mit drei Eigenschaften der DataColumn, um auch diese scheinbare Problematik zu lösen:
Um von ADO.NET in einer DataTable Autoinkrementwerte generieren zu lassen, muss die Eigenschaft AutoIncrement der betreffenden Spalte auf true gesetzt werden. Mit AutoIncrementSeed und AutoIncrementStep werden die von ADO.NET erzeugten Werte gesteuert. AutoIncrementSeed beschreibt dabei den Startwert der Autoinkrementspalte für die erste neu hinzugefügte Datenzeile. AutoIncrementStep gibt die Schrittweite an, mit der neue Schlüsselwerte generiert werden. Legen Sie für eine Autoinkrementspalte beispielsweise AutoIncrementSeed=1 und AutoIncrementStep=2 fest, lauten die Werte für die drei nachfolgend hinzugefügten Datenzeilen 1, 3 und 5.
Die Werte, die ADO.NET erzeugt, müssen Sie als Platzhalter verstehen. Sie werden später bei der Aktualisierung der Originaldatenbank nicht mit zurückgeschrieben. Die tatsächlichen Schlüsselwerte erzeugt die Datenbank selbst.
Doch welche Werte sollten Sie in der DataTable vergeben? Eigentlich müssen Sie nur sicherstellen, dass neue Schlüsselwerte nicht mit den Schlüsselwerten in Konflikt geraten, die bereits in der DataTable enthalten sind. Sie können auch davon ausgehen, dass negative Werte in der Datenbank nicht verwendet werden. Empfehlenswert ist daher, die beiden Eigenschaften AutoIncrementSeed und AutoIncrementStep auf jeweils –1 festzulegen. Zudem sollten diese Einstellungen erfolgen, ehe das DataSet mit den Daten gefüllt wird.
Sehen wir uns dazu nun ein Beispiel an.
// Beispiel: ..\Kapitel 34\AutoIncrementSample
class Program {
static void Main(string[] args) {
SqlConnection con = new SqlConnection();
con.ConnectionString = "...";
SqlCommand cmd = new SqlCommand();
cmd.Connection = con;
cmd.CommandText = "SELECT ProductID, ProductName FROM Products";
DataSet ds = new DataSet();
SqlDataAdapter da = new SqlDataAdapter(cmd);
// Schemainformationen abrufen
da.FillSchema(ds, SchemaType.Source);
// Festlegen, wie die neuen Schlüsselwerte erzeugt werden
ds.Tables[0].Columns[0].AutoIncrementSeed = -1;
ds.Tables[0].Columns[0].AutoIncrementStep = -1;
// DataSet füllen
da.Fill(ds);
// Neue Datenzeilen hinzufügen
DataRow row = ds.Tables[0].NewRow();
row["ProductName"] = "Kaffee";
ds.Tables[0].Rows.Add(row);
row = ds.Tables[0].NewRow();
row["ProductName"] = "Milch";
ds.Tables[0].Rows.Add(row);
row = ds.Tables[0].NewRow();
row["ProductName"] = "Zucker";
ds.Tables[0].Rows.Add(row);
// Ausgabe des DataSets
foreach (DataRow tempRow in ds.Tables[0].Rows)
Console.WriteLine("{0,-6}{1}",tempRow[0],tempRow[1]);
Console.ReadLine();
}
}
Listing 34.9 Hinzufügen von Datenzeilen mit einer Autoinkrementspalte
Damit der Code überschaubar bleibt, werden aus der Datenbank nur zwei Spalten der Tabelle Products abgefragt. Die Primärschlüsselspalte ProductID ist als Autoinkrementspalte definiert. Mit FillSchema werden die Metadaten der Tabelle bezogen. In der Praxis würde man diese Methode in einer Anwendung wohl aus den weiter oben angeführten Gründen nicht benutzen, aber für ein Beispielprogramm ist sie durchaus geeignet. Da FillSchema auch AutoIncrement=true für die Spalte ProductID setzt, muss diese Eigenschaft der DataColumn nicht mehr gesetzt werden.
Später werden der DataTable drei Datenzeilen hinzugefügt. Der temporäre Schlüsselwert der ersten ist auf –1 festgelegt. Alle weiteren neuen Schlüsselwerte werden mit der Schrittweite –1 generiert, so dass der Schlüsselwert der zweiten neuen Datenzeile –2 ist, der der dritten neuen Datenzeile –3. Beachten Sie, dass die Autoinkrementeigenschaften vor dem Füllen des DataSets gesetzt werden müssen. Ansonsten wirken sich die Eigenschaftswerte nicht auf die Autoinkrementwerte aus, die die DataTable generiert.
Zum Abschluss unserer Betrachtungen zu den Autoinkrementwerten noch eine Anmerkung: Vergessen Sie nicht, dass die generierten Schlüsselwerte nur Platzhalter innerhalb der DataTable darstellen. Erst nach der Übermittlung zur Originaldatenbank werden die tatsächlichen und endgültigen Schlüsselwerte von der Datenbank erzeugt. Sie sollten daher vermeiden, die temporären Schlüsselwerte dem Anwender anzuzeigen. Es könnte unabsehbare Folgen haben, wenn der Anwender sich eine ADO.NET-Schlüsselnummer notiert, die später nach der Aktualisierung nicht mehr existiert.
34.4.5 Was bei einer Änderung einer Datenzeile passiert
Die Eigenschaft »RowState«
Ein DataSet ist im lokalen Cache der Anwendung abgelegt. Während des Löschens, Änderns und Hinzufügens von Datenzeilen besteht zu der Originaldatenbank keine Verbindung. Wenn der Benutzer die geänderten Daten später an die Datenbank übermitteln möchte, muss sich das DataSet daran erinnern können, welche Zeilen von einer Änderung betroffen sind, und natürlich auch, welcher Natur diese Änderung ist. Haben Sie beispielsweise eine Datenzeile gelöscht, muss für die betreffende Datenzeile ein DELETE-SQL-Statement zur Datenbank geschickt werden, das das Löschen in der Originaltabelle bewirkt. Haben Sie eine Datenzeile geändert, bedarf es eines passend formulierten UPDATE-Statements. Wie die Aktualisierungsabfragen erzeugt werden, sei an dieser Stelle noch nicht erläutert. Das werden wir uns im Detail später noch ansehen. Aber Sie sollten an dieser Stelle erkennen, wie wichtig es ist, dass jede Datenzeile ihren eigenen Aktualisierungszustand beschreiben kann.
ADO.NET speichert die notwendigen Zustandsinformationen in der Eigenschaft RowState jeder Datenzeile. Die Eigenschaft wird durch die Enumeration DataRowState beschrieben, wie in Tabelle 34.3 aufgelistet.
Member | Beschreibung |
Added |
Die Zeile wurde einer DataRowCollection hinzugefügt. |
Deleted |
Die Zeile wurde mit der Delete-Methode der DataRow gelöscht. |
Detached |
Die Zeile wurde erstellt, ist jedoch nicht Teil einer DataRowCollection. Eine DataRow befindet sich in diesem Zustand, wenn sie unmittelbar nach ihrer Erstellung noch keiner Auflistung hinzugefügt wurde oder wenn sie aus einer Auflistung entfernt wurde. |
Modified |
Die Zeile wurde geändert. |
Unchanged |
Die Zeile wurde nicht geändert. |
Der ursprüngliche und der aktualisierte Inhalt einer Datenzeile
Sie wissen nun, dass eine Datenzeile beschreibt, ob und wie sie modifiziert wurde. Um später die Änderung zur Datenbank zu übermitteln, reicht das aber noch nicht aus: Es fehlen noch dringend notwendige Informationen. Stellen Sie sich dazu nur vor, Sie würden den Artikelbezeichner einer Datenzeile der Tabelle Products ändern und die Änderung mit einem UPDATE-Statement der Datenbank mitteilen. Das SQL-Statement könnte wie folgt lauten:
UPDATE Products
SET ProductName = @Param1
WHERE ProductID = @Param2 AND ProductName = @Param3
Im Parameter @Param1 wird der geänderte, also neue Wert übermittelt, in @Param2 der Schlüsselwert der Datenzeile und in @Param3 der ursprüngliche Wert der Spalte ProductID. Setzen Sie ein solches Statement ab, darf natürlich zwischen dem Abrufen der Dateninformationen und der Aktualisierung kein zweiter Benutzer den Produktnamen geändert haben. Die Folge wäre eine Konfliktsituation, weil die anstehende Änderung nicht in die Datenbank geschrieben werden kann. Dieser (scheinbaren) Problematik wollen wir an dieser Stelle noch nicht weiter nachgehen.
Sie sollten erkennen: Um das UPDATE-Statement erfolgreich absetzen zu können, bedarf es nicht nur der geänderten Werte, sondern auch des Originalwertes, um die Datenzeile in der Datenbank zu identifizieren. Für diesen Zweck ist der Indexer einer DataRow überladen. Anstatt mit
row["Productname"]
den aktuellen, also möglicherweise geänderten Wert der Spalte ProductName abzurufen, können Sie auch mit
row["Productname", DataRowVersion.Original]
auf den von der Datenbank bezogenen Originalwert zurückgreifen.
DataRowVersion ist eine Aufzählung, mit der die gewünschte Version der betreffenden Spalte in der Datenzeile angegeben werden kann.
Member | Beschreibung |
Current |
Die Zeile enthält aktuelle Werte. |
Default |
Die Zeile enthält einen vorgeschlagenen Wert. |
Original |
Die Standardversion der Zeile, dem aktuellen DataRowState entsprechend. |
Proposed |
Die Zeile enthält ihre ursprünglichen Werte. |
Sie können sich jetzt sicher vorstellen, dass es von jeder DataRow immer zwei Versionen gibt: Zunächst einmal DataRowVersion.Original für die Werte, die aus der Datenbank bezogen worden sind, und DataRowVersion.Current für die aktuellen und möglicherweise geänderten Werte. Jetzt wird auch verständlich, warum es nach der Einleitung einer Änderung mit BeginEdit mittels CancelEdit möglich ist, den ursprünglichen Zustand einer DataRow wiederherzustellen. Rufen Sie mit
row["ProductName"]
den Inhalt einer Spalte ab, wird immer DataRowVersion.Current ausgewertet. Das ist wichtig zu wissen, denn sollten Sie die DataRowCollection in einer Schleife durchlaufen, innerhalb deren zum Beispiel auf Spalten aller geänderten Zeilen zugegriffen wird, dürfen Sie von einer gelöschten Zeile nicht DataRowVersion.Current abrufen. Sie können aber sehr wohl DataRowVersion.Original auswerten, weil eine als gelöscht markierte Datenzeile nicht aus der DataRowCollection entfernt wird.
Das nächste Beispiel zeigt Ihnen die prinzipielle Vorgehensweise: Nachdem das DataSet aus der Artikeltabelle mit Daten gefüllt ist, wird zuerst ein weiterer Datensatz hinzugefügt. Anschließend wird in der Tabelle nach einem bestimmten Artikel gesucht (Tofu). Hierzu wird die Methode Select der DataTable aufgerufen, die mehrere Überladungen aufweist. Benutzt wird in diesem Beispiel die einfach parametrisierte Version, der ein Suchkriterium als Zeichenfolge übergeben wird. Die Zeichenfolge entspricht der WHERE-Klausel in einer SELECT-Abfrage ohne die Angabe von WHERE. Zum Schluss wird auch noch die fünfte Datenzeile aus der Liste »gelöscht«.
An der Konsole werden abschließend nur die Datenzeilen angezeigt, die in irgendeiner Form gegenüber dem Original eine Änderung erfahren haben.
// Beispiel: ..\Kapitel 34\AusgabeModifizierterDaten
class Program {
static void Main(string[] args) {
SqlConnection con = new SqlConnection();
con.ConnectionString = "...";
SqlCommand cmd = new SqlCommand();
cmd.Connection = con;
cmd.CommandText = "SELECT ProductID, ProductName, UnitsInStock " +
"FROM Products";
DataSet ds = new DataSet();
SqlDataAdapter da = new SqlDataAdapter(cmd);
da.FillSchema(ds, SchemaType.Source);
ds.Tables[0].Columns["ProductID"].AutoIncrementSeed = -1;
ds.Tables[0].Columns["ProductID"].AutoIncrementStep = -1;
da.Fill(ds);
// Neue Datenzeile hinzufügen
DataRow newRow = ds.Tables[0].NewRow();
newRow["ProductName"] = "Camembert";
newRow["UnitsInStock"] = 100;
ds.Tables[0].Rows.Add(newRow);
// Datenzeile ändern
DataRow[] editRow = ds.Tables[0].Select("ProductName='Tofu'");
if (editRow.Length == 1) {
editRow[0].BeginEdit();
editRow[0]["UnitsInStock"] = 1000;
editRow[0].EndEdit();
}
else
Console.WriteLine("Datenzeile 'Tofu' nicht gefunden.");
// Datenzeile löschen
ds.Tables[0].Rows[4].Delete();
// Ausgabe
foreach (DataRow tempRow in ds.Tables[0].Rows) {
if (tempRow.RowState == DataRowState.Added)
Console.WriteLine("Neue Datenzeile: {0}", tempRow["Productname"]);
else if (tempRow.RowState == DataRowState.Modified) {
Console.WriteLine("Modifiziert: {0}", tempRow["Productname"]);
Console.WriteLine("Alter Wert: {0}",
tempRow["UnitsInStock", DataRowVersion.Original]);
Console.WriteLine("Neuer Wert: {0}", tempRow["UnitsInStock"]);
}
else if (tempRow.RowState == DataRowState.Deleted)
Console.WriteLine("Gelöscht: {0}",
tempRow["ProductName", DataRowVersion.Original]);
else
continue;
Console.WriteLine(new string('-', 40));
}
Console.ReadLine();
}
}
Listing 34.10 Anzeige modifizierter Datenzeilen
34.4.6 Manuelles Steuern der Eigenschaft »DataRowState«
Die Eigenschaft RowState ist für jede Datenzeile nach dem Füllen des DataSets auf Unchanged gesetzt. Je nachdem, ob Sie eine Datenzeile ändern, löschen oder hinzufügen, wird ihr Zustand automatisch auf Modified, Deleted oder Added gesetzt.
Mit zwei Methoden können Sie den RowState per Code beeinflussen: AcceptChanges und RejectChanges. Es handelt sich hierbei um Methoden, die Sie auf dem DataSet, der DataTable oder einer bestimmten DataRow aufrufen können.
Die Methode »AcceptChanges«
AcceptChanges setzt den RowState einer Datenzeile von Added oder Modified auf Unchanged. Dabei wird der Inhalt von DataRowVersion.Original durch den von DataRowVersion.Current beschriebenen Inhalt ersetzt.
Trifft die Methode auf eine gelöschte Datenzeile, wird die Datenzeile aus der DataRowCollection entfernt und RowState auf DataRowState.Detached gesetzt. Rufen Sie AcceptChanges auf die Referenz des DataSets auf, wird mit allen Datenzeilenänderungen in sämtlichen Tabellen so verfahren. Der Aufruf auf eine bestimmte Tabelle im DataSet wirkt sich dementsprechend nur auf die betreffenden Datenzeilen der Tabelle aus. Analog können Sie auch den Zustand einer bestimmten Datenzeile ändern.
Die Methode »RejectChanges«
Mit RejectChanges verwerfen Sie alle Änderungen. Die Methode setzt die aktuellen Werte der DataRow auf ihre ursprünglichen Werte zurück. Dabei werden die in der DataRow enthaltenen Änderungen verworfen, also:
DataRowVersion.Current = DataRowVersion.Original
Der RowState hängt nach dem Aufruf von RejectChanges vom anfänglichen RowState ab. Der Zustand Deleted oder Modified wird zu Unchanged, eine hinzugefügte Datenzeile wird zu Detached.
Die Methoden »SetAdded« und »SetModified«
SetAdded ändert den Zustand einer Datenzeile in Added und kann nur für eine DataRow aufgerufen werden, deren RowState den Wert Unchanged oder Added hat. Ist der Ausgangszustand ein anderer, wird die Ausnahme InvalidOperationException ausgelöst.
Dementsprechend ändert SetModified den Zustand in Modified. Der Einsatz dieser Methode beschränkt sich auf Datenzeilen, deren Ausgangszustand Unchanged ist. Ansonsten wird ebenfalls die eben erwähnte Ausnahme ausgelöst.
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.