SQLAdapter und DataTable bei Update/Insert neue ID

  • VB.NET
  • .NET (FX) 4.5–4.8

Es gibt 8 Antworten in diesem Thema. Der letzte Beitrag () ist von Schwakowiak.

    SQLAdapter und DataTable bei Update/Insert neue ID

    Hallo liebe Leute,
    ich hoffe ihr könnt mir helfen :)

    Zur Zeit arbeite ich an einem kleinen Testprogramm indem ich Datenbankeinträge lesen, schreiben und einfügen möchte.
    Beim laden der WPF-Form wird das vorhandene DataGrid mit den Datenbankeinträgen gefüllt.
    Hier der XAML Code:

    XML-Quellcode

    1. <DataGrid x:Name="mitarbeiterGrid" ItemsSource="{Binding}" AutoGenerateColumns="False" Width=" Auto" >
    2. <DataGrid.Columns>
    3. <DataGridTextColumn IsReadOnly="True" Header="ID" Width="50" Binding="{Binding Path=Id}"> </DataGridTextColumn>
    4. <DataGridTextColumn Header="Vorname" Width="125" Binding="{Binding Path=Vorname}" ></DataGridTextColumn>
    5. <DataGridTextColumn Header="Nachname" Width="125" Binding="{Binding Path=Nachname}"></DataGridTextColumn>
    6. </DataGrid.Columns>
    7. </DataGrid>


    Member Variablen der Klasse "Mitarbeiter"

    VB.NET-Quellcode

    1. Dim da As OleDb.OleDbDataAdapter
    2. Dim dt As New DataTable()
    3. 'Connection String aus der app.config auslesen. So lässt sich das individuell anpassen
    4. Dim _connectionString As String = ConfigurationManager.ConnectionStrings("database").ConnectionString
    5. 'Verbindungsaufbau
    6. Dim _sqlConnection As New SqlConnection(_connectionString)
    7. 'SQL-Befehel
    8. Dim _queryCMD = "SELECT * FROM Mitarbeiter"
    9. Dim sqlAdapter As SqlDataAdapter = New SqlDataAdapter(_queryCMD, _sqlConnection)

    Den Connectionstring ziehe ich aus der App.Config

    Konstruktore der Form:

    VB.NET-Quellcode

    1. Public Sub New()
    2. ' Dieser Aufruf ist für den Designer erforderlich.
    3. InitializeComponent()
    4. _sqlConnection.Open()
    5. 'SqlAdapter fill das DataTable
    6. sqlAdapter.Fill(dt)
    7. 'Der dataGridView die Tabelle zuweisen
    8. mitarbeiterGrid.DataContext = dt
    9. 'Verbindung schließen
    10. _sqlConnection.Close()
    11. End Sub

    Bis dahin funktioniert auch alles top:-)

    Nun soll der User neue Zeilen einfügen löschen und editieren können.
    Wenn das Event Window_Closing eintrifft, updatet er mir die Tabelle - das funktioniert auch (wenn ich bestehende Datensätze bearbeite z.B. Vorname)

    VB.NET-Quellcode

    1. Private Sub Window_Closing(sender As Object, e As ComponentModel.CancelEventArgs)
    2. 'Update Command
    3. 'Update Commnand mit Variablen bestücken
    4. Dim upd As String = "Update Mitarbeiter SET Vorname = @vorname, Nachname = @nachname WHERE id = @id"
    5. 'SqlCommand generieren
    6. Dim cmd As New SqlCommand(upd, _sqlConnection)
    7. 'Parameter hinzufügen, vor und nachname
    8. cmd.Parameters.Add("@vorname", SqlDbType.VarChar, 25, "vorname")
    9. cmd.Parameters.Add("@nachname", SqlDbType.VarChar, 25, "nachname")
    10. 'Id bleibt im Originalen ja immer gleich. Hier wird nur verglichen
    11. Dim parm As SqlParameter = cmd.Parameters.Add("@id", SqlDbType.Int, 4, "id")
    12. '... Original
    13. parm.SourceVersion = DataRowVersion.Original
    14. 'dem SqlDapter den ganzen Befehl zuweisen
    15. sqlAdapter.UpdateCommand = cmd
    16. 'Update durchführen
    17. sqlAdapter.Update(dt)
    18. _sqlConnection.Close()
    19. End Sub


    Nun da ist auch das Problem. Habe ich neue Zeilen oder lösche ich welche - wirft er mir eine exception. Es wird ja auch kein DELETE oder INSERT Befehl ausgeführt und wie unterscheide ich die Fälle?

    Mit einem Button versuche ich eine neue Zeile eine ID zu zuweisen.

    VB.NET-Quellcode

    1. Private Sub Button_Click(sender As Object, e As RoutedEventArgs)
    2. Dim currentID As String = ""
    3. For Each row As DataRow In dt.Rows
    4. For Each column As DataColumn In dt.Columns
    5. If Not (row.IsNull(column)) Then
    6. currentID = row(column)
    7. 'Ab hier wissen wir ob es ne id gibt oder nicht
    8. Else
    9. 'Autoincrement wäre hier schön
    10. 'row(column) = currentID + 1
    11. MessageBox.Show(currentID + 1)
    12. End If
    13. Next
    14. Next
    15. End Sub


    Hoffe ihr könnt mir helfen :)
    ich würd empfehlen, deinen ganzen DB-Ansatz in die Tonne zu treten.
    Lege dir ein typisiertes Dataset an, befülle es, und binde dein Xaml daran.
    Erstelle nicht bei jedem Befüllen eine neue DataTable, denn sonst ist ja logisch, dass du nix zurückspeichern kannst, wenn ständig deine Datenklassen ausgetauscht werden.

    Wie man ein typDataset am einfachsten befüllt und auch wieder abspeichert, ist hier gezeigt: Dataset->Db
    Der Code ist nicht ganz einfach, aber dafür sind sämtliche mit Befüllung und Rückspeicherung zusammenhängende Probleme damit abgehandelt.
    Und du musst ja nix selbst coden, sondern kannst den Code einfach einbinden in dein Projekt.

    Einzige Vorraussetzung: Der Klasse DatasetPersistance muss ein typisiertes Dataset übergeben werden, ich hoffe, du kriegst das hin, ein solches zu creiern.
    Denn sonst weiß DatasetPersistance nicht die Struktur der Tabellen, und kann keine geeigneten DataAdapter konfigurieren.

    Edit: Ups, du musst doch selbst coden! ich hab ja nur DatasetPersistances für SqlCe und Access dort beigelegt, für SqlServer musst du alle Auftreten von "SqlCe" austauschen durch "Sql" - damit es SqlServer unterstützt.
    Ausserdem ist der Kram mit RequeryID zu löschen, denn SqlServer handelt Insert-Commands intelligenter.

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

    Hallo Erfinder,
    nun vielen Dank für deine Antwort und deine Referenz.
    Deine Klasse habe ich auch eingebunden und SqlCe durch SQL ersetzt. Fehler gibt es soweit keine.

    Du sprichst von einem typisierten Dataset, aber wie genau setzte ich das um? Ich habe die Wahl zwischen DataTable und TableAdapter - wo genau liegt der unterschied? Ich denke ich sollte die Elemente stricken wie meine SQL-Datenbank? (Sind letzlich Vorname, Nachname und eine ID mit AutoInc.)



    ---
    Weitere Frage:
    Was meinst du mit RequeryID ? Für mich ist die ganze Klasse etwas komplex - bin sicherlich nicht so erfahren wie du :-).
    In meinem XAML hab ich ich das DataGrid - am liebsten würde ich es ohne Buttons nutzen.
    In deinem "Anwendungs" Beispiel verwendest du ja die Button Click events und fängst sie mit einem Case ab.

    Danke dir!
    Ich bin unsicher, was dein Bild zeigt.
    Weil man kann mit Menu-Daten-Datenquelle_hinzufügen sich ein typisiertes Dataset generieren lassen, das hat dann Tabellen, die so 2-geteilt sind wie deine obere Tabelle im Bildle.

    Daher Frage: Ist dieses typDataset mit Menu-Daten-Datenquelle_hinzufügen generiert, oder hast du diese (bescheutert benannten) Dataset-Tabellen händisch hingemacht? Zumindest "DataTable1" ist offensichtlich händisch erzeugt worden, bei "MitarbeiterTableAdapter" bin ich nicht sicher, weil diese 2-teilige Darstellung entsteht normalerweise nur, wenn man mit Menu-Daten-Datenquelle_hinzufügen sich ein typisiertes Dataset generiert.


    Daher Frage: Ist dieses typDataset mit Menu-Daten-Datenquelle_hinzufügen generiert?
    Ich habe beide Objekte händisch erstellt und nicht über Menu -> Datenquelle hinzufügen. Sry für den Namen, ich wollte nur etwas testen.
    Nun habe ich über Datenquelle->Hinzufügen->DataSet->SQL-Datenbank->Meine SQL-Tabelle ausgewählt und hinzugefügt.

    Muss ich das DataSet als neues Objekt instaziieren und dann das XAML DataGrid binden?

    VB.NET-Quellcode

    1. Dim ds As New workWearDS.MitarbeiterDataTable
    2. Dim _connectionString As String = ConfigurationManager.ConnectionStrings("database").ConnectionString
    3. _Persistance = New SqlPersistance(_connectionString, ds.DataSet)
    4. mitarbeiterGrid.DataContext = ds.DataSet.Tables(0)



    Hier noch ein Bild der Datenquelle:

    Gut, dann kannst du nun das generierte Dataset nacharbeiten, nämlich sämtliche TableAdapter runterschmeissen. Also die Tabellen behalten, die Adapter runterschmeissen. Die Adapter sind im Dataset-Designer die an die Tabellen unten "angeklebten" Rechtecke - man kann sie gesondert selektieren, und dann Entf drücken.

    Zu deim Code kann ich nichts sagen - das sind ja nur verlorene Zeilen im irgendwo.
    Bitte poste ganze Methoden.

    Und - ja, man muss ein workWearDS instanzieren, und zwar genau ein einziges Mal, nicht öfter, aber auch nicht weniger.
    Daher ist wichtig, zu wissen, in welcher Methode deine verlorenen Zeilen stehen - es muss eine Methode sein, die nur ein einziges mal aufgerufen wird.
    Ebenso das Persistance-Dingens - das muss auch genau ein einziges Mal, nicht öfter, aber auch nicht weniger, instanziert werden.
    Und dann im Xaml daran binden - nicht im Code (zeile#6 ist also zu löschen, egal wo die steht)

    Übrigens rede nicht mehr von Dataset, sondern du arbeitest mit einem workWearDS, das ist etwas anderes.
    Nämlich eine generierte Klasse, die von Dataset erbt.

    Und erstell das workWearDS - mach es nicht wie in zeile#1, wo kein workWearDS erstellt wird, sonder eine workWearDS.MitarbeiterDataTable.

    eine workWearDS.MitarbeiterDataTable ist was anderes als ein workWearDS, nämlich workWearDS.MitarbeiterDataTable ist eine im workWearDS befindliche typisierte DataTable, eine MitarbeiterDataTable, um genau zu sein, (und Genauigkeit ist wichtig.)

    Wenn du ein workWearDS erstellt hast, brauchst darfst du nicht extra eine MitarbeiterDataTable erstellen, denn eine MitarbeiterDataTable ist im workWearDS ja bereits enthalten.

    Du kannst auch mal die modifizierte Persistance-Klasse posten, ob du die Anpassung an SqlServer richtig hingekriegt hast, also das mit der RequeryId korrekt entfernt hast.

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

    Gut, dann kannst du nun das generierte Dataset nacharbeiten, nämlich sämtliche TableAdapter runterschmeissen. Also die Tabellen behalten, die Adapter runterschmeissen. Die Adapter sind im Dataset-Designer die an die Tabellen unten "angeklebten" Rechtecke - man kann sie gesondert selektieren, und dann Entf drücken.


    Hier ein Bild des DataSet Designers:



    Die instaziierung des workWearDS() hab ich angepasst und auch die Persistance Klasse wird einmal instaziiert und zwar beim erstellen der Form
    Für den Anfang würde ich lediglich das DataGrid füllen (XAML Code etwas weiter unten)
    Hier der einfache Code:

    VB.NET-Quellcode

    1. Imports System.Configuration
    2. Imports ProjWorkWear.workWearDS
    3. Public Class testquery
    4. Private _Persistance As SqlPersistance
    5. Public Sub New()
    6. InitializeComponent()
    7. Dim _connectionString As String = ConfigurationManager.ConnectionStrings("database").ConnectionString
    8. Dim ds As New workWearDS()
    9. _Persistance = New SqlPersistance(_connectionString, ds.Mitarbeiter.DataSet)
    10. End Sub
    11. End Class


    Allerdings habe ich nicht ganz verstanden was du mit RequireID meinst. Der Code der Persistance Klasse:

    VB.NET-Quellcode

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




    XAML:

    XML-Quellcode

    1. <Window.Resources>
    2. <local:workWearDS x:Key="WorkWearDS"/>
    3. <CollectionViewSource x:Key="MitarbeiterViewSource" Source="{Binding Mitarbeiter, Source={StaticResource WorkWearDS}}"/>
    4. </Window.Resources>
    5. <Grid DataContext="{StaticResource MitarbeiterViewSource}">
    6. <DataGrid x:Name="MitarbeiterDataGrid" AutoGenerateColumns="False" EnableRowVirtualization="True" ItemsSource="{Binding }" RowDetailsVisibilityMode="VisibleWhenSelected">
    7. <DataGrid.Columns>
    8. <DataGridTextColumn x:Name="IdColumn" Binding="{Binding Id}" Header="Id" IsReadOnly="True" Width="SizeToHeader"/>
    9. <DataGridTextColumn x:Name="VornameColumn" Binding="{Binding Vorname}" Header="Vorname" Width="SizeToHeader"/>
    10. <DataGridTextColumn x:Name="NachnameColumn" Binding="{Binding Nachname}" Header="Nachname" Width="SizeToHeader"/>
    11. </DataGrid.Columns>
    12. </DataGrid>
    13. </Grid>
    14. </Window>


    Durch deinen Tipp der Genauigkeit mit dem DataSet, erhalte ich beim starten keine Fehler mehr. Allerdings auch keine Daten :)

    -----



    So, ich habe vergessen zu Fillen und saven ;(
    Wenn ich neue Datenzeilen einfüge, speichert er mir sie auch sogar mit AutoIncrement +1. Laut SQL Management Studio sind die Zeilen auch da.
    Allerdings funktioniert Updaten und löschen eines bestehenden Datensatzes nicht.
    Meldung:
    Dynamische SQL-Generierung für den DeleteCommand wird nicht für einen SelectCommand unterstützt, der keine Schlüsselspalteninformationen zurückgibt.


    Dieser Beitrag wurde bereits 1 mal editiert, zuletzt von „Schwakowiak“ ()

    hier die Persistance-Klasse ohne RequeryId.
    Dass du nicht verstehst, wovon ich rede, wundert mich. Einfach eine Textsuche nach "RequeryId" hätte dir doch gezeigt, wovon ich rede - zumindest teilweise. Egal - hier isses jdfs. nicht mehr drin
    Spoiler anzeigen

    VB.NET-Quellcode

    1. Imports System.Data.SqlClient
    2. Imports System.Data
    3. Imports System.Data.Common
    4. Public Class SqlPersistance
    5. Private _Con As SqlConnection
    6. Private _Adapters As Dictionary(Of DataTable, SqlDataAdapter)
    7. Private _RankedTables As List(Of DataTable)
    8. Private _Dts As DataSet
    9. Public Sub New(connectionString As String, dts As DataSet)
    10. _Dts = dts
    11. _Con = New SqlConnection(connectionString)
    12. Dim sorter = New TopologicSort(dts.Tables.Cast(Of DataTable), True)
    13. _RankedTables = sorter.GetRankedTables
    14. _Adapters = New Dictionary(Of DataTable, SqlDataAdapter)
    15. For Each tb In _RankedTables
    16. Dim adp = New SqlDataAdapter("Select * from [" & tb.TableName & "]", _Con)
    17. Dim cmb = New SqlCommandBuilder(adp)
    18. _Adapters.Add(tb, adp)
    19. Next
    20. End Sub
    21. Public Sub FillAll()
    22. _Dts.Clear()
    23. _Con.Open()
    24. For Each tb In _RankedTables
    25. _Adapters(tb).Fill(tb)
    26. Next
    27. _Con.Close()
    28. End Sub
    29. ''' <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>
    30. Public Sub CustomFill(table As DataTable, sqlAfterFrom As String, ParamArray args() As Object)
    31. Dim childrenFinder = New TopologicSort(_RankedTables, False)
    32. For Each tb In childrenFinder.GetRankedTables(table) : tb.Clear() : Next
    33. Using adp = DirectCast(DirectCast(_Adapters(table), ICloneable).Clone, SqlDataAdapter)
    34. Dim cmd = adp.SelectCommand
    35. Dim splits = sqlAfterFrom.Split("?"c)
    36. For i = 0 To args.Length - 1
    37. Dim name = "@p" & i
    38. cmd.Parameters.AddWithValue(name, args(i))
    39. splits(i) = String.Concat(splits(i), " ", name, " ")
    40. Next
    41. cmd.CommandText &= " " & String.Concat(splits)
    42. _Con.Open()
    43. adp.Fill(table)
    44. _Con.Close()
    45. End Using
    46. End Sub
    47. Public Sub Save()
    48. _Con.Open()
    49. For Each tb In _RankedTables
    50. Dim rows = tb.Select("", "", DataViewRowState.Added Or DataViewRowState.ModifiedCurrent)
    51. _Adapters(tb).Update(rows) ' first send Inserts and Updates
    52. Next
    53. Dim skipSubOrderedRowsConfig = New _AcceptruleCascadeConfig(_Dts)
    54. For Each tb In _RankedTables
    55. Dim n = _Adapters(tb).Update(tb) ' send the remaining Deletes, skipping Cascade-Deleted-Rows, which will be deleted by Db itself
    56. Next
    57. skipSubOrderedRowsConfig.Restore()
    58. _Con.Close()
    59. End Sub
    60. Private Class _AcceptruleCascadeConfig : Inherits List(Of Tuple(Of ForeignKeyConstraint, AcceptRejectRule))
    61. ' AccepRule.Cascade removes subordered rows from datasets change-tracking, during Updating Database
    62. ' Required while Updating deleted rows, since the db deletes subordered rows already by itself.
    63. Public Sub New(ByVal dts As DataSet)
    64. For Each rl As DataRelation In dts.Relations
    65. Dim ck = rl.ChildKeyConstraint
    66. If ck Is Nothing Then Continue For
    67. MyBase.Add(Tuple.Create(ck, ck.AcceptRejectRule))
    68. ck.AcceptRejectRule = AcceptRejectRule.Cascade
    69. Next
    70. End Sub
    71. Public Sub Restore()
    72. For Each tpl In Me
    73. tpl.Item1.AcceptRejectRule = tpl.Item2
    74. Next
    75. End Sub
    76. End Class
    77. Private Class TopologicSort
    78. Private _RankedTables As List(Of DataTable)
    79. Private _Hsh As HashSet(Of DataTable)
    80. Private _ConsiderParentRelations As Boolean ' (otherwise consider ChildRelations)
    81. Private _Tables As IEnumerable(Of DataTable)
    82. ''' <summary> if considerParentRelations = False it consideres ChildRelations </summary>
    83. Public Sub New(tables As IEnumerable(Of DataTable), considerParentRelations As Boolean)
    84. _Tables = tables : _ConsiderParentRelations = considerParentRelations
    85. End Sub
    86. ''' <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>
    87. Public Function GetRankedTables(Optional root As DataTable = Nothing) As List(Of DataTable)
    88. _Hsh = New HashSet(Of DataTable)(_Tables)
    89. _RankedTables = New List(Of DataTable)(_Hsh.Count)
    90. If root Is Nothing Then
    91. While _Hsh.Count > 0 : RecurseRelatedTables(_Hsh.First) : End While
    92. Else
    93. RecurseRelatedTables(root)
    94. End If
    95. Return _RankedTables
    96. End Function
    97. ''' <summary> before adding an element to result-list, recursively add its precusers </summary>
    98. Private Sub RecurseRelatedTables(tb As DataTable)
    99. If Not _Hsh.Remove(tb) Then Return 'prevent run in Cycles
    100. If _ConsiderParentRelations Then
    101. For Each rl As DataRelation In tb.ParentRelations
    102. RecurseRelatedTables(rl.ParentTable)
    103. Next
    104. Else
    105. For Each rl As DataRelation In tb.ChildRelations
    106. RecurseRelatedTables(rl.ChildTable)
    107. Next
    108. End If
    109. _RankedTables.Add(tb)
    110. End Sub
    111. End Class
    112. End Class



    Dein Fehler geschieht dir übrigens ganz recht. Deine Datenbank-Tabelle hat keinen Primärschlüssel, und dann können keine Update- oder Delete- Commands erzeugt werden, denn diese müss einen Primärschlüsselwert angeben, damit die Db weiß, welcher Datensatz gemeint ist. (Also genau das, was die Exception dir ja auch schon gesagt hat.)

    Willst du nicht lieber erst die Grundlagen lernen, ehe du anfängst, mit Datenbanken zu hantieren?
    Ich hab eine Art Lehrplan, da ist für dich so einiges dabei (fängt schon mit Option Strict On an, also dort der punkt#1): Datenverarbeitungs-Vorraussetzungen