Access-DB - TableAdapter - Datensatz einfügen - Primärschlüssel Problem

  • VB.NET

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

    Access-DB - TableAdapter - Datensatz einfügen - Primärschlüssel Problem

    Ich habe folgendes Problem: Ich habe mittels Daten - neue Datenquelle hinzufügen - Datenbank - DataSet - Verweis auf eine Access-MDB (Provider=Microsoft.Jet.OLEDB.4.0;Data Source=XYZ.mdb) ein stark typisiertes Dateset erhalten, das den Inhalt der Datenbank repräsentiert. Dieser wird mit .Fill befüllt. (soweit nichts besonderes, schon unzählige male gemacht)

    Nun möchte ich einen neuen Datensatz erstellen und habe mich an die Anweisungen der msdn gehalten (Gewusst wie: Einfügen neuer Datensätze in eine DatenbankGewusst wie: Einfügen neuer Datensätze in eine Datenbank)

    VB.NET-Quellcode

    1. ' Create a new row.
    2. Dim newRegionRow As NorthwindDataSet.RegionRow
    3. newRegionRow = Me.NorthwindDataSet._Region.NewRegionRow()
    4. newRegionRow.RegionID = 5
    5. newRegionRow.RegionDescription = "NorthWestern"
    6. ' Add the row to the Region table
    7. Me.NorthwindDataSet._Region.Rows.Add(newRegionRow)
    8. ' Save the new row to the database
    9. Me.RegionTableAdapter.Update(Me.NorthwindDataSet._Region)


    Das funktioniert soweit auch gut und der neuen Datensatz steht im Dataset und in der Datenbank, wie gewollt. Nur hat der Datensatz im Dataset die ID = -1, was auch klar ist, weil die entsprechende Spalte im DataSet folgende Einstellungen hat:
    AutoIncrement = True
    AutoIncrementSpeed = -1
    AutoIncrementStep = -1
    Das macht soweit auch Sinn (siehe "AutoIncrement"-Spalten), wenn wenn ich den letzten DS lösche und das Programm neu starte und einen neuen DS erstelle, bekommt er die ID des gelöschten DS. (zB DS 64 wird gelöscht, nach Neustart wird erneut 64 vergeben), was aber nicht dem Verhalten von Access entspricht, denn dort hat der DS die ID = 65 und 64 bleibt leer. Soweit für mich auch klar.

    Jetzt ist meine Frage, wie kann ich nach dem oben genannten Einfügen eines neuen DS das DataSet und die AccessDB soweit synchronisieren, dass die ID des neuen DS nicht -1 ist, sondern der ID des DS in der Access-DB entspricht. Momentan löse ich das indem ich den TableAdapter mit .Fill neu befülle, was aber - soweit ich es verstanden habe - nicht zweckdienlich ist, da die Datenbank komplett ausgelesen wird.

    Was ich sonst noch probiert habe:
    • Arbeite ich mit der TabeAdapter.Insert - Methode wird der DS nur in die Datenbank geschrieben, steht aber nicht in meinem DataSet. Da würd ich in aber auch benötigen. Daher bringt das auch nichts.
    • Wenn ich im DataSet in der entsprechenden Tabelle unter Konfigurieren gehe und dann unter "Erweiterte Optionen" gibt es den Eintrag "Datantabelle aktualisieren" (Fügt eine Select-Anweisung nach Insert- u Update-Anweisungen hinzu, um Identitätsspralten .. abzurufen, die von der Datenbank berechnet werden) - das wäre ja eigentlich, was ich will - nur ist dieser Eintrag nicht auswählbar, da ausgegraut
    • Wenn ich unter Abfrage hinfügen gehe - SQL-Anweisungen verwenden - INSERT - und eine erweiterte InsertAnweisung erstelle, die den neuen DS hinzufügt (Insert Into und dann mit Scope_Identity) den neuen abfragt (siehe unten), bekomme ich eine OleDBException (Zeichen nach Ende der SQL-Anweisung gefunden)

    SQL-Abfrage

    1. INSERT INTO `Region` (` RegionDescription `) VALUES (?);
    2. SELECT RegionID, RegionDescription
    3. FROM Region
    4. WHERE (RegionID= SCOPE_IDENTITY())


    • Was zwar geht, aber mir auch nicht ganz korrekt erscheint - ist die höchste ID nach dem hinzufügen ermitteln und dann die Zeile mit dieser ID aktualisieren
    neueID

    SQL-Abfrage

    1. SELECT MAX(RegionID) AS Expr1
    2. FROM Region


    FillByID

    SQL-Abfrage

    1. SELECT RegionID, RegionDescription
    2. FROM Region
    3. WHERE (RegionID= ?)


    Verwendung: Me.RegionTableAdapter.FillByID(Me.NorthwindDataSet._Region, Me.RegionTableAdapter.neueID())

    Was wäre denn die beste Möglichkeit den DataTable und die Datenbank in Bezug auf ihren AutoincrementWert synchron zu halten. Danke im voraus.
    Ich vermute das Problem ist das Du die falsche Vorgehensweise anwendest.

    So wie ich das sehe, wählst Du eine Vorgehensweise die die neue Row direkt in die Datenbank schreibt und Du nun vor dem Problem stehst wie akutalisiere ich mein typisiertes DataSet.

    Sinnvolle wäre es aber eher Änderungen in VB.NET nur direkt auf das typisierte DataSet auszuführen und ausschließlich darüber Änderungen an der DB vorzunehmen.

    Also vom Prinzip her: Neue Row wird nicht in die Datenbank geschrieben, sondern in das typisierte DataSet und das aktualisiert dann die Datenbank über AcceptChanges().

    Ich bin mir nicht sicher (weil ich eigentlich hauptsächlich mit der OCDB-Connection arbeite) aber meiner Meinung nach werden bei AcceptChanges() nämlich auch bei Added-Rows die AutoCounter-Werte gesynced.

    Gruß

    Rainer

    Chr78 schrieb:

    Nun möchte ich einen neuen Datensatz erstellen und habe mich an die Anweisungen der msdn gehalten
    Tja, msdn ist leider nur mit Vorsicht zu genießen, und gelegentlich auch ganz ungenießber ;)

    Chr78 schrieb:

    Jetzt ist meine Frage, wie kann ich nach dem oben genannten Einfügen eines neuen DS das DataSet und die AccessDB soweit synchronisieren, dass die ID des neuen DS nicht -1 ist, sondern der ID des DS in der Access-DB entspricht.

    Genau der richtige Gedanke.
    Dazu muß man sagen: Bei Access ist das Inserten neuer DS voll der Krampf, jdfs. bei Serverseitiger Primkey-Generierung.
    Viel einfacher isses, im Dataset den Primkey auf Autowert zu stellen - also im Client - und inne AccessDB eben nicht. Dann wird im Dataset gleich der endgültige Primkey erzeugt, und beim Updaten auch in die DB geschrieben.
    Ist natürlich nur bei Einzel-User machbar, bei Multi-User besteht das Risiko, dass verschiedene Clients denselben Primkey generieren.
    Für MultiUser kann man auf Guid als Primkey-Typ ausweichen. Guids können in jedem Client generiert werden, ohne dasses zu Kollisionen kommt.
    Aber auch serverseitige PrimKey-Generierung ist möglich, nur erfordert das, dass bei jedem Insert sofort ein Select @@Identity nachgeschossen wird, um vom Server den aktuellen Primkey zu erfahren.
    Ich hab mal ein Sample gemacht, bei dem ich dieses Nachschießen im UserCode des typisierten Datasets implementiert habe, nämlich im _RowUpdated-Event des DataAdapters:

    VB.NET-Quellcode

    1. Imports System.Data.OleDb
    2. Namespace MAAutoWertDataSetTableAdapters
    3. Partial Public Class MitarbeiterTableAdapter
    4. Private _IdentityCommand As New OleDbCommand("SELECT @@IDENTITY")
    5. Private Sub Adapter_RowUpdated(ByVal sender As Object, ByVal e As OleDbRowUpdatedEventArgs) Handles _adapter.RowUpdated
    6. If e.StatementType = StatementType.Insert Then
    7. _IdentityCommand.Connection = e.Command.Connection
    8. _IdentityCommand.Transaction = e.Command.Transaction
    9. e.Row(e.Row.Table.PrimaryKey(0)) = CInt(_IdentityCommand.ExecuteScalar())
    10. End If
    11. End Sub
    12. End Class
    13. End Namespace

    Das ist auch der Sinn, dass das Dataset die Primkeys zunächst negativ generiert: Dadurch kommt es zu keinen Kollisionen, wenn nachm Insert der vorläufige Primkey durch den endgültigen überschrieben wird.

    Du kannst die Funktionalität überprüfen, indem du in der App einen Mitarbeiter zufügst, und dann speicherst. In dem Moment ändert sich die MA-ID vom negativen Wert in einen positiven.

    Zu beachten ist auch, dass bei DataRelations "Regel Aktualisieren - Cascade" eingestellt sein muß, damit untergeordnete Datensätze entsprechend ihre ForeignKeys korrigieren.

    Dataset.AcceptChanges() hat eine ganz annere Bedeutung. Das resettet das Change-Tracking des Datasets. .AcceptChanges() lässt jede DataRow ihre Änderungen "vergessen". Da sollte man ganz die Finger von lassen, das ist eine Methode, die wird vom DataAdapter aufgerufen, nachdem er alle Änderungen mit der DB synchronisiert hat.

    Das ganze Theater entfällt beim SqlServer, weil der iwie In-/Out-Parameter unterstützt, und bei dem die Code-Generierung des typisierten Dataset nutzt dieses Feature, sodaß man dort komplett von diesem Murks befreit ist. (Aber ich find SqlServer so monströs.)
    Dateien
    • UpdateDB00.zip

      (155,52 kB, 268 mal heruntergeladen, zuletzt: )

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

    Hallo Erfinder des Rades,

    ich bin bei der Forensuche auf Deinen Code gestoßen, komme aber leider nicht ganz klar.
    Welchen Adapter sprichst Du darin an ?
    Der relevante Code meines Programms sieht so aus

    VB.NET-Quellcode

    1. Private daRechner As New dsMA_RechnerTableAdapters.tbl_RechnerTableAdapter
    2. Private daRechnerModell As New dsMA_RechnerTableAdapters.tbl_RechnerModellTableAdapter
    3. 'ACCESS Datenbank zuweisen
    4. Private _strDatenbank As String
    5. Public Property Datenbank() As String
    6. Get
    7. Return _strDatenbank
    8. End Get
    9. Set(ByVal value As String)
    10. If IO.File.Exists(value) Then
    11. _strDatenbank = value
    12. Else
    13. Throw New ArgumentException("Fehler: Datenbank " & value & " konnte nicht gefunden werden")
    14. End If
    15. End Set
    16. End Property
    17. 'ConnectionString zurückgeben
    18. Public ReadOnly Property ConnectionString() As String
    19. Get
    20. Const conProvider = "Provider=Microsoft.Jet.OLEDB.4.0;"
    21. Return String.Concat("", conProvider, "Data Source=", Datenbank, "")
    22. End Get
    23. End Property
    24. 'tbl_Rechner
    25. Public Sub FuelleTabelleRechner()
    26. daRechner.Connection.ConnectionString = ConnectionString
    27. daRechner.Fill(tbl_Rechner)
    28. End Sub
    29. Public Sub daRechnerupdate()
    30. daRechner.Update(tbl_Rechner)
    31. End Sub
    32. 'tbl_RechnerModell
    33. Public Sub FuelleTabelleRechnerModell()
    34. daRechnerModell.Connection.ConnectionString = ConnectionString
    35. daRechnerModell.Fill(tbl_RechnerModell)
    36. End Sub
    37. Public Sub daRechnerModellupdate()
    38. daRechnerModell.Update(tbl_RechnerModell)
    39. End Sub

    was muß ich anpassen, das das funktioniert ? Ich bastele nun schon über 3 h ohne Ergebnis.
    Ich brauche den Primärschlüssel aus Rechnermodell.

    Schoofi schrieb:

    Welchen Adapter sprichst Du darin an ?
    Den MitarbeiterTableAdapter. Steht da doch (zeile#5).

    Schoofi schrieb:

    was muß ich anpassen, das das funktioniert ?
    Keine Ahnung.
    Ich weiß nichtmal, was nicht funktioniert. Dein Code zeigt ja 7 Methoden/Properties, wo irgendetwas nicht funktionieren kann, und was?
    Vermutlich diese Zeile

    VB.NET-Quellcode

    1. Return String.Concat("", conProvider, "Data Source=", Datenbank, "")


    Der Pfad zur Datenbank sollte/muss in Anführungszeichen. Abgesehen davon prüfst du nicht ob die Variable Datenbank gefüllt ist.

    EDIT:
    Die Variable conProvider solltest du zu beginn der Klasse Deklarieren und nicht jedes mal wenn die Eigenschaft abgerufen wird.
    Die Datenbankzuweisungen funktionieren alle, ist mehrfach getestet.
    Die Deklaration der Konstante conProvider innerhalb der Property war ein Schusselfehler ist bereinigt.

    Nun zum eigentlichen Problem:
    Wenn ich den Code von EDR einsetze und anpasse, kommt in der Zeile

    VB.NET-Quellcode

    1. ​Private Sub Adapter_RowUpdated(ByVal sender As Object, ByVal e As OleDbRowUpdatedEventArgs) Handles _adapter.RowUpdated

    die Meldung."Die Handles Klausel erfordert eine With Events Variable...." so als ob er das Ereignis nicht erkennt.
    Der Code sieht jetzt folgendermaßen aus.

    VB.NET-Quellcode

    1. Imports System.Data.OleDb
    2. Partial Public Class tbl_RechnerModellTableAdapter
    3. Private _IdentityCommand As New OleDbCommand("SELECT @@IDENTITY")
    4. Private Sub Adapter_RowUpdated(ByVal sender As Object, ByVal e As OleDbRowUpdatedEventArgs) Handles _adapter.RowUpdated
    5. If e.StatementType = StatementType.Insert Then
    6. _IdentityCommand.Connection = e.Command.Connection
    7. _IdentityCommand.Transaction = e.Command.Transaction
    8. e.Row(e.Row.Table.PrimaryKey(0)) = CInt(_IdentityCommand.ExecuteScalar())
    9. End If
    10. End Sub
    11. End Class
    12. Partial Class dsMA_Rechner
    13. Const conProvider = "Provider=Microsoft.Jet.OLEDB.4.0;"
    14. Private daRechner As New dsMA_RechnerTableAdapters.tbl_RechnerTableAdapter
    15. Private daRechnerModell As New dsMA_RechnerTableAdapters.tbl_RechnerModellTableAdapter
    16. 'ACCESS Datenbank zuweisen
    17. Private _strDatenbank As String
    18. Public Property Datenbank() As String
    19. Get
    20. Return _strDatenbank
    21. End Get
    22. Set(ByVal value As String)
    23. If IO.File.Exists(value) Then
    24. _strDatenbank = value
    25. Else
    26. Throw New ArgumentException("Fehler: Datenbank " & value & " konnte nicht gefunden werden")
    27. End If
    28. End Set
    29. End Property
    30. 'ConnectionString zurückgeben
    31. Public ReadOnly Property ConnectionString() As String
    32. Get
    33. Const conProvider = "Provider=Microsoft.Jet.OLEDB.4.0;"
    34. Return String.Concat("", conProvider, "Data Source=", Datenbank, "")
    35. End Get
    36. End Property
    37. 'tbl_Rechner
    38. Public Sub FuelleTabelleRechner()
    39. daRechner.Connection.ConnectionString = ConnectionString
    40. daRechner.Fill(tbl_Rechner)
    41. End Sub
    42. Public Sub daRechnerupdate()
    43. daRechner.Update(tbl_Rechner)
    44. End Sub
    45. 'tbl_RechnerModell
    46. Public Sub FuelleTabelleRechnerModell()
    47. daRechnerModell.Connection.ConnectionString = ConnectionString
    48. daRechnerModell.Fill(tbl_RechnerModell)
    49. End Sub
    50. Public Sub daRechnerModellupdate()
    51. daRechnerModell.Update(tbl_RechnerModell)
    52. End Sub

    Schoofi schrieb:

    die Meldung."Die Handles Klausel erfordert eine With Events Variable...." so als ob er das Ereignis nicht erkennt.
    Jo, das musst du einfach nur lesen - erklären kann man das nicht weiter, bzw die Erklärung lautet: "Die Handles-Klausel erfordert eine WithEvents Variable".
    Wenn dir das nichts sagt, musste dich halt schlau machen, dasses dir was sagt - da haste einige Möglichkeiten:
    1. recherchier die Schlüsselworte Handles und Withevents entweder gutes Buch (zB der Löffelman2005 ausse Bücherliste) oder für Schlüsselworte geht auch MSDN-Sprachdefinition msdn.microsoft.com/de-de/library/dd409611(v=vs.140).aspx
    2. studiere Alles über Events , am besten sogar nicht nur das Video, sondern das ganze Tut.
    3. natürlich nachgucken, was an meim Tut-BeispielCode anders ist als an deinem - weil ich krieg den Fehler ja nicht