Laden...

Inhalt einer DataTable wieder speichern

Erstellt von Harry B. vor 14 Jahren Letzter Beitrag vor 13 Jahren 7.929 Views
H
Harry B. Themenstarter:in
33 Beiträge seit 2009
vor 14 Jahren
Inhalt einer DataTable wieder speichern

Eine weitere Frage in meiner Lernphase:

Ich wollte die Änderungen in meinem DGV wie folgt speichern:

	private void btnSpeichern_Click(object sender, EventArgs e) {
		// Update the database with the user's changes.
		dataAdapter.Update((DataTable)bindingSource1.DataSource);
	}

Zur Laufzeit bekam ich dann die Meldung: "Aktualisieren erfordert einen gültigen UpdateCommand, wenn eine DataRow-Auflistung mit modifizierten Zeilen weitergegeben wird."

Nun ist dataAdapter vom Typ SQLiteDataAdapter. Wie erstelle ich denn so ein UpdateCommand?

Danke für eine Antwort!

Gruß, Harry B.

Kaum macht man 's richtig, schon funktioniert 's!

F
27 Beiträge seit 2009
vor 14 Jahren

Die Klasse CommandBuilder sollte dir da weiterhelfen:

Generieren von Befehlen mit 'CommandBuilder'-Objekten (ADO.NET)

H
Harry B. Themenstarter:in
33 Beiträge seit 2009
vor 14 Jahren

Das hatte ich bereits versucht, aber SQLCommandBuilder passte nicht zu meinnem SQLite-Adapter. Etwas googeln hat dann SQLiteCommandBuilder hervorgebracht.

Damit ist die o.g. Fehlermeldung jetzt weg. Dafür kommt eine neue: "Abort due to constraint violation PRIMARY KEY must be unique"

Da muss ich aber erst einmal schauen, was ich dagegen machen kann, bevor ich dazu etwas fragen werde. Denn eigentlich sollte diese Fehler nicht kommen.

Gruß, Harry B.

Kaum macht man 's richtig, schon funktioniert 's!

H
Harry B. Themenstarter:in
33 Beiträge seit 2009
vor 14 Jahren

Es ist doch erstaunlich: Man muss es nur mal klar durchdenken.

Ich hatte eine komplette row per drag&drop verschoben und danach eine Spalte neu durchnummeriert.

Das Verschieben war inklusive des primary keys erfolgt, und so kam es natürlich zu dem o.g. Fehler beim Update.

Wenn ich vor dem Update die Reihenfolge der Daten wieder anhand des primary keys wiederherstelle, klappt auch das Update.

Ohh, da werde ich noch einiges hier fragen und noch mehr lernen müssen ...

Es ist halt schwierig, wenn man von einer gewohnten Umgebung eine ganz neue Welt kennenlernen will: War früher nur die Programmlogik zu beachten, muss man jetzt auch noch die neue IDE und die neue Sprache erlernen und bedenken. Das ist nicht so enfach, wie ich gedacht hatte. Aber jeder winzige erfolgreiche Schritt macht mir Mut, den Weg weiterzugehen.

Gruß, Harry B.

Kaum macht man 's richtig, schon funktioniert 's!

H
Harry B. Themenstarter:in
33 Beiträge seit 2009
vor 14 Jahren

Das Speichern der Änderungen klappt nicht.

Die Ausgangstabelle sieht im DGV wie folgt aus:


ID     name     order
1       VW        1
2       Opel      2
3       Benz      3

Per drag&drop mache ich daraus


ID     name     order
2       Opel      2
3       Benz      3
1       VW        1

Durch Umnummerierung mache ich daraus


ID     name     order
2       Opel      1
3       Benz      2
1       VW        3

Wenn ich das jetzt so speichere


	private void btnSpeichern_Click(object sender, EventArgs e) {
		// Update the database with the user's changes.
		dataAdapter.Update((DataTable)bindingSource1.DataSource);
	}

bekomme ich diesen Fehler: "Abort due to constraint violation PRIMARY KEY must be unique"

Wenn ich aber die Tabelle vor dem Speichern nochmals per drag&drop wie folgt umsortiere:


ID     name     order
1       VW        3
2       Opel      1
3       Benz      2

klappt das Sepichern.

Fragen dazu:

  1. Woher kommt der Fehler?
  2. Wie kann ich das Speichern richtig machen?

Gruß, Harry B.

Kaum macht man 's richtig, schon funktioniert 's!

F
10.010 Beiträge seit 2004
vor 14 Jahren

Drag&Drop macht nichts alleine, sondern du wirst da etwas machen.

Du weisst, das eine dataRow sich den alten zustand merkt, und diesen dann
beim update überprüft?

Du darfst auf garkeinen fall die spaltenwerte von hand irgendwie austauschen
um eine andere reihenfolge zu erhalten.

Zeig doch mal was Du da veranstaltest.

J
3.331 Beiträge seit 2006
vor 14 Jahren

Hallo Harry,

die Fehlermeldung wurde mir erst während der folgenden Überlegungen klar. Denn nach deinen Beispielen bleibt der PK (ID) unverändert und sollte deshalb auch weiterhin eindeutig sein.

Eines deiner (Verständnis-) Probleme könnte damit zusammenhängen: Der PK hat nichts mit der Reihenfolge zu tun. Eine Datenbank-Tabelle ist ebenso wie eine DataTable eine Datenmenge, die grundsätzlich unsortiert ist. Der PK dient "nur" zum direkten Zugriff auf eine einzelne Zeile.

Vielleicht liegt das Hauptproblem darin, dass dein Umsortieren für eine Änderung der Datensätze sorgt: Der Update-Befehl benutzt InsertCommand für neue Zeilen, UpdateCommand für geänderte Zeilen und DeleteCommand für gelöschte Zeilen. Das überprüft er anhand des RowState. Wenn dein Drag&Drop (einschl. Umsortieren und "PK wiederherstellen") dazu führt, dass eine der Zeilen den DataRowState.Added bekommt, will der DataAdapter den InsertCommand ausführen - und damit knallt es.

Lösungsmöglichkeiten:

  1. Du verzichtest auf Drag&Drop (das führt wahrscheinlich zu "Added") und bestimmst die neue Reihenfolge z.B. durch Änderungen von "Order".
  2. Du verzichtest auf DbDataAdapter.Update, sondern benutzt in einer Schleife einen manuell erstellten UPDATE-Befehl per ExecuteNonQuery.
  3. Nach dem Umsortieren rufst du DataTable.AcceptChanges auf und änderst jede Zeile mit DataRow.SetModified; dann wählt sich DataAdapter.Update den UpdateCommand aus.

Achtung: **AcceptChanges **darf normalerweise nicht aufgerufen werden - das verhindert, dass ein folgender Update-Befehl irgendeine Wirkung hat. Nur in dieser speziellen Situation wäre es zulässig. Du darfst auch keinesfalls Teile der einen Lösung mit Teilen einer anderen Lösung mischen; das würde endgültig zum Chaos führen.

Zusatzhinweise: Wenn du in Beschreibungen wie :rtfm: oder 🛈 von Sql-Klassen oder in Foren von Db-Klassen liest, musst du immer die entsprechende SQLite-Klasse verwenden; das gilt nicht nur für den CommandBuilder, sondern für alle Datenbank-Maßnahmen. Für manuell erstellte DbCommands beachte unbedingt [Artikelserie] Parameter von SQL Befehlen.

Viel Erfolg und Frohes Neues Jahr! Jürgen

Nachtrag: Ich lese jetzt gerade deinen anderen Beitrag. Ich hoffe doch sehr, dass du wirklich eine DataTable benutzt? Deine Beschreibungen und mancher Code klingen so, als wenn du die Daten direkt in das DGV einträgst und dort manipulierst. Das solltest du dir abgewöhnen: Daten werden per Code nicht in GUI-Objekten bearbeitet, sondern im jeweiligen Daten-Objekt.

H
Harry B. Themenstarter:in
33 Beiträge seit 2009
vor 14 Jahren

So, es klappt jetzt. Ich habe die zweite Lösungsmöglichkeit gewählt.

Ob ich nun eine DataTable verwende, weiß ich nicht. Ich glaube wohl eher nicht, denn ich verwende ja sowas wie "_dgv.Rows[r].Cells[c].Value", um die Werte aus dem DataGridView in die DB zurückzuschreiben. _dgv ist dabei ein DataGridView-Object. Aber ich war erst einmal froh, dass ich auf die Werte zugreifen konnte ... 😉

Eine DataTable habe ich aber irgendwie auch:


	private SQLiteConnection _sqlConnection;
	private SQLiteCommand _sqlCommand;
	private SQLiteDataAdapter _sqlDataAdapter;
	private DataSet _dataSet = new DataSet();
	private DataTable _dataTable = new DataTable();
	private DataGridView _dgv;
	private BindingSource _bindingSource;
	private SQLiteCommandBuilder _sqlCmdBuilder;
...
	_sqlCommand = _sqlConnection.CreateCommand();
	string CommandText = "select * from ChannelInfo";
	_sqlDataAdapter = new SQLiteDataAdapter(CommandText, _sqlConnection);
	_sqlCmdBuilder = new SQLiteCommandBuilder(_sqlDataAdapter);
	_sqlCmdBuilder.SetAllValues = false;

	_dataSet.Reset();
	_sqlDataAdapter.Fill(_dataSet);
	_dataTable = _dataSet.Tables[0];
			
	//BindingSource to sync DataTable and DataGridView
	_bindingSource = new BindingSource();

	//set the BindingSource DataSource
	_bindingSource.DataSource = _dataTable;

	//set the DataGridView DataSource
	_dataGridView.DataSource = _bindingSource;

(Wie) soll ich die jetzt verwenden anstatt "_dgv.Rows[r].Cells[c].Value"?

Gruß, Harry B.

Kaum macht man 's richtig, schon funktioniert 's!

J
3.331 Beiträge seit 2006
vor 14 Jahren

Ganz, ganz einfach: mit :rtfm: DataGridView.DataSource, und das benutzt du doch schon. Wozu dann eigentlich dgv.Row[].Cells[].Value? 🙄 Zum ganzen Zusammenhang siehe 🛈 Kap.25 ff.

Gruß Jürgen

H
Harry B. Themenstarter:in
33 Beiträge seit 2009
vor 14 Jahren

Jetzt speichere ich die Änderungen wie folgt:


private void btnSpeichern_Click(object sender, EventArgs e) {
	int inxChOrder = _dgv.Columns["ch_order"].Index;
	int inxChId = _dgv.Columns["ch_id"].Index;
	DataTable tbl = _dgv.DataSource;

	using (SQLiteCommand cmd = new SQLiteCommand("UPDATE ChannelInfo SET ch_order = @ch_order WHERE ch_id = @ch_id", _sqlConnection)) {
		_sqlConnection.Open();
		for (int r = 0; r < tbl.Rows.Count; r++) {
			cmd.Parameters.AddWithValue("@ch_order", tbl.Rows[r][inxChOrder]);
			cmd.Parameters.AddWithValue("@ch_id", tbl.Rows[r][inxChId]);
			cmd.ExecuteNonQuery();
		}
		_sqlConnection.Close();
	}
}

Zuvor hatte ich es so gemacht:


...
		for (int r = 0; r < _dgv.RowCount; r++) {
			cmd.Parameters.AddWithValue("@ch_order", _dgv.Rows[r].Cells[inxChOrder].Value);
			cmd.Parameters.AddWithValue("@ch_id", _dgv.Rows[r].Cells[inxChId].Value);
			cmd.ExecuteNonQuery();
		}
...

Ich sehe da noch nicht so den Riesenunterschied, aber wenn es empfohlen wird, werde ich gerne versuchen, mich daran zu orientieren.

Bis hierhin zunächst einmal ein großes DANKE für die Geduld und die hilfreichen Tipps!

Gruß, Harry B.

Kaum macht man 's richtig, schon funktioniert 's!

J
3.331 Beiträge seit 2006
vor 14 Jahren

Hallo Harry,

ich werde jetzt nicht alles begründen und erläutern, was in den Links steht. Aber ich möchte dich auf die Punkte hinweisen, die besser gemacht werden sollten:

  1. Eine **Connection **soll nicht dauerhaft bestehen bleiben, sondern nur solange, wie sie im Moment benötigt wird (also innerhalb deiner Speichern-Methode). Dazu wird sie vorzugsweise ebenso in einen using-Block eingebaut, siehe [Artikel] Ressourcen schonen - Datenbanken richtig öffnen und schließen.

  2. Die Parameter sollten vor der Schleife erstellt werden mit einer der Varianten von Parameters.Add, Näheres in dem Parameter-Artikel (siehe oben). Innerhalb der Schleife wird jeweils **Value **neu zugewiesen.

  3. Das hier ist sinnfrei:

DataTable tbl = _dgv.DataSource;

Du machst es doch vorher richtig:

    _bindingSource.DataSource = _dataTable;
    _dataGridView.DataSource = _bindingSource;

Damit sind DataTable und DGV verbunden. Vor dem Speichern wird noch _bindingSource.EndEdit erledigt; dann verfügt die DataTable automatisch, endgültig und vollkommen über die aktuellen Daten.

  1. Das hier ist fast sinnfrei:
    int inxChOrder = _dgv.Columns["ch_order"].Index;
    int inxChId = _dgv.Columns["ch_id"].Index;

Greife stattdessen auf DataTable.Columns zu. (In der Praxis kommt es fast auf dasselbe heraus; aber was ist, wenn der Nutzer die DGV-Spalten selbst verschieden darf?)

Merke: Maßgebend sind nicht die GUI-Objekte, sondern die Daten-Objekte. Im Code hast du dich fast ausschließlich darum zu kümmern.

Gruß Jürgen

H
Harry B. Themenstarter:in
33 Beiträge seit 2009
vor 14 Jahren

Vielen Dank für die vielen Informationen!

Ich werde sie mir ansehen und versuchen, alles richtig zu verstehen! Falls ich nähere Informationen brauche, werde ich mich hierzu wieder melden.

Jetzt weiß ich aber dank Deiner Hinweise schon mal, wie der richtige Weg aussieht und stochere nicht nur im Nebel des Neuen herum.

Gruß, Harry B.

Kaum macht man 's richtig, schon funktioniert 's!

H
Harry B. Themenstarter:in
33 Beiträge seit 2009
vor 14 Jahren

Mittlerweile sieht meine Methode zum Speichern wie folgt aus:


private void btnSpeichern_Click(object sender, EventArgs e) {
	int inxChOrder = _dataTable.Columns["ch_order"].Ordinal;
	int inxChId = _dataTable.Columns["ch_id"].Ordinal;

	this.Cursor = Cursors.WaitCursor;
			
	// alle Eingaben beenden
	_bindingSource.EndEdit();

	// DB-Connection aufbauen
	using (SQLiteCommand cmd = new SQLiteCommand("UPDATE ChannelInfo SET ch_order = @ch_order WHERE ch_id = @ch_id", _sqlConnection)) {
		_sqlConnection.Open();
		cmd.Parameters.Add("@ch_order", DbType.Int32);
		cmd.Parameters.Add("@ch_id", DbType.Int32);

		// Werte in die DB speichern
		foreach (DataRow row in _dataTable.Rows) {
			cmd.Parameters["@ch_order"].Value = row[inxChOrder];
			cmd.Parameters["@ch_id"].Value = row[inxChId];
			cmd.ExecuteNonQuery();
		}
		_sqlConnection.Close();
	}
	this.Cursor = Cursors.Default;
}

  1. Was mich persönlich noch stört, sind die beiden Ausdrücke "DbType.Int32". Wie kann ich das besser machen?

  2. Bringt es etwas z. B. an Performance oder Übersicht, wenn ich wie oben inxChOrder und inxChId in der Schleife verwende?

  3. Wie hält man es mit der Verwendung von "this"? Sollte man es wann immer möglich verwenden? Ich habe es aber in der Methode häufig vergessen. (Leider?) funktioniert es auch ohne "this".

Gruß, Harry B.

Kaum macht man 's richtig, schon funktioniert 's!

J
3.331 Beiträge seit 2006
vor 14 Jahren
  1. Was mich persönlich noch stört, sind die beiden Ausdrücke "DbType.Int32". Wie kann ich das besser machen?

Gar nicht, das ist fast völlig korrekt. Was stört dich daran? Du musst dem DbProvider einmal sagen, welcher Typ hier ankommt; du solltest ihn allenfalls in einen SQLiteDbType ändern; aber das läuft vermutlich auf dasselbe hinaus.

  1. Bringt es etwas z. B. an Performance oder Übersicht, wenn ich wie oben inxChOrder und inxChId in der Schleife verwende?

Es bringt minimalen Vorteil, wenn die Zahl einmalig geholt und dann so wie bei dir benutzt wird. Aber der ist so winzig, dass man ihn nicht spürt.

  1. Wie hält man es mit der Verwendung von "this"? Sollte man es wann immer möglich verwenden? Ich habe es aber in der Methode häufig vergessen. (Leider?) funktioniert es auch ohne "this".

Das ist fast eine Glaubensfrage. Man kann eigentlich immer darauf verzichten; nur dann, wenn identische Bezeichner zusammenstoßen (wie innerhalb eines Konstruktors oder einer Set-Methode) oder wenn mehrere Konstruktoren verbunden werden, braucht man es.

Wenn man es weglässt, bezieht man sich sowieso auf die aktuelle Instanz der aktuellen Klasse. Also kann es keine Unklarheiten geben.

Was mich erheblich mehr stört, ist die falsche Verwendung der Connection, siehe Punkt 1 in meinem vorigen Beitrag.

Gruß Jürgen

H
Harry B. Themenstarter:in
33 Beiträge seit 2009
vor 14 Jahren

Gut, ich habe noch mal was geändert:


// DB-Connection aufbauen
using (SQLiteConnection sqlConn = new SQLiteConnection("Data Source=db.dat;Version=3;New=False;Compress=True;"))
using (SQLiteCommand cmd = new SQLiteCommand("UPDATE ChannelInfo SET ch_order = @ch_order WHERE ch_id = @ch_id", sqlConn)) {
	sqlConn.Open();
...
	cmd.ExecuteNonQuery();
				}
	//sqlConn.Close();	nicht erforderlich, da das im Finalyzer mit Dispose erledigt wird.
}

Dann noch einmal zu #1:

Mich stört, dass ich hier den Typ explizit angebe. Wenn ich den mal ändere, muss ich auch an diese Stelle denken. Daher würde ich das gern anders machen.

Die Verwendung von SQLiteDbType wird bei mir als "im aktuellen Kontext nicht vorhanden" angemeckert. Welche using-Anweisung müsste ich da ergänzen?

Gruß, Harry B.

Kaum macht man 's richtig, schon funktioniert 's!

J
3.331 Beiträge seit 2006
vor 14 Jahren

Gut, ich habe noch mal was geändert:

// DB-Connection aufbauen

So ist es schön! 👍

Dann noch einmal zu #1:
Mich stört, dass ich hier den Typ explizit angebe. Wenn ich den mal ändere, muss ich auch an diese Stelle denken. Daher würde ich das gern anders machen.

Ich sagte schon: Wenn du es nicht mit AddWithValue machst - und das passt bei einer Schleife nicht -, dann muss der Typ angegeben werden.

Die Verwendung von SQLiteDbType wird bei mir als "im aktuellen Kontext nicht vorhanden" angemeckert. Welche using-Anweisung müsste ich da ergänzen?

Nix. Es geht um die spezielle Ableitung von DbType (das ist die Basisklasse) für deinen Provider. Wie die Klasse genau heißt, musst du nachschlagen; aber sie liegt mit Sicherheit im selben Namespace wie die anderen SQLite-Klassen.

Gruß Jürgen

H
Harry B. Themenstarter:in
33 Beiträge seit 2009
vor 14 Jahren

Wenn du es nicht mit AddWithValue machst - und das passt bei einer Schleife nicht -, dann muss der Typ angegeben werden.

Das will ich ja auch gerne tun. Aber man wird den Typ einer Spalte doch auslesen können, oder?

Gruß, Harry B.

Kaum macht man 's richtig, schon funktioniert 's!

J
3.331 Beiträge seit 2006
vor 14 Jahren

Natürlich. Aber der Provider will beim Ausführen eines DbCommands wissen, was er zu übergeben hat; dazu will er wissen, von welchem Datentyp die Werte sind, die in den Parametern stecken, und als welcher DB-Typ sie ankommen sollen. Das wird durch die richtige Add-Variante gesteuert. (Es gibt noch andere Varianten - schau dir einmal die vielen verschiedenen Parameter-Konstruktoren an).

Gruß Jürgen

H
Harry B. Themenstarter:in
33 Beiträge seit 2009
vor 14 Jahren

Da ich mal wieder etwas Zeit habe, hier noch eine Frage zu meiner Speicher-Methode aus dem Beitrag vom 05.01.2010 11:23 Uhr.

So wie ich das dort programmiert habe, ist das total langsam. Ich vermute, dass es daran liegt, dass cmd.ExecuteNonQuery(); innerhalb einer Schleife ausgeführt wird.

Wie kann ich das anders machen, um das Speichern drastisch zu beschleunigen?

Gruß, Harry B.

Kaum macht man 's richtig, schon funktioniert 's!

H
Harry B. Themenstarter:in
33 Beiträge seit 2009
vor 13 Jahren

Ich hatte mal wieder etwas Zeit und wollte meine Update-Methode etwas anders gestalten. Das Folgende funktioniert bereits gut:

		
private void btnSpeichern_Click(object sender, EventArgs e) {
int inxChOrder = _dataTable.Columns["ch_order"].Ordinal;
int inxChId = _dataTable.Columns["ch_id"].Ordinal;

	this.Cursor = Cursors.WaitCursor;

	Validate();
	// alle Eingaben beenden
	_bindingSource.EndEdit();

	// DB-Connection aufbauen
	using (SQLiteCommand cmd = new SQLiteCommand("UPDATE ChannelInfo SET ch_order = @ch_order WHERE ch_id = @ch_id", _sqlConnection)) {

		SQLiteParameter pChOrder = new SQLiteParameter("@ch_order");
		SQLiteParameter pChId = new SQLiteParameter("@ch_id");

		cmd.Parameters.Add(pChOrder);
		cmd.Parameters.Add(pChId);

		_sqlConnection.Open();

		foreach (DataRow row in _dataTable.GetChanges().Rows) {
			cmd.Parameters["@ch_order"].Value = row[inxChOrder];
			cmd.Parameters["@ch_id"].Value = row[inxChId];
			cmd.ExecuteNonQuery();
	}

	_sqlConnection.Close();	
}

Jetzt wollte ich es etwas besser strukturieren und so das Update so machen:


private void btnSpeichern_Click(object sender, EventArgs e) {

	_bindingSource.EndEdit();

	_sqlDataAdapter.UpdateCommand = CreateUpdateCommand(_sqlConnection);
	_sqlDataAdapter.InsertCommand = CreateInsertCommand(_sqlConnection);
	_sqlDataAdapter.DeleteCommand = CreateDeleteCommand(_sqlConnection);

	this.Cursor = Cursors.WaitCursor;

	_sqlConnection.Open();
	_sqlDataAdapter.Update(_dataSet.Tables[0]);
	_sqlConnection.Close();

	this.Cursor = Cursors.Default;

}

Der Update-Befehl wird dabei wie folgt erstellt:


public static SQLiteCommand CreateUpdateCommand(SQLiteConnection con) {
	
	SQLiteCommand cmd = new SQLiteCommand("UPDATE ChannelInfo SET ch_order = @ch_order WHERE ch_id = @ch_id", con);

	SQLiteParameter pChOrder = new SQLiteParameter("@ch_order", SqlDbType.Int);
	SQLiteParameter pChId = new SQLiteParameter("@ch_id", SqlDbType.Int);

	cmd.Parameters.Add(pChOrder);
	cmd.Parameters.Add(pChId);
	pChId.SourceVersion = DataRowVersion.Original;

	return cmd;
	
}

Leider werden aber so keine Veränderungen in die Tabelle geschrieben. Woran liegt das?

Gruß, Harry B.

Kaum macht man 's richtig, schon funktioniert 's!

H
Harry B. Themenstarter:in
33 Beiträge seit 2009
vor 13 Jahren

Hat niemand eine Idee zu meinem geschilderten Problem? Ich will doch nur lernen ... 🙂

Gruß, Harry B.

Kaum macht man 's richtig, schon funktioniert 's!

F
10.010 Beiträge seit 2004
vor 13 Jahren

Naja, dazu solltest Du einfach mal anfangen die Grundlagen des DataAdapters anzuschauen.

Du musst ihm natürlich bei den Parametern auch sagen welche Spalte sie benutzen sollen.

Aber das ganze brauchst du nicht, wenn du verstehst warum der SQLiteCommandBuilder UpdateCommands erzeugt, die die o.a. Fehlermeldung produzieren.