Tipp & Trick: DbParameter

    • VB.NET

    Es gibt 5 Antworten in diesem Thema. Der letzte Beitrag () ist von ErfinderDesRades.

      Tipp & Trick: DbParameter

      (Vorbemerkung: Nicht, dass jmd denkt, ich sei der einzige Spinner, der DbParameter als "Must-Use - No-Excuces" propagiert - dasselbe kann man zB auch hier nachlesen - (halt mit imo umständlicheren Code-Lösungen, und für c#).)

      Immer wieder sieht man DB-Zugriffe, bei denen Werte an die Datenbank gesendet werden, die direkt in den CommandText eingefrickelt sind.

      Also mal paar willkürliche Beispiele der letzten Tage:

      VB.NET-Quellcode

      1. dt = fillControl("Select distinct(Zahl) from Training where system = '" & getSystem() & "' and versuchsleiter='" & Form1.comboTechnican.Text & "' and session_date > sysdate - 28 and tier_nr < 9999 order by Zahl asc")
      2. 'oder
      3. Dim sql As String = "SELECT COUNT (*) FROM Tabelle WHERE NichtsGefangen = FALSE " & If(UserId <> 0, "User = " & UserId & """, "")
      4. 'oder
      5. Dim SQLAbfrage As String = "UPDATE `benutzer` SET akku = '" + tb_akku.Text - 1 + "' WHERE `Benutzername` = '" + Eingang.lbl_name.Text + "'"

      Diese Vorgehensweise ist unleserlich, fehleranfällig und v.a. kriminell fahrlässig im Umgang mit der Datenbank des Datenbankbesitzers und den Daten der Programm-Nutzer.
      NoGo ists, weil vollkommen unnötig, denn man kann mit demselben oder sogar weniger Aufwand auch eine sicherere Kommunikation proggen, indem man DbParameter verwendet.
      Und ist ja auch klar, wo man solchen X/ X/ X/ lernt: Galileio Openbook, wo sonst? (oder - ganz hardcore: YouTube :cursing: ).

      Also falls jmd. VB erlernen möchte, ohne sich von Galileio verderben zu lassen, empfehle ich dieses Buch Lesen

      Ich persönlich halte von selbstgebastelten DbCommands ja insgesamt garnichts.
      Bei mir gibts nur DataAdapter, mit denen man gleich ganze DataTables befüllt, und über die man auch die Insert-, Delete- und Update-Commands laufen lässt - ohne eine Extra-Zeile dafür coden zu müssen.

      Aber da typisierte Datasets und datengebundene Datagridviews sich einiger Unbeliebtheit erfreuen, stelle ich hier wenigstens mal eine Extension-Methode vor, mit der man sich ein parametrisiertes Command erzeugen kann, mit weniger Aufwand, und lesbarer als das String-Gefrickel.

      Die Methode:

      VB.NET-Quellcode

      1. Public Module SqlCeExtensions
      2. ''' <summary> allgemeine Methode, aus einer SqlCeConnection, einem CommandText und optionalen Werten
      3. ''' ein parametrisiertes SqlCeCommand zu erstellen </summary>
      4. ''' <remarks>Vorrausgeetzt ist, dass die übergebenen values Datentypen haben, die zu den in der Datenbank
      5. ''' festgelegten Spalten passen. Eine Überprüfung findet nicht statt.</remarks>
      6. <Runtime.CompilerServices.Extension()> _
      7. Public Function CreateCommandX(ByVal con As SqlCeConnection, ByVal commandText As String, ByVal ParamArray values As Object()) As SqlCeCommand
      8. Dim splits = commandText.Split("?"c) ' Parameter-Platzhalter '?' identifizieren
      9. If splits.Count <> values.Length + 1 Then Throw New ArgumentException( _
      10. "Anzahl der Argumente passt nicht zur Anzahl der Parameter-Platzhalter im CommandText")
      11. Dim cmd = New SqlCeCommand()
      12. ' für jeden Platzhalter einen DbParameter adden, und den Param-Namen an den Platzhalter schreiben
      13. For i = 0 To values.Length - 1
      14. Dim param = cmd.Parameters.AddWithValue("@a" & i, values(i))
      15. splits(i) = splits(i) & param.ParameterName
      16. Next
      17. cmd.CommandText = String.Concat(splits)
      18. cmd.Connection = con
      19. Return cmd
      20. End Function
      21. End Module
      Das ist jetzt die Lösung für SqlCe. Für andere DBProvider muß natürlich jeweils eine annere Extension geschrieben werden, etwa für Access müsste SqlCeConnection, SqlCeCommand ausgetauscht werden durch OleDbConnection, OleDbCommand.

      Also eine string-frickelige Abfrage mag so aussehen:

      VB.NET-Quellcode

      1. 'Der normale NoGo-Progger täte gar Textboxen verwenden (statt hier: DateTimePicker), sodass Angreifer dort ihre Sql-Injection-Angriffe reinschreiben können.
      2. Dim sql = "Select * From [Bestellung] where [Datum] between #" & dtpVon.Value & "# and #" & dtBis.Value & "#"
      3. Dim cmd = New SqlCeCommand(sql, _Con)


      Demgegenüber sähe die Verwendung meiner Extension so aus:

      VB.NET-Quellcode

      1. Dim cmd = _Con.CreateCommandX( _
      2. "Select * From [Bestellung] where [Datum] between ? and ?", dtpVon.Value, dtBis.Value)


      Wie gesagt, ich halte insgesamt nicht viel davon, aber ich habs mirmal gegeben, ein paar ListViews damit zu befüllen:
      viel Code für wenig Funktionalität

      VB.NET-Quellcode

      1. Imports System.Data.SqlServerCe
      2. Public Class frmOhneDataset
      3. Private _Con As New SqlCeConnection(My.Settings.BestellungenConnectionString)
      4. Private Sub frmOhneDataset_Load(ByVal sender As Object, ByVal e As EventArgs) Handles MyBase.Load
      5. Me.AlignOnTop()
      6. End Sub
      7. Private Sub LadeKundenToolStripMenuItem_Click(ByVal sender As Object, ByVal e As EventArgs) Handles LadeKundenToolStripMenuItem.Click, LadeBearbeiterToolStripMenuItem.Click, LadeBestellungenToolStripMenuItem.Click, btLadeGefiltert.Click, ClearAllToolStripMenuItem.Click
      8. Select Case True
      9. Case sender Is LadeKundenToolStripMenuItem
      10. LadeKunden()
      11. Case sender Is LadeBearbeiterToolStripMenuItem
      12. LadeBearbeiter()
      13. Case sender Is LadeBestellungenToolStripMenuItem
      14. LadeBestellungen()
      15. Case sender Is ClearAllToolStripMenuItem
      16. For Each lv In New ListView() {lvKunde, lvBearb, lvBest}
      17. lv.Items.Clear()
      18. Next
      19. Case sender Is btLadeGefiltert
      20. LadeBestellungenGefiltert()
      21. End Select
      22. End Sub
      23. Private Sub LadeKunden()
      24. _Con.Open()
      25. Using cmd = _Con.CreateCommandX("Select * From [Kunde]"), rd = cmd.ExecuteReader
      26. While rd.Read
      27. Dim lvi = lvKunde.Items.Add(rd(0).ToString)
      28. For i = 1 To rd.VisibleFieldCount - 1
      29. lvi.SubItems.Add(rd(i).ToString)
      30. Next
      31. End While
      32. End Using
      33. _Con.Close()
      34. End Sub
      35. Private Sub LadeBearbeiter()
      36. _Con.Open()
      37. Using cmd = _Con.CreateCommandX("Select * From [Bearbeiter]"), rd = cmd.ExecuteReader
      38. While rd.Read
      39. Dim lvi = lvBearb.Items.Add(rd(0).ToString)
      40. For i = 1 To rd.VisibleFieldCount - 1
      41. lvi.SubItems.Add(rd(i).ToString)
      42. Next
      43. End While
      44. End Using
      45. _Con.Close()
      46. End Sub
      47. Private Sub LadeBestellungen()
      48. _Con.Open()
      49. Using cmd = _Con.CreateCommandX("Select * From [Bestellung]"), rd = cmd.ExecuteReader
      50. While rd.Read
      51. Dim lvi = lvBest.Items.Add(rd(0).ToString)
      52. For i = 1 To rd.VisibleFieldCount - 1
      53. lvi.SubItems.Add(rd(i).ToString)
      54. Next
      55. End While
      56. End Using
      57. _Con.Close()
      58. End Sub
      59. Private Sub LadeBestellungenGefiltert()
      60. _Con.Open()
      61. Using cmd = _Con.CreateCommandX( _
      62. "Select * From [Bestellung] where [Datum] between ? and ?", dtpVon.Value, dtBis.Value), _
      63. rd = cmd.ExecuteReader
      64. While rd.Read
      65. Dim lvi = lvBest.Items.Add(rd(0).ToString)
      66. For i = 1 To rd.VisibleFieldCount - 1
      67. lvi.SubItems.Add(rd(i).ToString)
      68. Next
      69. End While
      70. End Using
      71. _Con.Close()
      72. End Sub
      73. Private Sub NoGo()
      74. 'würde vmtl. crashen, weil die Datumse automatisch konvertiert würden, und zwar falsch.
      75. 'Aber normale NoGo-Progger verwenden eh Textboxen, sodass Angreifer dort ihre Sql-Injection-Angriffe reinschreiben können.
      76. Dim sql = "Select * From [Bestellung] where [Datum] between " & dtpVon.Value & " and " & dtBis.Value
      77. Dim cmd = New SqlCeCommand(sql, _Con)
      78. '...
      79. End Sub
      80. Private Sub LadeGefiltert_OhneExtensionMethode()
      81. 'klassische Verwendung von DbParametern: etwas umständlich, weil zw. Command erstellen und Command abfahren noch die Parameter geaddet wern müssen
      82. 'etwas anfällig, weil die Parameternamen zweimal identisch festgelegt werden müssen.
      83. Dim sql = "Select * From [Bestellung] where [Datum] between @von and @bis"
      84. _Con.Open()
      85. Using cmd = New SqlCeCommand(sql, _Con)
      86. 'DBParameter vereiteln Sql-Injection und konvertieren i.a. typ-richtig
      87. cmd.Parameters.AddWithValue("@von", dtpVon.Value)
      88. cmd.Parameters.AddWithValue("@bis", dtBis.Value)
      89. Using rd = cmd.ExecuteReader
      90. While rd.Read
      91. Dim lvi = lvBest.Items.Add(rd(0).ToString)
      92. For i = 1 To rd.VisibleFieldCount - 1
      93. lvi.SubItems.Add(rd(i).ToString)
      94. Next
      95. End While
      96. End Using
      97. End Using
      98. _Con.Close()
      99. End Sub
      100. End Class
      101. Public Module SqlCeExtensions
      102. ''' <summary>
      103. ''' allgemeine Methode, aus einer SqlCeConnection, einem CommandText und optionalen Werten ein parametrisiertes SqlCeCommand zu erstellen
      104. ''' </summary>
      105. <Runtime.CompilerServices.Extension()> _
      106. Public Function CreateCommandX(ByVal con As SqlCeConnection, ByVal commandText As String, ByVal ParamArray values As Object()) As SqlCeCommand
      107. Dim splits = commandText.Split("?"c)
      108. If splits.Count <> values.Length + 1 Then Throw New ArgumentException( _
      109. "Anzahl der Argumente passt nicht zur Anzahl der Parameter-Platzhalter im CommandText")
      110. Dim cmd = New SqlCeCommand()
      111. For i = 0 To values.Length - 1
      112. Dim param = cmd.Parameters.AddWithValue("@a" & i, values(i))
      113. splits(i) = splits(i) & param.ParameterName
      114. Next
      115. cmd.CommandText = String.Concat(splits)
      116. cmd.Connection = con
      117. Return cmd
      118. End Function
      119. End Module


      Hier dieselbe Funktionalität, zuzüglich Ändern und Abspeichern, unter Verwendung der DBExtensions:
      wenig Code für viel Funktionalität

      VB.NET-Quellcode

      1. Imports System.Data.Common
      2. Imports System.Data.SqlServerCe
      3. Public Class frmMainM_N
      4. Private Sub Form_Load(ByVal sender As Object, ByVal e As EventArgs) Handles MyBase.Load
      5. Dim adp = New DatasetAdapter( _
      6. SqlCeProviderFactory.Instance, My.Settings.BestellungenConnectionString, _
      7. ConflictOption.OverwriteChanges)
      8. Me.AlignOnTop.BestellungenDts.Register(Me).Adapter(adp).Fill()
      9. AddHandler Me.FormClosing, BestellungenDts.HandleFormClosing
      10. End Sub
      11. Private Sub MenuItem_Click(ByVal sender As Object, ByVal e As EventArgs) _
      12. Handles ReloadToolStripMenuItem.Click, SaveToolStripMenuItem.Click, btLadeGefiltert.Click
      13. Select Case True
      14. Case sender Is ReloadToolStripMenuItem
      15. BestellungenDts.Fill()
      16. Case sender Is SaveToolStripMenuItem
      17. BestellungenDts.Save(Me)
      18. Case sender Is btLadeGefiltert
      19. BestellungenDts.Bestellung.Fill("where [Datum] between ? and ?", dtpVon.Value, dtBis.Value)
      20. End Select
      21. End Sub
      22. End Class
      Das ist jetzt nur ein Viertel des anderen Codes, kann dafür aber auch Sortieren, Editieren und Abspeichern.
      Auch die Darstellung ist inhaltsreicher, es werden nämlich die Namen der Bearbeiter und Kunden angezeigt, wo eine ListView nur mit ID-Nummern aufwarten kann (oder extra-Aufwand erforderte).
      Vergleiche: mit:
      Die Combo-DropDowns im DGV deuten übrigens an, dass man auch die Bearbeiter und Kunden editieren kann, nämlich indem man das DropDown öffnet, und einen anneren Bearbeiter/Kunden anwählt.

      Zur Solution:
      Habe ich in VB2008 gemacht, und als DB eine SqlCe-Datenbank genommen. Der SqlCe-Provider scheint allmählich Access an allgemeiner Verfügbarkeit zu überholen.
      Im Sample ist auch die neuesete Version der DBExtensions drin - ich bin einfach zu faul, die ganze Funktionalität immer wieder neu "from the Scratch" zu erfinden.

      Anhang:
      Da OleDB als einziges Datenbank-System benannte Parameter im CommandText nicht akzeptiert, muß die CreateConnectionX-Funktion für OleDb etwas abweichend formuliert werden:

      VB.NET-Quellcode

      1. ''' <summary>
      2. ''' allgemeine Methode, aus einer OleDbConnection, einem CommandText und optionalen Werten ein parametrisiertes OleDbCommand zu erstellen
      3. ''' </summary>
      4. ''' <remarks>Vorrausgeetzt ist, dass die übergebenen values Datentypen haben, die zu den in der Datenbank
      5. ''' festgelegten Spalten passen. Eine Überprüfung findet nicht statt.</remarks>
      6. <Runtime.CompilerServices.Extension()> _
      7. Public Function CreateCommandX(ByVal con As OleDb.OleDbConnection, ByVal commandText As String, ByVal ParamArray values As Object()) _
      8. As OleDb.OleDbCommand
      9. If commandText.Split("?"c).Count <> values.Length + 1 Then Throw New ArgumentException( _
      10. "Anzahl der Argumente passt nicht zur Anzahl der Parameter-Platzhalter im CommandText")
      11. CreateCommandX = New OleDb.OleDbCommand() With {.CommandText = commandText, .Connection = con}
      12. For i = 0 To values.Length - 1
      13. CreateCommandX.Parameters.AddWithValue("@a" & i, values(i))
      14. Next
      15. End Function


      Zu Antwort-Posts:
      Ich täte euch bitten, Fragen hierzu im HauptForum zu klären. Diese VBP-Abteilung heißt "Tipps & Tricks und Tutorials", und ist nicht dazu gedacht, spezielle Einzelfall-Probleme zu lösen.
      Dateien
      • SqlCeSample.zip

        (120,6 kB, 311 mal heruntergeladen, zuletzt: )

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

      Mit der Extension Methode mache ich es auch, jedoch halte ich nur das ? als Splitter etwas umständlich. Ich nutze da lieber die Methode ähnlich der String.Format()-Methode, wo man die übergebenen Values mittels {1}, {2} usw. zuweisen kann. Der Vorteil liegt auf der Hand:

      1. Man muss sich nicht an die Reihenfolge der übergebenen Parameter halten
      2. Öfters vorkommende Parameter müssen nur einmal angegeben werden

      VB.NET-Quellcode

      1. Dim param = cmd.Parameters.AddWithValue("@a" & i, values(i))


      Option Strict Off?? Ansonsten müsste doch bei "@a" & i ein Fehler kommen das i nicht in String konvertiert werden kann.

      Dodo schrieb:

      1. Man muss sich nicht an die Reihenfolge der übergebenen Parameter halten

      SOLLTE man aber -> Access interessiert sich nicht für Parameternamen, sondern nur für die Reihenfolge. Wenn man den Code dann zb später mal umstellen will, knallt es evtl. Deswegen lieber in solchen Fällen: kleinster gemeinsamer Nenner ...
      Es geht hierbei um die VB Funktion die btw. noch nichts mit der Datenbank zu tun hat.
      Der CommantText bleibt ja identisch und DORT kann man sich meinetwegen auch an die Reihenfolge halten, mir gehts aber eher an die Reihenfolge der Parameter für die Funktion, beispiel

      VB.NET-Quellcode

      1. Dim cmd = _Con.CreateCommandX("Select t1.* From [Bestellung] AS `t1` LEFT JOIN `tabelle` AS `t2` ON t2.name = ? where [Datum] t1.aId = ? AND t2.bId = ?", "Dodo", 1, 1)


      Hier wird fiktiv einmal 2 mal die ID abgefragt und die 1 muss der Funktion ja nun 2 mal übergeben werden.
      Meine gemeinte Methode

      VB.NET-Quellcode

      1. Dim cmd = _Con.CreateCommandX("Select t1.* From [Bestellung] AS `t1` LEFT JOIN `tabelle` AS `t2` ON t2.name = {1} where [Datum] t1.aId = {0} AND t2.bId = {0}", 1, Dodo)


      Ein weiterer Vorteil, wenn man irgendwo noch ein Parameter einfügen will, müsste man erstmal zählen wieviele ?-Replacer vor meinem eingefügten neuen Parameter sind um dann beim ParamArray an der entsprechenden Stelle auch ein neuen Wert einzufügen. Mit meiner Methode fügt man den neuen Wert einfach ans Ende des ParamArrays und den Replacer mit dem Index, der ja einfach ausfindig zu machen ist an die Stelle im SQL Query.

      Edit: Hier mal meine umgeschriebene Funktion

      VB.NET-Quellcode

      1. Public Module SqlCeExtensions
      2. ''' <summary>
      3. ''' allgemeine Methode, aus einer SqlCeConnection, einem CommandText und optionalen Werten ein parametrisiertes SqlCeCommand zu erstellen
      4. ''' </summary>
      5. <Runtime.CompilerServices.Extension()> _
      6. Public Function CreateCommandX(ByVal con As SqlCeConnection, ByVal commandText As String, ByVal ParamArray values As Object()) As SqlCeCommand
      7. Dim cmd = New SqlCeCommand()
      8. ' für jeden Platzhalter einen DbParameter adden, und den Param-Namen an den Platzhalter schreiben
      9. Dim paramNames(values.Length - 1) As String
      10. For i = 0 To values.Length - 1
      11. ' ParameterNamen setzten
      12. paramNames(i) = "@a" & i
      13. Dim param = cmd.Parameters.AddWithValue(paramNames(i), values(i))
      14. Next
      15. cmd.CommandText = String.Format(commandText, paramNames)
      16. cmd.Connection = con
      17. Return cmd
      18. End Function
      19. End Module


      Rauskommen wird das selbe, nur das die Funktion meiner Meinung nach einfacherer zu Handhaben ist.

      Dieser Beitrag wurde bereits 3 mal editiert, zuletzt von „Dodo“ ()

      Deine Alternative mit der an String.Format angelehnten Syntax (statt meiner Anlehnung an die Access-Sql-Syntax) gefällt mir auch nicht schlecht, nur halt mit Access kann das scheitern, also müssteman für OleDbCommands die Methode anders formulieren.

      Dodo schrieb:

      EDR schrieb:

      VB.NET-Quellcode

      1. Dim param = cmd.Parameters.AddWithValue("@a" & i, values(i))
      Option Strict Off?? Ansonsten müsste doch bei "@a" & i ein Fehler kommen das i nicht in String konvertiert werden kann.
      nee - dassis schön Strict On: der & - Operator arbeitet wie String.Concat(): akzeptiert jedes Objekt und ruft dessen .Tostring() methode auf.
      .ToString() anhängen ist also nicht erforderlich.

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

      Habe die Extension mal fett überarbeitet:

      VB.NET-Quellcode

      1. Imports System.Data.Common
      2. Public Module CommonDataExtensions
      3. ''' <summary> befüllt die DbParameter eines DbCommands mit Werten </summary>
      4. <Runtime.CompilerServices.Extension()> _
      5. Public Function FillParams(ByVal cmdWithParams As DbCommand, ByVal ParamArray values As Object()) As DbCommand
      6. If cmdWithParams.Parameters.Count <> values.Length Then Throw New ArgumentException( _
      7. "Anzahl der Argumente passt nicht zur Anzahl der Parameter")
      8. For i = 0 To values.Length - 1
      9. cmdWithParams.Parameters(i).Value = values(i)
      10. Next
      11. Return cmdWithParams
      12. End Function
      13. ''' <summary> erzeugt ein DBCommand mit DBParametern. '?' im commandText ist Platzhalter für einen DBParameter </summary>
      14. <Runtime.CompilerServices.Extension()> _
      15. Public Function CreateCommandWithParams(ByVal con As DbConnection, ByVal commandText As String) As DbCommand
      16. Dim splits = commandText.Split("?"c)
      17. Dim cmd = con.CreateCommand
      18. For i = 0 To splits.Length - 2
      19. Dim param = cmd.CreateParameter
      20. param.ParameterName = "@a" & i
      21. cmd.Parameters.Add(param)
      22. Next
      23. If TypeOf con Is OleDb.OleDbConnection Then
      24. cmd.CommandText = commandText
      25. Else
      26. For i = 0 To splits.Length - 2
      27. splits(i) = splits(i) & cmd.Parameters(i).ParameterName
      28. Next
      29. cmd.CommandText = String.Concat(splits)
      30. End If
      31. Return cmd
      32. End Function
      33. ''' <summary> erzeugt ein DBCommand inklusive bereits mit Werten beschickter DBParameter. '?' im commandText ist Platzhalter für einen DBParameter </summary>
      34. <Runtime.CompilerServices.Extension()> _
      35. Public Function CreateCompleteCommand(ByVal con As DbConnection, ByVal commandText As String, ByVal ParamArray values As Object()) As DbCommand
      36. Return con.CreateCommandWithParams(commandText).FillParams(values)
      37. End Function
      38. End Module
      Nun ist sie ganz allgemein formuliert für glaub alle DBProvider.
      Ausserdem kann man nun das Erzeugen eines parametrisierten Commands trennen von der Befüllung der Parameter.
      Das ist "sinnvoll", wenn man etwa viele einzelne Insert- oder Update- Statements absetzen möchte, mit demselben CommandText, aber immer unterschiedlichen Argumenten.

      (Sofern man hier ühaupt von "sinnvoll" sprechen kann, denn gleich DataAdapter zu nehmen wäre natürlich ungleich viel sinnvoller)