none
DGV - Binding Source UpdateCommand Parallelitätsverletzung RRS feed

  • 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

    Freitag, 20. März 2015 17:25

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
    Samstag, 21. März 2015 06:38

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

    Freitag, 20. März 2015 19:15
  • 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



    Freitag, 20. März 2015 20:04
  • 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

    Freitag, 20. März 2015 21:06
  • 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 Klammeraffen

    Nur mal BTW:

    Die "Konfliktlösungsstrategie", letzter Eintrag gilt, ist für den Kunden ok. Who pays last is in the records

    Danke Dir Peter

    Freitag, 20. März 2015 21:38
  • -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
    Samstag, 21. März 2015 06:38
  • 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

    Samstag, 21. März 2015 07:25
  • 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

    Samstag, 21. März 2015 07:54
  • 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.
    Danke

    Raimo

    Donnerstag, 26. März 2015 20:34