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
Die
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
Konstruktor und Initialisierung
Die Klasse hält 5 eiglich selbsterklärende Variablen (#5-9), die in
Für
Zwischen (selbsterklärender) Vor- und Nach-Bereitung werden die in Reihenfolge gebrachten Tabellen durchgenudelt, und mit jeder führt der zugeordnete DataAdapter sein
Parmeter-Platzhalter ist
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
Ausserdem werden im CommandText die
Wie auch beim
Danach kommt meine
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
Eventhandler
Weder SqlCe noch Access unterstützen In-/Out-DbParameter, und deshalb ist das
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
Jo, dann wird der Wert an die richtige Spalte zugewiesen, und zwar konvertiert auch auf den richtigen Typ - das wars.
Klasse
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.
Um dieses zu verstehen muss man Rekursion können, und auch wissen, wie
Zunächst, in
Letzteres bestimmt, ob TopologicSort die Reihenfolge an den ParentRelations der DataTables ausrichtet oder an den ChildRelations - beides ist möglich.
Und
Gibt man kein
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
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
Anwendung
Hier will ich nur zeigen, wie aufgeräumt ein Form aussieht, wenn man zum Persistieren allgemeingültige Methoden nutzen kann.
Und wie man mit
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 -
Der Code ist auch noch recht entwicklungsfähig, insbesondere was differenziertere Befüllung angeht (
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 (
17.10.2020:
2.10.2021: AllTogether2021
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.
VB.NET-Quellcode
- Imports System.Data.SqlServerCe
- Public Class SqlCePersistance
- Private _Con As SqlCeConnection
- Private _Adapters As Dictionary(Of DataTable, SqlCeDataAdapter)
- Private _RankedTables As List(Of DataTable)
- Private _Dts As DataSet
- Private _RequeryIdCommand As SqlCeCommand ' not neccessary, if in-/out-DbParameter-Support is present
- Public Sub New(connectionString As String, dts As DataSet)
- _Dts = dts
- _Con = New SqlCeConnection(connectionString)
- _RequeryIdCommand = New SqlCeCommand("SELECT @@IDENTITY", _Con)
- Dim sorter = New TopologicSort(dts.Tables.Cast(Of DataTable), True)
- _RankedTables = sorter.GetRankedTables
- _Adapters = New Dictionary(Of DataTable, SqlCeDataAdapter)
- For Each tb In _RankedTables
- Dim adp = New SqlCeDataAdapter("Select * from [" & tb.TableName & "]", _Con)
- Dim cmb = New SqlCeCommandBuilder(adp)
- If tb.PrimaryKey.Length = 1 AndAlso tb.PrimaryKey(0).AutoIncrement Then
- AddHandler adp.RowUpdated, AddressOf Table_RowUpdated ' not neccessary, if in-/out-DbParameter-Support is present
- End If
- _Adapters.Add(tb, adp)
- Next
- End Sub
- Public Sub FillAll()
- _Dts.Clear()
- _Con.Open()
- For Each tb In _RankedTables
- _Adapters(tb).Fill(tb)
- Next
- _Con.Close()
- End Sub
- ''' <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>
- Public Sub CustomFill(table As DataTable, sqlAfterFrom As String, ParamArray args() As Object)
- Dim childrenFinder = New TopologicSort(_RankedTables, False)
- For Each tb In childrenFinder.GetRankedTables(table) : tb.Clear() : Next
- Using adp = DirectCast(DirectCast(_Adapters(table), ICloneable).Clone, SqlCeDataAdapter)
- Dim cmd = adp.SelectCommand
- Dim splits = sqlAfterFrom.Split("?"c)
- For i = 0 To args.Length - 1
- Dim name = "@p" & i
- cmd.Parameters.AddWithValue(name, args(i))
- splits(i) = String.Concat(splits(i), " ", name, " ")
- Next
- cmd.CommandText &= " " & String.Concat(splits)
- _Con.Open()
- adp.Fill(table)
- _Con.Close()
- End Using
- End Sub
- Public Sub Save()
- _Con.Open()
- For Each tb In _RankedTables
- Dim rows = tb.Select("", "", DataViewRowState.Added Or DataViewRowState.ModifiedCurrent)
- _Adapters(tb).Update(rows) ' first send Inserts and Updates
- Next
- Dim skipSubOrderedRowsConfig = New _AcceptruleCascadeConfig(_Dts)
- For Each tb In _RankedTables
- Dim n = _Adapters(tb).Update(tb) ' send the remaining Deletes, skipping Cascade-Deleted-Rows, which will be deleted by Db itself
- Next
- skipSubOrderedRowsConfig.Restore()
- _Con.Close()
- End Sub
- ''' <summary> on Insert-Statements requery the database-generated primary-key. This is not neccessary on in-/out-DbParameters supporting Databases </summary>
- Private Sub Table_RowUpdated(sender As Object, e As SqlCeRowUpdatedEventArgs)
- If e.StatementType <> StatementType.Insert Then Return
- Dim primCol = e.Row.Table.PrimaryKey(0)
- Dim primVal = _RequeryIdCommand.ExecuteScalar
- e.Row(primCol) = Convert.ChangeType(primVal, primCol.DataType)
- End Sub
- Private Class _AcceptruleCascadeConfig : Inherits List(Of Tuple(Of ForeignKeyConstraint, AcceptRejectRule))
- ' AccepRule.Cascade removes subordered rows from datasets change-tracking, during Updating Database
- ' Required while Updating deleted rows, since the db deletes subordered rows already by itself.
- Public Sub New(ByVal dts As DataSet)
- For Each rl As DataRelation In dts.Relations
- Dim ck = rl.ChildKeyConstraint
- If ck Is Nothing Then Continue For
- MyBase.Add(Tuple.Create(ck, ck.AcceptRejectRule))
- ck.AcceptRejectRule = AcceptRejectRule.Cascade
- Next
- End Sub
- Public Sub Restore()
- For Each tpl In Me
- tpl.Item1.AcceptRejectRule = tpl.Item2
- Next
- End Sub
- End Class
- Private Class TopologicSort
- Private _RankedTables As List(Of DataTable)
- Private _Hsh As HashSet(Of DataTable)
- Private _ConsiderParentRelations As Boolean ' (otherwise consider ChildRelations)
- Private _Tables As IEnumerable(Of DataTable)
- ''' <summary> if considerParentRelations = False it consideres ChildRelations </summary>
- Public Sub New(tables As IEnumerable(Of DataTable), considerParentRelations As Boolean)
- _Tables = tables : _ConsiderParentRelations = considerParentRelations
- End Sub
- ''' <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>
- Public Function GetRankedTables(Optional root As DataTable = Nothing) As List(Of DataTable)
- _Hsh = New HashSet(Of DataTable)(_Tables)
- _RankedTables = New List(Of DataTable)(_Hsh.Count)
- If root Is Nothing Then
- While _Hsh.Count > 0 : RecurseRelatedTables(_Hsh.First) : End While
- Else
- RecurseRelatedTables(root)
- End If
- Return _RankedTables
- End Function
- ''' <summary> before adding an element to result-list, recursively add its precusers </summary>
- Private Sub RecurseRelatedTables(tb As DataTable)
- If Not _Hsh.Remove(tb) Then Return 'prevent run in Cycles
- If _ConsiderParentRelations Then
- For Each rl As DataRelation In tb.ParentRelations
- RecurseRelatedTables(rl.ParentTable)
- Next
- Else
- For Each rl As DataRelation In tb.ChildRelations
- RecurseRelatedTables(rl.ChildTable)
- Next
- End If
- _RankedTables.Add(tb)
- End Sub
- End Class
- End Class
Konstruktor und Initialisierung
VB.NET-Quellcode
- Private _Con As SqlCeConnection
- Private _Adapters As Dictionary(Of DataTable, SqlCeDataAdapter)
- Private _RankedTables As List(Of DataTable)
- Private _Dts As DataSet
- Private _RequeryIdCommand As SqlCeCommand ' not neccessary, if in-/out-DbParameter-Support is present
- Public Sub New(connectionString As String, dts As DataSet)
- _Dts = dts
- _Con = New SqlCeConnection(connectionString)
- _RequeryIdCommand = New SqlCeCommand("SELECT @@IDENTITY", _Con)
- Dim sorter = New TopologicSort(dts.Tables.Cast(Of DataTable), True)
- _RankedTables = sorter.GetRankedTables
- _Adapters = New Dictionary(Of DataTable, SqlCeDataAdapter)
- For Each tb In _RankedTables
- Dim adp = New SqlCeDataAdapter("Select * from [" & tb.TableName & "]", _Con)
- Dim cmb = New SqlCeCommandBuilder(adp)
- If tb.PrimaryKey.Length = 1 AndAlso tb.PrimaryKey(0).AutoIncrement Then
- AddHandler adp.RowUpdated, AddressOf Table_RowUpdated ' not neccessary, if in-/out-DbParameter-Support is present
- End If
- _Adapters.Add(tb, adp)
- Next
- 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 dieConnection
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:Fill()
-Command aus.Sub CustomFill()
:VB.NET-Quellcode
- ''' <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>
- Public Sub CustomFill(table As DataTable, sqlAfterFrom As String, ParamArray args() As Object)
- Dim childrenFinder = New TopologicSort(_RankedTables, False)
- For Each tb In childrenFinder.GetRankedTables(table) : tb.Clear() : Next
- Using adp = DirectCast(DirectCast(_Adapters(table), ICloneable).Clone, SqlCeDataAdapter)
- Dim cmd = adp.SelectCommand
- Dim splits = sqlAfterFrom.Split("?"c)
- For i = 0 To args.Length - 1
- Dim name = "@p" & i
- cmd.Parameters.AddWithValue(name, args(i))
- splits(i) = String.Concat(splits(i), " ", name, " ")
- Next
- cmd.CommandText &= " " & String.Concat(splits)
- _Con.Open()
- adp.Fill(table)
- _Con.Close()
- End Using
- 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
- Public Sub Save()
- _Con.Open()
- For Each tb In _RankedTables
- Dim rows = tb.Select("", "", DataViewRowState.Added Or DataViewRowState.ModifiedCurrent)
- _Adapters(tb).Update(rows) ' first send Inserts and Updates
- Next
- Dim skipSubOrderedRowsConfig = New _AcceptruleCascadeConfig(_Dts)
- For Each tb In _RankedTables
- Dim n = _Adapters(tb).Update(tb) ' send the remaining Deletes, skipping Cascade-Deleted-Rows, which will be deleted by Db itself
- Next
- skipSubOrderedRowsConfig.Restore()
- _Con.Close()
- 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

Eventhandler
Sub Table_RowUpdated()
:VB.NET-Quellcode
- ''' <summary> on Insert-Statements requery the database-generated primary-key. This is not neccessary on in-/out-DbParameters supporting Databases </summary>
- Private Sub Table_RowUpdated(sender As Object, e As SqlCeRowUpdatedEventArgs)
- If e.StatementType <> StatementType.Insert Then Return
- Dim primCol = e.Row.Table.PrimaryKey(0)
- Dim primVal = _RequeryIdCommand.ExecuteScalar
- e.Row(primCol) = Convert.ChangeType(primVal, primCol.DataType)
- End Sub
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
- Private Class _AcceptruleCascadeConfig : Inherits List(Of Tuple(Of ForeignKeyConstraint, AcceptRejectRule))
- ' AccepRule.Cascade removes subordered rows from datasets change-tracking, during Updating Database
- ' Required while Updating deleted rows, since the db deletes subordered rows already by itself.
- Public Sub New(ByVal dts As DataSet)
- For Each rl As DataRelation In dts.Relations
- Dim ck = rl.ChildKeyConstraint
- If ck Is Nothing Then Continue For
- MyBase.Add(Tuple.Create(ck, ck.AcceptRejectRule))
- ck.AcceptRejectRule = AcceptRejectRule.Cascade
- Next
- End Sub
- Public Sub Restore()
- For Each tpl In Me
- tpl.Item1.AcceptRejectRule = tpl.Item2
- Next
- End Sub
- End Class
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
- Private Class TopologicSort
- Private _RankedTables As List(Of DataTable)
- Private _Hsh As HashSet(Of DataTable)
- Private _ConsiderParentRelations As Boolean ' (otherwise consider ChildRelations)
- Private _Tables As IEnumerable(Of DataTable)
- ''' <summary> if considerParentRelations = False it consideres ChildRelations </summary>
- Public Sub New(tables As IEnumerable(Of DataTable), considerParentRelations As Boolean)
- _Tables = tables : _ConsiderParentRelations = considerParentRelations
- End Sub
- ''' <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>
- Public Function GetRankedTables(Optional root As DataTable = Nothing) As List(Of DataTable)
- _Hsh = New HashSet(Of DataTable)(_Tables)
- _RankedTables = New List(Of DataTable)(_Hsh.Count)
- If root Is Nothing Then
- While _Hsh.Count > 0 : RecurseRelatedTables(_Hsh.First) : End While
- Else
- RecurseRelatedTables(root)
- End If
- Return _RankedTables
- End Function
- ''' <summary> before adding an element to result-list, recursively add its precusers </summary>
- Private Sub RecurseRelatedTables(tb As DataTable)
- If Not _Hsh.Remove(tb) Then Return 'prevent run in Cycles
- If _ConsiderParentRelations Then
- For Each rl As DataRelation In tb.ParentRelations
- RecurseRelatedTables(rl.ParentTable)
- Next
- Else
- For Each rl As DataRelation In tb.ChildRelations
- RecurseRelatedTables(rl.ChildTable)
- Next
- End If
- _RankedTables.Add(tb)
- End Sub
- End Class
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
- Imports SqlCeSample.SqlCeDts
- Public Class frmSqlCeSample
- Private _Persistance As SqlCePersistance
- Public Sub New()
- InitializeComponent()
- _Persistance = New SqlCePersistance("Data Source = |DataDirectory|\SqlCe_FW4.sdf", SqlCeDts)
- End Sub
- Private Sub Button_Click(sender As Object, e As EventArgs) Handles btFillAll.Click, btSave.Click, btFillBestellDetailCustom.Click
- Select Case True
- Case sender Is btFillBestellDetailCustom : FillSelectedBestellDetail()
- Case sender Is btFillAll : _Persistance.FillAll()
- Case sender Is btSave : _Persistance.Save()
- End Select
- System.Media.SystemSounds.Asterisk.Play()
- End Sub
- Private Sub FillSelectedBestellDetail()
- 'lädt nur die BestellDetails der im HierarchicalView angewählten Bestellung
- Dim rwBestellung = DirectCast(DirectCast(bsKundeBestellung.Current, DataRowView).Row, BestellungenRow)
- _Persistance.CustomFill(SqlCeDts.Bestelldetails, "Where BestellID=?", rwBestellung.BestellID)
- End Sub
- End Class
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.
Dieser Beitrag wurde bereits 24 mal editiert, zuletzt von „ErfinderDesRades“ ()