Dataset->Db

    • VB.NET
    • .NET 4.0

    Es gibt 1 Antwort in diesem Thema. Der letzte Beitrag () ist von ErfinderDesRades.

      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).

      Zusatz-Info vom 18.11.15:
      @DahProgrammah hat für MySql einen Bugfix recherchiert, mit dem man auch MySql - DataAdapter so konfigurieren kann, dass ein explizites NachSchiessen eines Selects nicht mehr erforderlich ist: DataSet über TableAdapterManager in DB, wie neue DB-Schlüssel einlesen?
      Auf der Basis könnte man jetzt auch für MySql einen optimierte DbPersistance-Klasse schreiben, aber bin ich grad zu faul für, zumal ich kein MySql installiert hab.
      Dateien

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

      Neu

      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> resetted das ChangeTracking aller Modified DataRows, deren Changes nicht an die DB zu senden sind.
      2. ''' Betrifft Änderungen an ForeignKeyColumns und Änderungen, die wieder zurückgeändert wurden auf den Original-Stand</summary>
      3. <Extension()>
      4. Public Sub AcceptVirtualChanges(dts As DataSet)
      5. For Each tb As DataTable In dts.Tables
      6. Dim modifiedRows = tb.Select("", "", DataViewRowState.ModifiedCurrent)
      7. If modifiedRows.Length = 0 Then Continue For
      8. Dim fkColumns = Aggregate rl In tb.ParentRelations.Cast(Of DataRelation)
      9. Where rl.ChildKeyConstraint IsNot Nothing Into SelectMany(rl.ChildColumns)
      10. Dim noFkColumns = tb.Columns.Cast(Of DataColumn).Where(Function(col) col.Expression = "").Except(fkColumns).ToList
      11. For Each rw In modifiedRows
      12. 'rw-ChangeTracking rücksetzen, wenn für alle Nicht-FK-Spalten der aktuelle Wert mit dem Original-Wert übereinstimmt
      13. If noFkColumns.All(Function(col) Object.Equals(rw(col), rw(col, DataRowVersion.Original))) Then rw.AcceptChanges()
      14. Next
      15. Next
      16. End Sub
      In Folge werden diese Rows beim Abspeichern nicht an die DB gesendet - und die DbConcurrencyException bleibt uns erspart :D

      Download-Zip in Post#1

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