Dataset->Db (DbPersistance)

    • VB.NET
    • .NET (FX) 4.0

    Es gibt 3 Antworten in diesem Thema. Der letzte Beitrag () ist von ErfinderDesRades.

      Dataset->Db (DbPersistance)

      Die Änderungs-Verfolgung des Datasets notiert jede Änderung, sodass zu jedem Zeitpunkt bekannt ist, welche DataRows zugefügt/gelöscht/geändert wurden.
      Dementsprechend kann man eine allgemeine Abspeicher-Methode schreiben, die zu jedem Zeitpunkt alle Abweichungen des Datenbestandes der Db übermittelt - sie also updated.
      Die Methode verwendet selbstverständlich DbParameter, sodass auch Sql-Injection - Angriffen ein Riegel vorgeschoben ist.

      Seit 2010 versucht Microsoft so etwas bereitzustellen mit dem TableAdapterManager, der generiert wird, wenn man mittels Connector sich ein typisiertes Dataset generieren lässt (s.auch: Db-Zugriff mit Connectoren).
      Die TableAdapterManager.UpdateAll()-Methode funktioniert leider nur bei Datenbanken, die In-/Out-DbParameter unterstützen: SqlServer, MySql, SqLite,...
      Bei anderen Datenbanken schlägt Sie fehl: Access, SqlCe,...
      Der Fehlschlag betrifft "nur" neu eingefügte Datensätze, deren Primkey-Spalte autoIncrementiert. Denn solche Primkeys werden im Dataset nur provisorisch generiert, und beim Abspeichern muss von der DB abgerufen werden, welchen AutoIncrement-Wert diese als den Endgültigen generiert hat.
      Also meine Empfehlung: Wenn man mittels Connector sich ein typisiertes Dataset aus einer Db generiert hat, schmeiße man im Dataset-Designer sofort alle TableAdapter wieder runter. Der Kram generiert zig-tausend Codezeilen, und funktioniert nichtmal!

      Schon vor längerem habich DbExtensions veröffentlich, die neben vielem anderen auch das Speicherungs-Problem ein für allemal lösen.
      Aber die sind zu umfangreich, um den Code noch im Einzeln nachvollziehen zu können.
      Daher stelle ich hier nochmal den "Lade-/Speicher-Kern" vor und erläutere ihn im Einzelnen. Im Download habe ichs sogar 2-fach ausgeführt, für SqlCe und für Access. Ich erläutere die SqlCe-Variante, die Access ist 98% identisch - tatsächlich habe ich sie mittels Textersetzung ("SqlCe" -> "OleDb") "programmiert".
      Die Access-Variante im Download enthält auch ein Form, wo das Failen des TableAdapterManagers nachvollzogen werden kann - Anweisungen, wie den Fehler reproduzieren, finden sich im Readme.

      Persistance-Klasse komplett

      VB.NET-Quellcode

      1. Imports System.Data.SqlServerCe
      2. Public Class SqlCePersistance
      3. Private _Con As SqlCeConnection
      4. Private _Adapters As Dictionary(Of DataTable, SqlCeDataAdapter)
      5. Private _RankedTables As List(Of DataTable)
      6. Private _Dts As DataSet
      7. Private _RequeryIdCommand As SqlCeCommand ' not neccessary, if in-/out-DbParameter-Support is present
      8. Public Sub New(connectionString As String, dts As DataSet)
      9. _Dts = dts
      10. _Con = New SqlCeConnection(connectionString)
      11. _RequeryIdCommand = New SqlCeCommand("SELECT @@IDENTITY", _Con)
      12. Dim sorter = New TopologicSort(dts.Tables.Cast(Of DataTable), True)
      13. _RankedTables = sorter.GetRankedTables
      14. _Adapters = New Dictionary(Of DataTable, SqlCeDataAdapter)
      15. For Each tb In _RankedTables
      16. Dim adp = New SqlCeDataAdapter("Select * from [" & tb.TableName & "]", _Con)
      17. Dim cmb = New SqlCeCommandBuilder(adp)
      18. If tb.PrimaryKey.Length = 1 AndAlso tb.PrimaryKey(0).AutoIncrement Then
      19. AddHandler adp.RowUpdated, AddressOf Table_RowUpdated ' not neccessary, if in-/out-DbParameter-Support is present
      20. End If
      21. _Adapters.Add(tb, adp)
      22. Next
      23. End Sub
      24. Public Sub FillAll()
      25. _Dts.Clear()
      26. _Con.Open()
      27. For Each tb In _RankedTables
      28. _Adapters(tb).Fill(tb)
      29. Next
      30. _Con.Close()
      31. End Sub
      32. ''' <summary> since SELECT and FROM-Clause are predefined by the datatable-structure sqlAfterFrom can contain any other valid Sql-clauses, especially WHERE. To prevent Sql-Injection-attacs it is strongly recommended to use '?' as Parameter-PlaceHolder and submit appropriate args</summary>
      33. Public Sub CustomFill(table As DataTable, sqlAfterFrom As String, ParamArray args() As Object)
      34. Dim childrenFinder = New TopologicSort(_RankedTables, False)
      35. For Each tb In childrenFinder.GetRankedTables(table) : tb.Clear() : Next
      36. Using adp = DirectCast(DirectCast(_Adapters(table), ICloneable).Clone, SqlCeDataAdapter)
      37. Dim cmd = adp.SelectCommand
      38. Dim splits = sqlAfterFrom.Split("?"c)
      39. For i = 0 To args.Length - 1
      40. Dim name = "@p" & i
      41. cmd.Parameters.AddWithValue(name, args(i))
      42. splits(i) = String.Concat(splits(i), " ", name, " ")
      43. Next
      44. cmd.CommandText &= " " & String.Concat(splits)
      45. _Con.Open()
      46. adp.Fill(table)
      47. _Con.Close()
      48. End Using
      49. End Sub
      50. Public Sub Save()
      51. _Con.Open()
      52. For Each tb In _RankedTables
      53. Dim rows = tb.Select("", "", DataViewRowState.Added Or DataViewRowState.ModifiedCurrent)
      54. _Adapters(tb).Update(rows) ' first send Inserts and Updates
      55. Next
      56. Dim skipSubOrderedRowsConfig = New _AcceptruleCascadeConfig(_Dts)
      57. For Each tb In _RankedTables
      58. Dim n = _Adapters(tb).Update(tb) ' send the remaining Deletes, skipping Cascade-Deleted-Rows, which will be deleted by Db itself
      59. Next
      60. skipSubOrderedRowsConfig.Restore()
      61. _Con.Close()
      62. End Sub
      63. ''' <summary> on Insert-Statements requery the database-generated primary-key. This is not neccessary on in-/out-DbParameters supporting Databases </summary>
      64. Private Sub Table_RowUpdated(sender As Object, e As SqlCeRowUpdatedEventArgs)
      65. If e.StatementType <> StatementType.Insert Then Return
      66. Dim primCol = e.Row.Table.PrimaryKey(0)
      67. Dim primVal = _RequeryIdCommand.ExecuteScalar
      68. e.Row(primCol) = Convert.ChangeType(primVal, primCol.DataType)
      69. End Sub
      70. Private Class _AcceptruleCascadeConfig : Inherits List(Of Tuple(Of ForeignKeyConstraint, AcceptRejectRule))
      71. ' AccepRule.Cascade removes subordered rows from datasets change-tracking, during Updating Database
      72. ' Required while Updating deleted rows, since the db deletes subordered rows already by itself.
      73. Public Sub New(ByVal dts As DataSet)
      74. For Each rl As DataRelation In dts.Relations
      75. Dim ck = rl.ChildKeyConstraint
      76. If ck Is Nothing Then Continue For
      77. MyBase.Add(Tuple.Create(ck, ck.AcceptRejectRule))
      78. ck.AcceptRejectRule = AcceptRejectRule.Cascade
      79. Next
      80. End Sub
      81. Public Sub Restore()
      82. For Each tpl In Me
      83. tpl.Item1.AcceptRejectRule = tpl.Item2
      84. Next
      85. End Sub
      86. End Class
      87. Private Class TopologicSort
      88. Private _RankedTables As List(Of DataTable)
      89. Private _Hsh As HashSet(Of DataTable)
      90. Private _ConsiderParentRelations As Boolean ' (otherwise consider ChildRelations)
      91. Private _Tables As IEnumerable(Of DataTable)
      92. ''' <summary> if considerParentRelations = False it consideres ChildRelations </summary>
      93. Public Sub New(tables As IEnumerable(Of DataTable), considerParentRelations As Boolean)
      94. _Tables = tables : _ConsiderParentRelations = considerParentRelations
      95. End Sub
      96. ''' <summary> returns all to root related tables (either parents or childs), including root as last element. If no root specified returns all tables in topologic order </summary>
      97. Public Function GetRankedTables(Optional root As DataTable = Nothing) As List(Of DataTable)
      98. _Hsh = New HashSet(Of DataTable)(_Tables)
      99. _RankedTables = New List(Of DataTable)(_Hsh.Count)
      100. If root Is Nothing Then
      101. While _Hsh.Count > 0 : RecurseRelatedTables(_Hsh.First) : End While
      102. Else
      103. RecurseRelatedTables(root)
      104. End If
      105. Return _RankedTables
      106. End Function
      107. ''' <summary> before adding an element to result-list, recursively add its precusers </summary>
      108. Private Sub RecurseRelatedTables(tb As DataTable)
      109. If Not _Hsh.Remove(tb) Then Return 'prevent run in Cycles
      110. If _ConsiderParentRelations Then
      111. For Each rl As DataRelation In tb.ParentRelations
      112. RecurseRelatedTables(rl.ParentTable)
      113. Next
      114. Else
      115. For Each rl As DataRelation In tb.ChildRelations
      116. RecurseRelatedTables(rl.ChildTable)
      117. Next
      118. End If
      119. _RankedTables.Add(tb)
      120. End Sub
      121. End Class
      122. End Class

      Konstruktor und Initialisierung

      VB.NET-Quellcode

      1. Private _Con As SqlCeConnection
      2. Private _Adapters As Dictionary(Of DataTable, SqlCeDataAdapter)
      3. Private _RankedTables As List(Of DataTable)
      4. Private _Dts As DataSet
      5. Private _RequeryIdCommand As SqlCeCommand ' not neccessary, if in-/out-DbParameter-Support is present
      6. Public Sub New(connectionString As String, dts As DataSet)
      7. _Dts = dts
      8. _Con = New SqlCeConnection(connectionString)
      9. _RequeryIdCommand = New SqlCeCommand("SELECT @@IDENTITY", _Con)
      10. Dim sorter = New TopologicSort(dts.Tables.Cast(Of DataTable), True)
      11. _RankedTables = sorter.GetRankedTables
      12. _Adapters = New Dictionary(Of DataTable, SqlCeDataAdapter)
      13. For Each tb In _RankedTables
      14. Dim adp = New SqlCeDataAdapter("Select * from [" & tb.TableName & "]", _Con)
      15. Dim cmb = New SqlCeCommandBuilder(adp)
      16. If tb.PrimaryKey.Length = 1 AndAlso tb.PrimaryKey(0).AutoIncrement Then
      17. AddHandler adp.RowUpdated, AddressOf Table_RowUpdated ' not neccessary, if in-/out-DbParameter-Support is present
      18. End If
      19. _Adapters.Add(tb, adp)
      20. Next
      21. End Sub

      Die Klasse hält 5 eiglich selbsterklärende Variablen (#5-9), die in Sub New() initialisiert werden.
      _RankedTables und _Adapters initialisieren besonders aufwändig:
      Für _RankedTables wird ein TopologicSort-Objekt erstellt, was die Tabellen des Datasets in widerspruchsfreie Reihenfolge bringt. Die Sortier-Bedingung ist: Keine untergeordnete DataTable darf vor einer ihr übergeordneten Tables aufgeführt sein.
      _Adapters initialisiert noch komplizierter (#17-25): Alle Tables werden durchgegangen, und zu jeder ein DataAdapter erstellt und konfiguriert - was im einzelnen bedeutet:
      • #19: der SelectCommand-Text und die Connection wird gleich beim Erstellen festgelegt.
      • #20: ein CommandBuilder-Objekt wird mit dem DataAdapter initialisiert. (das ist ein eigenartiges Design (nicht von mir): der CB wird nirgends sonst angesprochen - dennoch tut er seinen Dienst im DataAdapter)
      • #22: Liegt der PrimaryKey als AutoIncrement-Spalte vor, so muss der Row_Updated()-EventHandler abonniert sein, der bei Insert-Vorgängen den datenbankseitig generierte Key abruft und ins DataSet einpflegt.
      • #24: Den konfigurierten DataAdapter ins Dictionary packen - Schlüssel ist genau die Tabelle, für die er auch konfiguriert wurde.

      Sub FillAll() macht gleich regen Gebrauch der so vorbereiteten Variablen:

      VB.NET-Quellcode

      1. Public Sub FillAll()
      2. _Dts.Clear()
      3. _Con.Open()
      4. For Each tb In _RankedTables
      5. _Adapters(tb).Fill(tb)
      6. Next
      7. _Con.Close()
      8. End Sub
      Zwischen (selbsterklärender) Vor- und Nach-Bereitung werden die in Reihenfolge gebrachten Tabellen durchgenudelt, und mit jeder führt der zugeordnete DataAdapter sein Fill()-Command aus.

      Sub CustomFill():

      VB.NET-Quellcode

      1. ''' <summary> since SELECT and FROM-Clause are predefined by the datatable-structure sqlAfterFrom can contain any other valid Sql-clauses, especially WHERE. To prevent Sql-Injection-attacs it is strongly recommended to use '?' as Parameter-PlaceHolder and submit appropriate args</summary>
      2. Public Sub CustomFill(table As DataTable, sqlAfterFrom As String, ParamArray args() As Object)
      3. Dim childrenFinder = New TopologicSort(_RankedTables, False)
      4. For Each tb In childrenFinder.GetRankedTables(table) : tb.Clear() : Next
      5. Using adp = DirectCast(DirectCast(_Adapters(table), ICloneable).Clone, SqlCeDataAdapter)
      6. Dim cmd = adp.SelectCommand
      7. Dim splits = sqlAfterFrom.Split("?"c)
      8. For i = 0 To args.Length - 1
      9. Dim name = "@p" & i
      10. cmd.Parameters.AddWithValue(name, args(i))
      11. splits(i) = String.Concat(splits(i), " ", name, " ")
      12. Next
      13. cmd.CommandText &= " " & String.Concat(splits)
      14. _Con.Open()
      15. adp.Fill(table)
      16. _Con.Close()
      17. End Using
      18. End Sub

      CustomFill() bietet die Möglichkeit, mit frei definierbaren Sql-Klauseln die Menge der abgerufenen Daten einzuschränken. Dabei unterstützt CustomFill() die Verwendung von DbParametern, damit man seine Anwendung nicht Sql-Injction-Angriffen aussetzt.
      Parmeter-Platzhalter ist ?, und im ParamArray sind dann die Werte anzuliefern (Anwendungsbeipiel weiter unten).
      Die Methode bemüht zuerst nochmal die TopologicSort-Klasse, um aller ChildTables der table habhaft zu werden - die müssen nämlich alle geCleared werden, denn sonst würden die Verweise der Rows verwaisen, wenn ihre ParentTable gecleared wird.
      Die Methode klont dann den SelectCommand (#41), und der Parameters-Auflistung des Klons fügt sie die args hinzu(#44-48).
      Ausserdem werden im CommandText die ? ausgetauscht durch die Namen der Parameter, denn SqlCe-Sql versteht ? im CommandText nicht als Parameter-Platzhalter.

      Sub Save():

      VB.NET-Quellcode

      1. Public Sub Save()
      2. _Con.Open()
      3. For Each tb In _RankedTables
      4. Dim rows = tb.Select("", "", DataViewRowState.Added Or DataViewRowState.ModifiedCurrent)
      5. _Adapters(tb).Update(rows) ' first send Inserts and Updates
      6. Next
      7. Dim skipSubOrderedRowsConfig = New _AcceptruleCascadeConfig(_Dts)
      8. For Each tb In _RankedTables
      9. Dim n = _Adapters(tb).Update(tb) ' send the remaining Deletes, skipping Cascade-Deleted-Rows, which will be deleted by Db itself
      10. Next
      11. skipSubOrderedRowsConfig.Restore()
      12. _Con.Close()
      13. End Sub

      Wie auch beim Fill() werden die geordneten Tabellen durchlaufen. Aber geupdatet wird (zunächst mal) nur ein Auszug der DataRows, nämlich nur geänderte oder hinzugefügte (#59)
      Danach kommt meine _AcceptruleCascadeConfig-Klasse zum Einsatz, die in allen DataRelations die AcceptRules auf .Cascade stellt. Der Sinn davon ist in #79,#80 auch kommentiert, nämlich diese DataRelation-Konfiguration bewirkt beim Updaten, dass die ChildRows einer geupdateten Row vom Update ausgenommen werden. Und das ist bei Löschungen notwendig, performant und praktisch, denn die Löschweitergabe der Db löscht diese Datensätze auch ohne dass man sie ihr extra nochmal senden müsste.
      Also unter dieser Konfiguration die Tabellen alle nochmal updaten (#63-65) - betrifft jetzt eh nur noch die deleted Rows - alle anderen wurden ja bereits zuvor updatet.
      Jo, dann die AcceptRules restaurieren, Connection schließen und done - für alle Tabellen, jedweden typisierten Datasets :D

      Eventhandler Sub Table_RowUpdated():

      VB.NET-Quellcode

      1. ''' <summary> on Insert-Statements requery the database-generated primary-key. This is not neccessary on in-/out-DbParameters supporting Databases </summary>
      2. Private Sub Table_RowUpdated(sender As Object, e As SqlCeRowUpdatedEventArgs)
      3. If e.StatementType <> StatementType.Insert Then Return
      4. Dim primCol = e.Row.Table.PrimaryKey(0)
      5. Dim primVal = _RequeryIdCommand.ExecuteScalar
      6. e.Row(primCol) = Convert.ChangeType(primVal, primCol.DataType)
      7. End Sub
      Weder SqlCe noch Access unterstützen In-/Out-DbParameter, und deshalb ist das DataAdapter.RowUpdated-Event zu abonnieren, damit das DataSet bei Inserts mitbekommt, welchen Primkey die Db als für diesen Datensatz endgültig festgelegt hat.
      Eiglich auch selbsterklärend: zunächstmal gleich Abbruch, wenn das Event was anderes als einen Insert meldet (#72).
      Dann wird die PrimaryKey-Spalte der DataTable abgerufen, und dann von der Datenbank mittels des _RequeryIdCommand der generierte Primkey-Wert (#74 - bitte auch nochmal den CommandText nachgucken in #14 - er lautet: SELECT @@IDENTITY)
      Jo, dann wird der Wert an die richtige Spalte zugewiesen, und zwar konvertiert auch auf den richtigen Typ - das wars.

      Klasse _AcceptruleCascadeConfig:

      VB.NET-Quellcode

      1. Private Class _AcceptruleCascadeConfig : Inherits List(Of Tuple(Of ForeignKeyConstraint, AcceptRejectRule))
      2. ' AccepRule.Cascade removes subordered rows from datasets change-tracking, during Updating Database
      3. ' Required while Updating deleted rows, since the db deletes subordered rows already by itself.
      4. Public Sub New(ByVal dts As DataSet)
      5. For Each rl As DataRelation In dts.Relations
      6. Dim ck = rl.ChildKeyConstraint
      7. If ck Is Nothing Then Continue For
      8. MyBase.Add(Tuple.Create(ck, ck.AcceptRejectRule))
      9. ck.AcceptRejectRule = AcceptRejectRule.Cascade
      10. Next
      11. End Sub
      12. Public Sub Restore()
      13. For Each tpl In Me
      14. tpl.Item1.AcceptRejectRule = tpl.Item2
      15. Next
      16. End Sub
      17. End Class
      Für die temporäre Umkonfigurierung aller betreffenden DataRelations gibts eine eigene Klasse, die ihre Eingriffe sich merkt, um später den Orig-Zustand wieder herstellen zu können.
      Sub New() geht alle DataRelations durch, und wenn eine ChildKeyConstraint gegeben ist, speichert er zunächst die Relation zusammen mit dem AcceptRejectRule-Wert, dann ändert er letzteren - die Restore() - Methode restauriert dann alles wieder, wie's vorher war.

      TopologicSort-Klasse:

      VB.NET-Quellcode

      1. Private Class TopologicSort
      2. Private _RankedTables As List(Of DataTable)
      3. Private _Hsh As HashSet(Of DataTable)
      4. Private _ConsiderParentRelations As Boolean ' (otherwise consider ChildRelations)
      5. Private _Tables As IEnumerable(Of DataTable)
      6. ''' <summary> if considerParentRelations = False it consideres ChildRelations </summary>
      7. Public Sub New(tables As IEnumerable(Of DataTable), considerParentRelations As Boolean)
      8. _Tables = tables : _ConsiderParentRelations = considerParentRelations
      9. End Sub
      10. ''' <summary> returns all to root related tables (either parents or childs), including root as last element. If no root specified returns all tables in topologic order </summary>
      11. Public Function GetRankedTables(Optional root As DataTable = Nothing) As List(Of DataTable)
      12. _Hsh = New HashSet(Of DataTable)(_Tables)
      13. _RankedTables = New List(Of DataTable)(_Hsh.Count)
      14. If root Is Nothing Then
      15. While _Hsh.Count > 0 : RecurseRelatedTables(_Hsh.First) : End While
      16. Else
      17. RecurseRelatedTables(root)
      18. End If
      19. Return _RankedTables
      20. End Function
      21. ''' <summary> before adding an element to result-list, recursively add its precusers </summary>
      22. Private Sub RecurseRelatedTables(tb As DataTable)
      23. If Not _Hsh.Remove(tb) Then Return 'prevent run in Cycles
      24. If _ConsiderParentRelations Then
      25. For Each rl As DataRelation In tb.ParentRelations
      26. RecurseRelatedTables(rl.ParentTable)
      27. Next
      28. Else
      29. For Each rl As DataRelation In tb.ChildRelations
      30. RecurseRelatedTables(rl.ChildTable)
      31. Next
      32. End If
      33. _RankedTables.Add(tb)
      34. End Sub
      35. End Class
      Um dieses zu verstehen muss man Rekursion können, und auch wissen, wie HashSet(Of T) tickt.
      Zunächst, in Sub New() werden alle Tables ins HashSet gesteckt, und _ConsiderParentRelations sich gemerkt (#104).
      Letzteres bestimmt, ob TopologicSort die Reihenfolge an den ParentRelations der DataTables ausrichtet oder an den ChildRelations - beides ist möglich.
      Und GetRankedTables() (#108) bietet nochmal 2 Möglichkeien: Gibt man ein root an, so kriegt man alle dessen parent-Tables (bzw die childTables).
      Gibt man kein root an, so bekommt man alle Tables, und zwar in topologischer Sortierung.
      In der rekursiven Methode (#120) wird als erstes versucht, die angegebene Tabelle aus dem Hashset zu entfernen. Gelingt das nicht, war sie wohl bereits entfernt worden, und gibt garnix zu tun.
      Ansonsten aber alle Parent-/Child-Tables in die Rekursion voraus-schicken (#122-130), und erst danach(!!) der Ergebnisliste auch die table selbst adden.
      Ich finds verblüffend: Im Code steht direkt, dass vor jeder Table alle ihre Parent-Tables in die Rekursion gehen, und das ist exakt die SaveAll()-Vorraussetzung, die umzusetzen war: Keine ChildTable darf vor einer ihrer ParentTables drankommen!

      Anwendung

      VB.NET-Quellcode

      1. Imports SqlCeSample.SqlCeDts
      2. Public Class frmSqlCeSample
      3. Private _Persistance As SqlCePersistance
      4. Public Sub New()
      5. InitializeComponent()
      6. _Persistance = New SqlCePersistance("Data Source = |DataDirectory|\SqlCe_FW4.sdf", SqlCeDts)
      7. End Sub
      8. Private Sub Button_Click(sender As Object, e As EventArgs) Handles btFillAll.Click, btSave.Click, btFillBestellDetailCustom.Click
      9. Select Case True
      10. Case sender Is btFillBestellDetailCustom : FillSelectedBestellDetail()
      11. Case sender Is btFillAll : _Persistance.FillAll()
      12. Case sender Is btSave : _Persistance.Save()
      13. End Select
      14. System.Media.SystemSounds.Asterisk.Play()
      15. End Sub
      16. Private Sub FillSelectedBestellDetail()
      17. 'lädt nur die BestellDetails der im HierarchicalView angewählten Bestellung
      18. Dim rwBestellung = DirectCast(DirectCast(bsKundeBestellung.Current, DataRowView).Row, BestellungenRow)
      19. _Persistance.CustomFill(SqlCeDts.Bestelldetails, "Where BestellID=?", rwBestellung.BestellID)
      20. End Sub
      21. End Class
      Hier will ich nur zeigen, wie aufgeräumt ein Form aussieht, wenn man zum Persistieren allgemeingültige Methoden nutzen kann.
      Und wie man mit _Persistance.CustomFill() (#24) einen durch Where-Klausel gefilterten Daten-Auszug lädt (konkret: nur die Bestelldetails, die einer bestimmten Bestellung untergeordnet sind - selbsterklärend, oder?)

      Zusammenfassung
      Das hier gezeigte konfliktfreie Abspeicher-Prinzip ist anwendbar unter jedem Db-System, mit jedem typisierten Dataset, und es updated immer alle Änderungen in allen Tabellen, und zwar dank Änderungs-Verfolgung ohne Traffic-Overhead. Also egal, ob nur eine Telefonnummer geändert wurde, oder ob die Artikel eines ganzen Warenlagers aufgenommen wurden - .Save() aufrufen, und die Datenbank ist updated.
      Der Code ist auch noch recht entwicklungsfähig, insbesondere was differenziertere Befüllung angeht (Sub CustomFill()). Das ist halt der Vorzug dieses "kleinen" DatasetAdapters, dass man ihn vollständig verstehen kann, und folglich auch nach Sachlage erweitern.
      Umfangreichere Untertützung, auch verschiedener Db-Provider, bietet der DatasetAdapter der DbExtensions, vor allem auch im Zusammenspiel mit den dortigen Helpers-Bibliotheken (WinForms, General).

      Updates:
      28.8.2015: Im SqlCe-Projekt ist jetzt auch eine Persistance-Klasse für SqlServer drinne, also (etwas überarbeitet und ansonsten) dasselbe nochmal, nur ohne das explizite Abrufen der datenbankseitig generierten Primkeys (weil das hat SqlServer ja besser gelöst). Ein TestProjekt dazu gibts aber nicht.

      23.11.2015: Bugfix für Access (OleDbPersistance-Klasse) - Dem OleDbCommandBuilder muss man QuotePrefix und QuoteSuffix extra angeben, sonst quoted er die Datenbank-Namen nicht, und es können Namenskonflikte auftreten mit in Access reservierten Worten.

      17.10.2020: DbConcurrencyException-Problem gelöst, wenn ForeignKeys vom Typ String editiert wurden (siehe Post#2).

      2.10.2021: AllTogether2021
      • MysqlPersistance zugefügt
        Leider kann hierfür keine Beispiel-Anwendung geliefert werden, weil Mysql (im Gegensatz zu Access, Sqlserver, SqlserverCE, DatasetOnly) keine verzippbaren DatenbankDateien kennt.
      • DatasetOnly-Variante: Die Funktionalität der anderen Samples, mal ganz ohne DB dargestellt.
      • Zu jedem Projekt ein Readme.
      Dateien

      Dieser Beitrag wurde bereits 24 mal editiert, zuletzt von „ErfinderDesRades“ ()

      Gelegentlich designed man eine DataRelation unter Verwendung von String-Spalten.
      Das ist in der GUI-Entwicklung vielleicht ganz praktisch, weil man kann menschen-lesbare Werte anzeigen, ohne dazu erst den relationalen Verweis auflösen zu müssen.
      Entspricht aber nicht der reinen datenbänkerischen Lehre, nach der einer Spalte genau eine Aufgabe zukommt: Entweder Daten beinhalten oder verweisen - aber nicht beides.

      Und die Probleme folgen prompt (Beispiel): Wird bei einer String-Relation in der ParentTable der Wert geändert, so ändert sich auch der FK-Wert der ChildTable (UpdateRule.Cascade - muss ja).
      Wird das System dann abgespeichert, so tritt eine DbConcurrencyException (alias "ParalellitätsVerletzung") auf, wenn die ChildTable ihre Änderung abspeichern will. Weil diese Änderung hat die Db beim Speichern der ParentTable bereits eigenständig intern vorgenommen, um ihre Datenkonsistenz zu wahren.
      (Wir erinnern uns (oder auch nicht): Eine DbConcurrencyException tritt auf, wenn beim Abspeichern eines Datensatzes dieser in der DB nicht mehr vorgefunden wird bzw. geändert. In dem Falle geht der DataAdapter davon aus, dass mehrere Benutzer denselben Datensatz konkurrierend bearbeitet/gelöscht haben - was einen nicht lösbaren Konflikt darstellt.
      Der "Konflikt" ist hier in diesem Falle eigentlich ein "Missverständnis", weil die ChildRow-Änderung ging ja vom selben Benutzer aus. Aber das kann der DataAdapter beim Speichern ja nicht feststellen)

      Also hab ich eine Suche gebaut, die alle Modified Rows des Datasets untersucht, ob ihre Changes vielleicht nur ForeignKeys betreffen. Trifft das zu, so wird deren Change-Tracking mittels DataRow.AcceptChanges() zurückgesetzt:

      VB.NET-Quellcode

      1. ''' <summary> reset ChangeTracking of modified Datarows, which are not to send to the Database.
      2. ''' This affects changes at ForeignKeyColumns as well as Changes made undone by the user.</summary>
      3. <Extension()>
      4. Public Sub AcceptVirtualChanges(dts As DataSet)
      5. Dim changes = 0, virtualChanges = 0 ' counter for debug
      6. For Each tb As DataTable In dts.Tables
      7. Dim modifiedRows = tb.Select("", "", DataViewRowState.ModifiedCurrent)
      8. If modifiedRows.Length = 0 Then Continue For
      9. changes += modifiedRows.Length
      10. Dim fkRels = Aggregate rl In tb.ParentRelations.Cast(Of DataRelation)
      11. Where rl.ChildKeyConstraint IsNot Nothing Into ToList
      12. Dim fkColumns = Aggregate rl In fkRels Into SelectMany(rl.ChildColumns)
      13. Dim payloadColumns = tb.Columns.Cast(Of DataColumn).Where(Function(col) col.Expression = "").Except(fkColumns).ToList
      14. For Each rw In modifiedRows
      15. 'rw-ChangeTracking rücksetzen, wenn in allen Nicht-FK-Spalten ('payload') sich aktueller und Original-Wert gleichen
      16. 'und ausserdem für alle FK-Relationen sich Original- und Parent-Original-Wert gleichen.
      17. 'letzteres markiert einen 'VirtualChange': die Spalte wurde nicht explizit geändert, sondern von der ParentRow
      18. ' wurde die Änderung aufgrund DataRelation.UpdateRule.Cascade injiziert
      19. Dim IsNoChange As Func(Of DataColumn, Boolean) = Function(col) Object.Equals(rw(col), rw(col, DataRowVersion.Original))
      20. Dim IsVirtualChange As Func(Of DataRelation, Boolean) _
      21. = Function(rl)
      22. Dim childValues = rl.ChildColumns.Select(Function(col) rw(col, DataRowVersion.Original))
      23. Dim rwParent = rw.GetParentRow(rl)
      24. Dim parentValues = rl.ParentColumns.Select(Function(col) rwParent(col, DataRowVersion.Original))
      25. Return childValues.SequenceEqual(parentValues)
      26. End Function
      27. If payloadColumns.All(IsNoChange) AndAlso fkRels.All(IsVirtualChange) Then rw.AcceptChanges() : virtualChanges += 1
      28. Next
      29. Next
      30. Debug.WriteLine($"AcceptVirtualChanges(): changes={changes}, remainingChanges={changes - virtualChanges}")
      31. End Sub
      Die Geschichte ist ziemlich tricky, weil ForeignKeyColumns gesondert zu checken sind, nachdem die Prüfung der anderen Columns ("payload") keinen Change ergab.
      Bei FK-Columns wird nicht Original- und Aktueller Wert verglichen, sondern Original mit Parentrow.Original. Sind diese gleich, so ist die verwiesene ParentRow dieselbe wie zuvor - also kein Change.
      Dabei kann durchaus der Aktuelle Wert geändert sein - aber nur aufgrund von Aktualisierungsweitergabe eines ParentRow-Changes. Also kein Change der jeweils betrachteten ChildRow: ein 'VirtualChange' also.
      Weitere Herausforderung war, dass eine DataRelation auch mehrere DataColumns umfassen kann. So designed zwar kein normaler Mensch, aber egal.
      (Die Methode ist übrigens vergleichsweise langsam, und zwar proportional zur Menge der geänderten Rows. Aber das ist irrelevant, denn der folgende Abgleich mit der Datenbank ist um ein viiielfaches langsamer.)
      Jedenfalls in Folge werden diese Rows beim Abspeichern nicht an die DB gesendet - und die DbConcurrencyException bleibt uns erspart :D



      Ach - und ich hab die Architektur geändert. Es gibt jetzt eine PersistanceBase mit dem Löwenanteil an Funktionalität. Davon erben sowohl OleDbPersistance als auch SqlServerPersistance und brauchen deshalb nur sehr wenig Code, um die Spezifica des jeweiligen DbProviders korrekt zu bedienen.
      Über PersistanceBase sind nun beide Persistances mit AcceptVirtualChanges() ausgerüstet, und haben eine zusätzliche komfortable Befüll-Methode für einen häufigen Standard-Fall:
      FillChildTable(rwParent As DataRow, childTable As DataTable)
      Da muss man den nötigen Sql-Filter nun nicht mehr selber formulieren.

      Weiters ein CustomFill(), was mit Sql-Preamblen umgehen kann wie Select Distinct... oder Select Top....
      Das in post#1 besprochene Projekt ist aber nachwievor im Download enthalten

      Download-Zip in Post#1

      Dieser Beitrag wurde bereits 20 mal editiert, zuletzt von „ErfinderDesRades“ ()

      Update 2020 - 11

      Ich hab einen Bug der DbPersistanceBase.CustomFill()-Methode gefixt, und bei dem Anlass auch ein paar schnucklige Extensions eingeführt.
      Die CustomFill()-Methode dient der benutzerdefinierten Befüllung jeweils einer Datatable.
      Weil bei grossen Datenbeständen ists nicht immer geraten, sämtliche Daten abzurufen, wenn man nur die Daten zB eines Kunden braucht.
      An dieser Stelle benötigt man nun grundlegende Sql-Kenntnisse, nämlich einfache Select-Statements muss man beherrschen, mit Where-Condition und Inner Join.
      Ich nenne das "einfach", weil Sql-Select ist noch deutlich mächtiger, mit Left/Right Join, Order By, Group By, Having, Aliases, Common Table Expression, Cases, SubSelects und hastenichjesehn...
      In der Praxis ist schon dieses "einfache" Select nicht einfach, und wird immer un-einfacher, je mehr Tabellen man zusammen-joint.

      Wiedemauchsei. Ein Select-Command unseres "einfachen" Strickmusters besteht aus den 4 Abschnitten:
      1. Select-Abschnitt: nennt die abzurufenden Spalten
      2. From-Abschnitt: nennt die Tabelle
      3. (optional) Join-Abschnitt: nennt weitere Tabellen, und wie sie mit der ersten Tabelle verknüpft sind
      4. Where-Abschnitt: Bedingung(en), die die Datensätze erfüllen müssen
      Beispiel für die Befüllung der Bestelldetail-DataTable mit den BestellDetails aller Bestellungen des Monats November 1995:

      SQL-Abfrage

      1. Select `Bestelldetails`.*
      2. from Bestelldetails
      3. inner join Bestellungen on Bestelldetails.BestellID=Bestellungen.BestellID
      4. where Bestellungen.Bestelldatum > @p0 and Bestellungen.Bestelldatum < @p1
      Als Parameter @p0, @p1 codeseitig mitzugeben sind natürlich Start- und End-Datum des Zeitabschnitts, also #11-1-1995# und #12-1-1995#.

      Beim Befüllen einer DataTable ist man nun in Punkto Select- und From-Abschnitt komplett festgelegt: Man muss(!!) genau die Spalten abrufen, die in der DataTable vorhanden sind. Nicht mehr und nicht weniger, und auch nicht andere.
      Daher stellt meine CustomFill-Methode Select-und From-Abschnitt auch garnet zur Disposition, sondern generiert das selber: Select TableName.* from TableName
      Wir brauchen (und können) dem CustomFill also nur das halbe Select-Command übergeben, nämlich alles nach dem From-Abschnitt. So habe ich denn auch die CustomFill-Methoden-Signatur designed (beachtet bitte die sprechende Benamung):

      VB.NET-Quellcode

      1. Public Sub CustomFill(table As DataTable, sqlAfterFrom As String, ParamArray paramValues() As Object)

      AufrufBeispiel einfach (ohne Join):

      VB.NET-Quellcode

      1. Dts.Bestellungen.CustomFill("where Bestelldatum >? and Bestelldatum <? ", #11-1-1995#, #12-1-1995#)
      Das befüllt schonmal die 1995-11-Bestellungen, mittels des Where-Abschnitts und der ParamValues.

      Fehlen noch die BestellDetails der Bestellungen, also was haben die Kunden denn in ihren Bestellungen bestellt?
      Hier ist ein Sql-Join erforderlich, denn das Datum (hier: 1995-11) ist ja gar nicht vonne Bestelldetails abrufbar. Ein BestellDetail hat kein Datum, sondern es verweist auf "seine" Bestellung, und da ist das Datum.
      Also muss beim Abruf der BestellDetails obigem Where-Abschnitt ein Join-Abschnitt vorangestellt werden, der dahin verknüpft, wo das Datum abrufbar ist - dann tun derselbe Where-Abschnitt und dieselben ParamValues auch hier ihren Dienst:

      VB.NET-Quellcode

      1. Dts.Bestelldetails.CustomFill(
      2. "Bestelldetails inner join Bestellungen on Bestelldetails.BestellID=Bestellungen.BestellID " _
      3. & "where Bestellungen.Bestelldatum >? and Bestellungen.Bestelldatum <? ",
      4. #11-1-1995#, #12-1-1995#)
      Ein grausiger Bandwurm, aber so ist Sql nunmal :thumbdown:
      Dieses Aufruf-Beispiel führt dann genau zu dem Sql-Command im obigen Sql-Snippet.

      Ach, das schnucklige hab ich noch nicht erwähnt:
      Sieht man genau hin, erkennt man, dass die DBPersistance in diesen Aufrufen garnet auftaucht. Sondern die Bestelldetails-DataTable kann sich auf einmal selbst aus der Datenbank befüllen!
      Jo - Extensions eben (in Kombination mit Dataset.ExtendedProperties, wo ich die DbPersistance reingestopft habe :) ).

      (Update in post#1)

      Update 2021-10

      Ich hab eine MysqlPersistance zugefügt, und eine DatasetOnly-Variante.
      Das c#-Projekt rausgeworfen.
      Zu jedem Projekt ein Readme.

      Zur MysqlPersistance kann leider keine Beispiel-Anwendung geliefert werden, weil Mysql (im Gegensatz zu Access, Sqlserver, SqlserverCE, DatasetOnly) keine verzippbaren DatenbankDateien kennt.
      (Download in post#1)