none
DataTable auf Datenbank übertragen und Primary Key von der Datenbank erhalten RRS feed

  • Frage

  • Hallo zusammen,

    wie geht man vor wenn man hinzugefügte Datensätze in einem DataTable auf die Datenbank (MS SQL-Server) übertragen möchte und gleichzeitig die (bisher temporär gesetzte) Primary-Key Spalte aktualisieren will?

    Ich habe hier ein Buch liegen da gehen die darauf irgendwie überhaupt nicht ein. Es wird nur beschrieben wie man mittels "@@IDENTITY" die ID des letzen Datensatzes abruft, aber die ID-Spalte im DataTable ist ja Schreibgeschützt.
    In einem anderen Beispiel wird das Update durchgeführt, danach die DataTable gelöscht (DataTable.Clear) und dann neu befüllt - aber das kann doch nicht Zweckmäßig sein...

    Public Sub UpdateDatabase(ByVal dt As DataTable, ByVal TableName As String) As Integer
    
            Dim conn As New SqlConnection(ConnectionString)
            conn.Open()
            Dim sqlDataadapter As New SqlDataAdapter("SELECT * FROM " & TableName, conn)
            Dim cb As New SqlCommandBuilder(sqlDataadapter)
    
            ''Neue Zeilen einzeln hinzufügen und ID auslesen
    ' -> geht nicht, da ID Schreibgeschützt!
            'For Each dr As DataRow In From _dr As DataRow In dt Where _dr.RowState = DataRowState.Added
            '    sqlDataadapter.Update({dr})
            '    dr(dt.PrimaryKey(0)) = New SqlCommand("SELECT @@IDENTITY FROM " & TableName, conn)
            '    dr.AcceptChanges()
            'Next
    
            sqlDataadapter.Update(dt)
    
            
            sqlDataadapter.Fill(dt)
    
            cb.Dispose()
            sqlDataadapter.Dispose()
            conn.Dispose()
    
        End Sub

    Versteh ich da was Grundsätzliches falsch?

    Vielen Dank und viele Grüße

    Mike

    Dienstag, 26. Januar 2016 15:30

Antworten

  • Hi Mike,
    die ID automatisch mit 1 beginnend zu vergeben, ist keine gute Idee. Das funktioniert zwar, aber nur solange keine Fremdschlüssel damit verbunden sind (z.B. Master-Child). Besser ist es, im Programm negative Werte zu nutzen. Da kann es keine Konflikte geben.

    Der CommandBuilder (Verweisvariable cb) braucht nicht genutzt zu werden, solange die Standardeinstellungen zur Generierung der SQL Anweisungen ausreichend sind. Wenn aber der "Der Letzte gewinnt" einzustellen ist, ist "cb" für die Einstellung zu nutzen. Mit der Übergabe des Verweises auf den DataAdapter im Konstruktor wird die Beziehung zwischen CommandBuilder und DataAdapter hergestellt und der DataAdapter fragt bei Bedarf den Commandbuilder, dass dieser ihm auf Basis des SelectCommands eine SQL Anweisung bereitstellt. Der Bedarf liegt dann vor, wenn dem DataAdapter kein Command-Objekt für Update, Delete, Insert zugewiesen wurde.

    Für das Einfügen und Rücklesen kann die folgende SQL Anweisung genutzt werden:

    INSERT INTO [dbo].[Tab1] ([info]) VALUES (@info);
    SELECT Id, info FROM Tab1 WHERE (Id = SCOPE_IDENTITY())

    Den gesamten Ablauf kann man so demonstrieren:

        Debug.Print("---")
        Using da As New SqlDataAdapter("SELECT ID,info FROM Tab1", My.Settings.Database1ConnectionString)
          Dim dt As New DataTable
          With dt.Columns
            With .Add("ID", GetType(Integer))
              .AutoIncrement = True
              .AutoIncrementSeed = -1
              .AutoIncrementStep = -1
            End With
          End With
          da.Fill(dt)
          Dim row = dt.NewRow
          row(1) = "Neue Zeile"
          dt.Rows.Add(row)
          Debug.Print("Vergebene ID: {0}", row(0))
          Dim cmd As New SqlCommand("INSERT INTO [dbo].[Tab1] ([info]) VALUES (@info); SELECT Id, info FROM Tab1 WHERE (Id = SCOPE_IDENTITY())", da.SelectCommand.Connection)
          Dim par As New SqlParameter
          With par
            .ParameterName = "@info"
            .SourceVersion = DataRowVersion.Current
            .SourceColumn = "info"
          End With
          cmd.Parameters.Add(par)
          da.InsertCommand = cmd
          Debug.Print("SQL Anweisung für Insert: {0}", da.InsertCommand.CommandText)
          da.Update(dt)
          Debug.Print("ID nach Update: {0}", row(0))
        End Using

    Parallelitätsverletzung kommt, wenn die Originalversion im Client (=Programm) nicht mit dem Inhalt in der Datenbank übereinstimmt. Das kann der Fall sein, wenn AcceptChanges falsch angewandt wurde oder ein Datensatz nach dem Update in der Datenbank nicht zurückgelesen wurde, damit wieder beide Inhalte übereinstimmen. Auch kann eine Parallelitätsverletzung entstehen, wenn im Mehrnutzerbetrieb positive Autowerte vergeben werden.


    --
    Viele Grüsse
    Peter Fleischer (MVP, Partner)
    Meine Homepage mit Tipps und Tricks
    Kommas richtig setzen!
    Schüler sagen, Lehrer haben es gut.
    Schüler, sagen Lehrer, haben es gut



    • Bearbeitet Peter Fleischer Samstag, 30. Januar 2016 20:37
    • Als Antwort markiert Mike0680 Montag, 1. Februar 2016 18:40
    Samstag, 30. Januar 2016 20:35

Alle Antworten

  • Hallo Mike,

    ich glaube da wirfst du etwas durcheinander.

    1. schalte OPTION EXPLICIT und STRICT auf ON.
    2. Möchtest du eine SUB oder FUNCTION erstellen? Du schreibst SUB und diese soll ein INTEGER liefern.

    Ansonsten hab ich dir mal ein bisschen Code zusammengestellt wo das Einfügen in die DB funktionieren sollte und dein SKALAR zurück in die übergebene Tabelle geschrieben wird.

        Public Function UpdateDatabase(ByVal dt As DataTable, ByVal TableName As String) As DataTable
            Using con As New SqlConnection("connectionstring")
    
                'Command vorbereiten zum eintragen von neuen Datensätzen
                Dim cmd As New SqlCommand("INSERT INTO TabelleXYZ (Wert1, Wert2, Wert3) VALUES (@Wert1, @Wert2, @Wert3);SELECT CAST(scope_identity() AS int)", con)
    
                'Parameter definieren
                cmd.Parameters.Add("@Wert1", SqlDbType.VarChar)
                cmd.Parameters.Add("@Wert2", SqlDbType.VarChar)
                cmd.Parameters.Add("@Wert3", SqlDbType.VarChar)
    
                'Übergebene Tabelle Zeile für Zeile durchlaufen
                For Each rw As DataRow In dt.Rows
                    'Parameter mit Informationen aus der übergebenen Tabelle zuweisen
                    cmd.Parameters("@Wert1").Value = rw.Field(Of String)("Name")
                    cmd.Parameters("@Wert2").Value = rw.Field(Of String)("Vorname")
                    cmd.Parameters("@Wert3").Value = rw.Field(Of String)("Adresse")
    
                    'Feuer! Und der abgerufene SCALAR soll in die übergebene Tabelle zurückgeschrieben werden.
                    rw("ID") = DirectCast(cmd.ExecuteScalar(), Integer)
    
                Next
            End Using
    
            'Übergebene Tabelle jetzt mit den IDs zurück
            Return dt
    
        End Function
    Hoffe du kannst das auf dein Vorhaben umsetzen.

    Dienstag, 26. Januar 2016 15:53
  • Hi Mike,
    um konfliktfrei in Mehrnutzerumgebungen zu arbeiten, werden üblicherweise neue Datensätze im Programm erst einmal mit negativem Autowert belegt. Beim Schreiben in die Datenbank (INSERT) wird dann der endgültige Autowert vergeben. @@IDENTITY liefert den zuletzt vergebenen Autowert zurück. Wenn in der Datenbank aber Trigger aktiv sind, die auch neue Datensätze erzeugen (z.B. Protokollierung der Inserts), dann liefert @@IDENTITY den falschen Wert. Es sollte deshalb immer Scope_Identity genutzt werden. Außerdem können im Datenbankserver hinterlegte Regeln für Standardwerte oder auch Tripper den neu hinzugefügten Datensatz verändert ablegen. Deshalb sollte nach dem INSERT der gesamte Datensatz zur Scope_Identity zurückgelesen werden und den im Programm vorhandenen Datensatz überschreiben. Die DataAdapter.Update-Methode bietet da entsprechende Einstellungen und Ereignisse, wo man das machen kann. Außerdem bietet der SQL Server die Möglichkeit, an das Update gleich nach das Rücklesen des hinzugefügten Datensatzes zu hängen.

    --
    Viele Grüsse
    Peter Fleischer (MVP, Partner)
    Meine Homepage mit Tipps und Tricks
    Kommas richtig setzen!
    Schüler sagen, Lehrer haben es gut.
    Schüler, sagen Lehrer, haben es gut

    Mittwoch, 27. Januar 2016 01:32
  • Hallo Mike,

    alternativ zu Davids Lösung kann man bei SQL Server 2005 und später die OUTPUT-Klausel (Transact-SQL) nutzen, um sich hier die Identität, aber auch andere Werte liefern zu lassen. Die Klausel funktioniert auch bei UPDATE und DELETE und ist insofern universeller einsetzbar.

    Für ein (C#) Beispiel siehe: SqlCommand und Output

    Gruß Elmar

    Mittwoch, 27. Januar 2016 16:21
    Beantworter
  • Hallo Mike,

    es ist auch möglich temporär den Schreibschutz aufzuheben.

    Du kannst SET IDENTITY_INSERT nutzen, um den PrimaryKey zu ändern / setzen.

    SET IDENTITY_INSERT deineTabelle ON
    
     -- dein UPDATE
    
    SET IDENTITY_INSERT deineTabelle OFF

    Mehr Informationen: https://msdn.microsoft.com/de-de/library/ms188059(v=sql.120).aspx

    Ist natürlich nicht das schönste, aber es funktioniert ;).

    VG JSX


    Mittwoch, 27. Januar 2016 21:11
  • Nicht schön ist noch milde formuliert. Was willst du denn machen wenn du in mehrere Tabellen schreiben willst? Bei jeder Tabelle den Schreibschutz aufgeben und wieder setzen? Das ist meiner Meinung nach überhaupt keine Alternative!
    Donnerstag, 28. Januar 2016 08:34
  • Es kommt halt immer darauf an was man erreichen will. Will ich eine Tabelle auf einen anderen Server migrieren und die Schlüssel exakt so haben, dann ist das eine durchaus denkbare Lösung. Wenn ich in mehrere Tabellen schreiben möchte, welche zusammenhängen, dann habe ich ggf. Fremdschlüssel mit ON UPDATE CASCADE?

    Es gibt zu viele unklare Faktoren in der Frage. Allgemein ist es ja nicht angedacht einen automatischen PK nach belieben upzudaten, das sollte ja nur ein Ausnahmefall sein oder man hat Etwas im Datenkonzept nicht richtig gemacht.

    Donnerstag, 28. Januar 2016 13:18
  • Hallo zusammen und vielen Dank für Eure Antworten!

    @David: 'As Integer' war ein 'Copy-Paste-Fehler' von mir. Ich hatte ursprünglich mal eine Funktion daraus gemacht und die anzahl der Betroffenen Datensätze zurückgegeben.

    Option Explicit/Strict habe ich auf ON gestellt...

    Prinzipiell Beantwortet mir Dein Beispiel die Frage schon mal insofern dass anscheinend keine Möglichkeit gibt ein DataTable mit einem Ein-Zeiler (SqlDataAdapter.Update) upzudaten und gleichzeitig die ID´s zu aktualisieren. 

    Die ID vergibt das DataTable aktuell automatisch, wenn ich eine neue Zeile hinzufüge (beginnend bei 1). Jedoch stoße ich beim Versuch die ID nach dem Update zu ändern an den Schreibschutz der ID-Spalte.

    Was ich auch nicht verstehe: In meinem Beispiel (aus dem Buch) wird der 'CommandBuilder' verwendet: Dim cb As New CommandBuilder. Aber die erstellte Variable 'cb' wird dann nirgends mehr verwendet. Ist das die Vorgehensweise zur Verwendung des CommandBuilders?! Im Internet habe ich einige Beispiele gefunden wo man das CommandBuilder.GetInsert/Update/DeleteCommand dem SqlDatadapter zugewiesen hat. Hat der CommandBuilder in meinem Beispiel überhaupt eine Funktion?!

    Wenn ich Peters Ausführung richtig verstanden habe würde das bedeuten
    1.) eine Zeile des DT in Datenbank
    2.) mittels Scope_Identity die ID auslesen
    3.) mittels der ID die ganze Zeile auslesen und mit der Zeile des DataTables vergleichen?!

    Grundsätzliches zu meinem Vorhaben: eine Anwendung Loggt diverse Vorkommnisse in einer SQL-Datenbank (SQL-Express). Dazu werden diese Vorkommnisse mit einigen Informationen und einem Zeitstempel bei Auftreten in das DataTable geschrieben und direkt auf die Datenbank übertragen. Sobald der Nutzer das Vorkommnis bestätigt hat, kommt ein weiterer Zeitstempel dazu und das Ganze wird wieder auf die Datenbank übertragen.

    Aktuell habe ich in beiden Fällen obiges beispiel verwenden wollen. Das erste Eintragen in die Datenbank geht auch noch, beim Update (2.Zeitstempel) erhalte ich eine Parallelitätsverletzung (ich vermute, weil die ID meiner DataTable nicht mit der ID in der Datenbank übereinstimmt?!)

    Viele Grüße
    Mike

    Samstag, 30. Januar 2016 06:08
  • Hallo

    aufgrund der Beschreibung gehe ich davon aus, dass es sich um eine "Single-User"-Anwendung handelt. Ein zweiter Client würde so ja nichts von den neuen Log-Datensätzen des anderen Clients mitbekommen.

    Grundsätzlich kann man die Verwendung eines "Autokeys" breit diskutieren. Ich mache deshalb auch nur mal einen alternativen Vorschlag. Man könnte als Key einen GUID-Wert verwenden den man lokal generiert und überlässt das also nicht dem DBS. Beim Insert wird also der Key verwendet, den der Client sendet. Dieser weiß somit schon vorher, wie der Key heißt und kann den für Updates benutzen ohne dass noch mal was geändert werden müsste.

    Die Verwendung der CommandBuilder-Klasse ist hier beschrieben. Ob das Sinn macht, hängt wohl von der Situation ab. Eine Verwendung von "cb" in deinem Codeschnipsel kann ich auch nicht erkennen.

    Gruß

    Samstag, 30. Januar 2016 15:51
  • Hi Mike,
    die ID automatisch mit 1 beginnend zu vergeben, ist keine gute Idee. Das funktioniert zwar, aber nur solange keine Fremdschlüssel damit verbunden sind (z.B. Master-Child). Besser ist es, im Programm negative Werte zu nutzen. Da kann es keine Konflikte geben.

    Der CommandBuilder (Verweisvariable cb) braucht nicht genutzt zu werden, solange die Standardeinstellungen zur Generierung der SQL Anweisungen ausreichend sind. Wenn aber der "Der Letzte gewinnt" einzustellen ist, ist "cb" für die Einstellung zu nutzen. Mit der Übergabe des Verweises auf den DataAdapter im Konstruktor wird die Beziehung zwischen CommandBuilder und DataAdapter hergestellt und der DataAdapter fragt bei Bedarf den Commandbuilder, dass dieser ihm auf Basis des SelectCommands eine SQL Anweisung bereitstellt. Der Bedarf liegt dann vor, wenn dem DataAdapter kein Command-Objekt für Update, Delete, Insert zugewiesen wurde.

    Für das Einfügen und Rücklesen kann die folgende SQL Anweisung genutzt werden:

    INSERT INTO [dbo].[Tab1] ([info]) VALUES (@info);
    SELECT Id, info FROM Tab1 WHERE (Id = SCOPE_IDENTITY())

    Den gesamten Ablauf kann man so demonstrieren:

        Debug.Print("---")
        Using da As New SqlDataAdapter("SELECT ID,info FROM Tab1", My.Settings.Database1ConnectionString)
          Dim dt As New DataTable
          With dt.Columns
            With .Add("ID", GetType(Integer))
              .AutoIncrement = True
              .AutoIncrementSeed = -1
              .AutoIncrementStep = -1
            End With
          End With
          da.Fill(dt)
          Dim row = dt.NewRow
          row(1) = "Neue Zeile"
          dt.Rows.Add(row)
          Debug.Print("Vergebene ID: {0}", row(0))
          Dim cmd As New SqlCommand("INSERT INTO [dbo].[Tab1] ([info]) VALUES (@info); SELECT Id, info FROM Tab1 WHERE (Id = SCOPE_IDENTITY())", da.SelectCommand.Connection)
          Dim par As New SqlParameter
          With par
            .ParameterName = "@info"
            .SourceVersion = DataRowVersion.Current
            .SourceColumn = "info"
          End With
          cmd.Parameters.Add(par)
          da.InsertCommand = cmd
          Debug.Print("SQL Anweisung für Insert: {0}", da.InsertCommand.CommandText)
          da.Update(dt)
          Debug.Print("ID nach Update: {0}", row(0))
        End Using

    Parallelitätsverletzung kommt, wenn die Originalversion im Client (=Programm) nicht mit dem Inhalt in der Datenbank übereinstimmt. Das kann der Fall sein, wenn AcceptChanges falsch angewandt wurde oder ein Datensatz nach dem Update in der Datenbank nicht zurückgelesen wurde, damit wieder beide Inhalte übereinstimmen. Auch kann eine Parallelitätsverletzung entstehen, wenn im Mehrnutzerbetrieb positive Autowerte vergeben werden.


    --
    Viele Grüsse
    Peter Fleischer (MVP, Partner)
    Meine Homepage mit Tipps und Tricks
    Kommas richtig setzen!
    Schüler sagen, Lehrer haben es gut.
    Schüler, sagen Lehrer, haben es gut



    • Bearbeitet Peter Fleischer Samstag, 30. Januar 2016 20:37
    • Als Antwort markiert Mike0680 Montag, 1. Februar 2016 18:40
    Samstag, 30. Januar 2016 20:35
  • Hallo Peter hallo K.,

    vielen Dank Euch für die Ausführlichen Erklärungen. Die Angelegenheit scheint mir jetzt schon etwas  verständlicher und ich werde mein Programm entsprechend etwas umgestalten.

    Bis dahin - viele Grüße und nochmals herzlichen Dank!

    Mike

    Montag, 1. Februar 2016 18:40