Benutzer mit den meisten Antworten
DGV - Binding Source UpdateCommand Parallelitätsverletzung

Frage
-
Hallo zusammen,
ich habe ein DGV welches per BindingSource aus einer Tabelle aus SQL-Server befüllt wird.
Beim Laden mit "SELECT * FROM [WHERE = Multifilter]". Es gibt eine Reihe von Comboboxen und Textfeldern, die im Change- oder LeaveEvent jeweils die BindingSource über den 'Multifilter' neu erstellen.Das funktioniert im allgemeinen wunderbar, da sich die Mitarbeiter ihren Filter recht genau abgrenzen (Kundenbereich,Lieferant etc.) Selten, sehr selten, kommt aber 'Das UpdateCommand hat sich auf 0 der 1 Datensätze ausgewirkt'. [Wobei ..0 der 1... wohl eher UpdateCommand Succes=False ist oder irre ich mich?)
Mir ist klar, dass wenn Meyer und Schulze gleichzeitig im Datensatz ID 1234 rumfuhrwerken sowas passieren kann.Ist es möglich den genauen Datensatz 1234 zu identifizieren, der nicht aktualisiert wurde (ID ist Primary) oder - noch schlimmer- werden dann alle nicht aktualisiert?
Gruß
Raimo
Antworten
-
-Zitate kriege ich immer noch nicht hin--
Diesen Datensatz kann man über die Ereignisse des CommandBuilders abfangen.
Hi,
ein Zitat kannst Du mit "Zitieren" vorn anstellen oder <blockquote> im html kennzeichnen.Der CommandBuilder wird nicht für jeden Filter neu gesetzt. Es wird ein CommandBuilder instanziiert, dem der betreffende DataAdapater mitgeteilt wird. Intern werden DataAdapter und CommandBuilder verknüpft. Wenn jetzt entsprechend RowState eine Aktivität mit der Datenbank auszuführen ist, wird der CommandBuilder nach der auszuführenden SQL Anweisung gefragt.
Nachfolgend habe ich kleine Konsolen-Demo zusammengestellt.
Imports System.Data.SqlClient Module Module5 Public Sub Main() Demo1() Console.Write("--- mit Tastendruck Programm beenden") Console.ReadKey() End Sub Dim rnd As New Random Sub Demo1() Console.WriteLine("--- Arbeit mit klasssichen Methoden mit Konflikterkennung") Try Using cn As New SqlConnection(My.Settings.cn) Using da As New SqlDataAdapter("Select * FROM TestTab", cn) ' Datenpuffer Dim dt1 As New DataTable ' Nutzer 1 Dim dt2 As New DataTable ' Nutzer 2 ' Datenpuffer füllen da.Fill(dt1) da.Fill(dt2) ' Anzahl der geladenen Datensätze Dim anzahl = dt1.Rows.Count Console.WriteLine("es wurden {0} Datensätze geladen", anzahl) ' Datensatznummer für zu ändernden datensatz festlegen Dim satznummer = anzahl \ 2 ' zu ändernder Datensatz Dim r1 = dt1.Rows(satznummer) ' r1(1) ist vom Typ "money" = "decimal" Console.WriteLine("ID: {0}, alter Inhalt: {1}, Zustand: {2}", r1(0), r1(1), r1.RowState) ' Feldinhalt ändern r1(1) = CDec(rnd.Next(10000) / 100) Console.WriteLine("ID: {0}, neuer Inhalt: {1}, Zustand: {2}", r1(0), r1(1), r1.RowState) ' Instanz des CommandBuilders erzeugen Dim cb As New SqlCommandBuilder(da) ' Konflikterkennung setzen cb.ConflictOption = ConflictOption.CompareAllSearchableValues ' Vor dem Aktualisieren Arbeiten ausführen AddHandler da.RowUpdating, Sub(sender As Object, e As SqlRowUpdatingEventArgs) Console.WriteLine("Update wird ausgeführt für ID: {0}", e.Row(0)) End Sub ' Datenbank aktualisieren da.Update(dt1) ' weitere Änderung durch anderen Nutzer simulieren ' zu ändernder Datensatz Dim r2 = dt2.Rows(satznummer) ' r2(1) ist vom Typ "money" = "decimal" Console.WriteLine("ID: {0}, alter Inhalt: {1}, Zustand: {2}", r2(0), r2(1), r2.RowState) ' Feldinhalt ändern r2(1) = CDec(rnd.Next(10000) / 100) Console.WriteLine("ID: {0}, neuer Inhalt: {1}, Zustand: {2}", r2(0), r2(1), r2.RowState) ' nochmals Datenbank aktualisieren da.Update(dt2) ' vernünftig abschließen End Using End Using Console.WriteLine("--- Ende Demo1") Catch ex As Exception Console.WriteLine(ex.Message) End Try End Sub End Module
--
Viele Grüsse
Peter Fleischer (MVP, Partner)
Meine Homepage mit Tipps und Tricks- Als Antwort markiert RaimoBecker Donnerstag, 26. März 2015 20:22
Alle Antworten
-
Hi Raimo,
die Standardmittel im .NET arbeiten mit den Datenbanken mit einem clientseitigem Cursor. Das bedeutet, dass Daten aus der Datenbank in einen Puffer im Client geladen werden und der Zustand der Daten wird im Client verfolgt. Zum Zeitpunkt eines Updates werden die Daten im Client mit der Datenbank synchronisiert. Wenn nicht "der Letzte gewinnt" eingestellt ist, dann wird bei Änderungen vor der Änderung geprüft, ob die Daten in der Datenbank noch mit dem Inhalt übereinstimmen, der beim Laden der Daten in den Client vorlag. Das sichert, dass nur dann der Inhalt der Datenbank geändert wird, wenn in der Zwischenzeit kein anderer Anwender die Daten geändert hat. Diese Prüfung und Änderung betrifft in jedem Schritt immer nur einen Datensatz. Anhand des im Client zu jedem Datensatz gespeicherten Zustandes, wird dann entschieden, was beim Update (Synchronisieren mit der Datenbank) mit jedem Datensatz zu machen ist (deleted -> Delete, added -> Insert, modified -> Update). Parallelitätsverletzung bedeutet, dass beim Update oder Delete eines Datensatzes dieser zwischenzeitlich durch einen anderen Anwender geändert wurde.Um das Problem zu lösen, muss zuerst ein Konzept zur Lösung von Konflikten erarbeitet werden, d.h., was ist zu machen, wenn ein Anwender einen Datensatz speichern, der vorher geladen wurde und zwischenzeitlich durch einen anderen Anwender verändert wurde. Mögliche Lösungswege sind:
- der letzte gewinnt (ggf. nach Nachfrage)
- der betreffende Datensatz wird nochmals geladen und der Anwender muss seine Kaffeepausen verkürzen, um schneller die Änderungen ablegen zu können
- beim Update werden nur die wirklich geänderten Spalten aktualisiert (muss selbst programmiert werden)
- Datensätze bekommen ein Verfallszeitpunkt und softwareseitig wird vor Erreichen des Verfallszeitpunkt eine Änderung verhindert (z.B. Buchungssysteme)
- es gibt bestimmt noch mehr Szenarien.
--
Viele Grüsse
Peter Fleischer (MVP, Partner)
Meine Homepage mit Tipps und Tricks -
Hallo Peter,
zunächst einmal komme ich mit der Zitierfunktion nicht klar. Sorry. Deswegen CopyPaste aus Deinem Post:
- der betreffende Datensatz wird nochmals geladen und der Anwender muss seine Kaffeepausen verkürzen, um schneller die Änderungen ablegen zu können
- beim Update werden nur die wirklich geänderten Spalten aktualisiert (muss selbst programmiert werden).
Zum Ersten: Da die User jederzeit den Filter und damit die BS ändern können wird es wohl zutreffen: Bsp.
Meyer zieht PLZ 9xxxx und fängt an zu arbeiten (90 Datensätze)
Schulze geht ne Stunde später rein und zieht sich auch 9xxxx rein ins DGV,
Meyer speichert, hat 90 DS vollständig bearbeitet
Schulze speichert 1 (einen) - sind dann die 89 unbearbeiteten anderen futsch?Wenn Du mir sagst, dass es nur einer ist, ist es zwar einer zuviel, aber... welcher?
Letzter gewinnt ist oK - Überschreiben ist halt Fehleingabe-, wenn es sich im einen DS handelt, aber die anderen DS machen mir Sorgen.
Zum zweiten: Ich denke das könnte ich hinkriegen, die Frage ist dann halt "wie lang wie viel" und ich traue der Performance nicht. Jedes DGV.CellEndEdit mit
UPDATE Table Set mySpalte = DGV.CurrentCell.Value WHERE ID = 'Hier die ID aus dem DGV'
per SQLCommand abzuschießen, ist ja wohl auch nich der Weisheit letzter Schluss
Die Idee mit den Verfallszeitpunkten habe ich nicht verstanden.
Gruß
Raimo
- Bearbeitet RaimoBecker Freitag, 20. März 2015 20:17
-
Hi Raimo,
zuerst wäre zu klären, ob die Änderung des Filters nur die in den Client geladenen Daten neu filtert, oder, ob die Daten erneut aus der Datenbank abgerufen werden. Wenn die Daten erneut geladen werden, reduziert sich die Wahrscheinlichkeit von Konflikten.Wenn Meyer 90 Datensätze bearbeitet hat und gespeichert hat, dann sind diese auch in der Datenbank (vorausgesetzt, niemand hat zwischenzeitlich gespeichert).
Nehmen wir an, Schulze hat vor Meyer und/oder vor dem Abspeichern durch Meyer die gleichen 90 Datensätze in seinen Client geladen und dann nur einen Datensatz geändert. Wenn jetzt nachdem Meyer seine 90 Datensätze abgespeichert hat, Schulze ein Update für seine 90 Datensätze auslöst, wird für jeden Datensatz dessen Status geprüft. Alle Datensätze mit Modus=unchanged werden übergangen, da nichts geändert wurde. Der eine Datensatz mit Modus=modified wird mit einem Update in der Datenbank geändert. Das schlägt aber fehl, weil zwischenzeitlich ein Änderung durch Meyer abgespeichert wurde. Diesen Datensatz kann man über die Ereignisse des CommandBuilders abfangen.
Zur Reduzierung der Wahrscheinlichkeit von Konflikten sollte vor dem Bearbeiten eines Datensatzes dieser nochmals geladen werden. Außerdem ist trotzdem die Konfliktlösungsstrategie zu implementieren, da zwischenzeitlich auch in diesem Fall durch einen anderen Anwender Änderungen abgespeichert werden können.
Es ist nicht notwendig, nach jedem CellEdit ein Update auszuführen. Vielmehr sollte davon ausgegangen werden, dass in den seltensten Fällen zwei Anwender die gleichen Feldinhalte bearbeiten. Und für diese seltenen Fälle reicht meist eine einfache Konfliktlösungsstrategie, wie das Nachladen der Originalwerte und eine Aufforderung an den Anwender, seine Eingaben nochmals zu überprüfen. Wenn zwei Anwender den gleichen Datensatz bearbeiten, dann werden meist unterschiedliche Prozesse bearbeitet und damit auch unterschiedliche Spalten. Das kann man in unterschiedlichen Datenmodellen berücksichtigen und es muss keine Konflikte geben.
Zusammengefasst sollte zuerst eine Konfliktlösungsstrategie erarbeitet werden.
--
Viele Grüsse
Peter Fleischer (MVP, Partner)
Meine Homepage mit Tipps und Tricks -
Hallo Peter,
durch den Filter wird tatsächlich die Datenherkunft geändert, also das "WHERE xy= AND ... AND ... AND ..."
Wie gesagt, man kann damit leben, dass Einträge überschrieben werden. "Das war immer schon so"-Zitate kriege ich immer noch nicht hin--
Diesen Datensatz kann man über die Ereignisse des CommandBuilders abfangen.
- Der CommandBuilder wird für jeden Filter vom Framework neu gesetzt, ich könnte den abgreifen und verändern. Aber wie? Das Objekt habe ich noch nie manipuliert.
Werde ich mal nachschauen was drin steht ----- ich sehe zuviele KlammeraffenNur mal BTW:
Die "Konfliktlösungsstrategie", letzter Eintrag gilt, ist für den Kunden ok. Who pays last is in the records
Danke Dir Peter
-
-Zitate kriege ich immer noch nicht hin--
Diesen Datensatz kann man über die Ereignisse des CommandBuilders abfangen.
Hi,
ein Zitat kannst Du mit "Zitieren" vorn anstellen oder <blockquote> im html kennzeichnen.Der CommandBuilder wird nicht für jeden Filter neu gesetzt. Es wird ein CommandBuilder instanziiert, dem der betreffende DataAdapater mitgeteilt wird. Intern werden DataAdapter und CommandBuilder verknüpft. Wenn jetzt entsprechend RowState eine Aktivität mit der Datenbank auszuführen ist, wird der CommandBuilder nach der auszuführenden SQL Anweisung gefragt.
Nachfolgend habe ich kleine Konsolen-Demo zusammengestellt.
Imports System.Data.SqlClient Module Module5 Public Sub Main() Demo1() Console.Write("--- mit Tastendruck Programm beenden") Console.ReadKey() End Sub Dim rnd As New Random Sub Demo1() Console.WriteLine("--- Arbeit mit klasssichen Methoden mit Konflikterkennung") Try Using cn As New SqlConnection(My.Settings.cn) Using da As New SqlDataAdapter("Select * FROM TestTab", cn) ' Datenpuffer Dim dt1 As New DataTable ' Nutzer 1 Dim dt2 As New DataTable ' Nutzer 2 ' Datenpuffer füllen da.Fill(dt1) da.Fill(dt2) ' Anzahl der geladenen Datensätze Dim anzahl = dt1.Rows.Count Console.WriteLine("es wurden {0} Datensätze geladen", anzahl) ' Datensatznummer für zu ändernden datensatz festlegen Dim satznummer = anzahl \ 2 ' zu ändernder Datensatz Dim r1 = dt1.Rows(satznummer) ' r1(1) ist vom Typ "money" = "decimal" Console.WriteLine("ID: {0}, alter Inhalt: {1}, Zustand: {2}", r1(0), r1(1), r1.RowState) ' Feldinhalt ändern r1(1) = CDec(rnd.Next(10000) / 100) Console.WriteLine("ID: {0}, neuer Inhalt: {1}, Zustand: {2}", r1(0), r1(1), r1.RowState) ' Instanz des CommandBuilders erzeugen Dim cb As New SqlCommandBuilder(da) ' Konflikterkennung setzen cb.ConflictOption = ConflictOption.CompareAllSearchableValues ' Vor dem Aktualisieren Arbeiten ausführen AddHandler da.RowUpdating, Sub(sender As Object, e As SqlRowUpdatingEventArgs) Console.WriteLine("Update wird ausgeführt für ID: {0}", e.Row(0)) End Sub ' Datenbank aktualisieren da.Update(dt1) ' weitere Änderung durch anderen Nutzer simulieren ' zu ändernder Datensatz Dim r2 = dt2.Rows(satznummer) ' r2(1) ist vom Typ "money" = "decimal" Console.WriteLine("ID: {0}, alter Inhalt: {1}, Zustand: {2}", r2(0), r2(1), r2.RowState) ' Feldinhalt ändern r2(1) = CDec(rnd.Next(10000) / 100) Console.WriteLine("ID: {0}, neuer Inhalt: {1}, Zustand: {2}", r2(0), r2(1), r2.RowState) ' nochmals Datenbank aktualisieren da.Update(dt2) ' vernünftig abschließen End Using End Using Console.WriteLine("--- Ende Demo1") Catch ex As Exception Console.WriteLine(ex.Message) End Try End Sub End Module
--
Viele Grüsse
Peter Fleischer (MVP, Partner)
Meine Homepage mit Tipps und Tricks- Als Antwort markiert RaimoBecker Donnerstag, 26. März 2015 20:22
-
Guten Morgen Peter,
das mit dem AddHandler und CommandBuilder.ConflictOption hört sich schon mal sehr gut an.
Ich werde das mal (Falls doch noch Lust und Zeit ist dieses Wochenende) prüfen.Mein "Filter" setzt übrigens den SQL-Adapter jedemal neu, also inklusive New CommandBuilder, Neue Datatable.
Ob das was ändert, kann ich nicht sagen-Gruß
Raimo
-
Hi Raimo,
den DataAdapter brauchst Du nicht jedes Mal neu zu instanziieren. Es ist problemlos möglich, für jede neue Filterkombination einem Command-Object vor der Ausführung (Fill) eine neue SQL Anweisung zuzuweisen. Dieses Command-Objekt ist der SelectCommand-Eigenschaft des DataAdapters zuzuweisen oder das bereits implizit erzeugte Command-Objekt zu nutzen.--
Viele Grüsse
Peter Fleischer (MVP, Partner)
Meine Homepage mit Tipps und Tricks -
Hallo Peter,
letztendlich war es ein "echter" Konflikt. Einkäufer Meyer arbeitete genauso wie Einkäufer Schulze korrekt.
Im Datensatz stand bei Einkäufer "Meyer/Schulze/nochEinName".Was auch korrekt ist, da F&E noch nicht genau weiß, wer der Einkäufer sein wird, wenn das Produkt freigegeben/bestellt wird.
Die Combobox filtert oder setzt die Datenherkunft aber auf "Buyer...Like '%" & cboEinkäufer.text & "%'". Deshalb hatten beide den DS im Result des SQL.
Ich habe ein ExceptionLogFile in die Anwendung eingebaut und die Kollegen gefragt. Es kommt (wohl) weit weniger als einmal pro Monat vor. Und wenn die wissen, dass es daran liegen könnte... Good old Telefon und absprechen.
DankeRaimo