J'ai beaucoup lu sur l'injection SQL et sur l'utilisation de paramètres provenant de sources telles que bobby-tables.com . Cependant, je travaille avec une application complexe dans Access, qui a beaucoup de SQL dynamique avec concaténation de chaînes dans toutes sortes d'endroits.
Je souhaite modifier les paramètres suivants et y ajouter des paramètres pour éviter les erreurs et me permettre de gérer les noms avec des guillemets simples, comme Jack O'Connel.
Il utilise:
DoCmd.RunSQL
pour exécuter des commandes SQLDoCmd.OpenForm
et DoCmd.OpenReport
, à l'aide de la concaténation de chaînes dans l'argument WhereCondition
DLookUp
qui utilisent la concaténation de chaînesLes requêtes sont généralement structurées comme suit:
DoCmd.RunSQL "INSERT INTO Table1(Field1) SELECT Field1 FROM Table2 WHERE ID = " & Me.SomeTextbox
Quelles sont mes options pour utiliser des paramètres pour ces différents types de requêtes?
Cette question est conçue comme une ressource, pour les fréquents comment utiliser les paramètres commenter sur divers posts
Il existe de nombreuses façons d’utiliser des paramètres dans les requêtes. Je vais essayer de donner des exemples pour la plupart d’entre eux et leur application.
Dans un premier temps, nous aborderons les solutions propres à Access, telles que les formulaires, les rapports et les agrégats de domaine. Ensuite, nous parlerons de DAO et ADO.
Dans Access, vous pouvez directement utiliser la valeur actuelle des contrôles sur les formulaires et les rapports dans votre code SQL. Cela limite le besoin de paramètres.
Vous pouvez vous référer aux contrôles de la manière suivante:
Forms!MyForm!MyTextbox
pour un contrôle simple sur un formulaire
Forms!MyForm!MySubform.Form!MyTextbox
pour un contrôle sur un sous-formulaire
Reports!MyReport!MyTextbox
pour un contrôle sur un rapport
Exemple d'implémentation:
DoCmd.RunSQL "INSERT INTO Table1(Field1) SELECT Forms!MyForm!MyTextbox" 'Inserts a single value
DoCmd.RunSQL "INSERT INTO Table1(Field1) SELECT Field1 FROM Table2 WHERE ID = Forms!MyForm!MyTextbox" 'Inserts from a different table
Ceci est disponible pour les utilisations suivantes:
Lorsque vous utilisez DoCmd.RunSQL
, les requêtes normales (dans l'interface graphique), les sources d'enregistrement de formulaire et de rapport, les filtres de formulaire et de rapport, les agrégats de domaine, DoCmd.OpenForm
et DoCmd.OpenReport
Ceci est pas disponible pour les utilisations suivantes:
Lors de l'exécution de requêtes à l'aide de DAO ou ADODB (par exemple, ouvrir des jeux d'enregistrements, CurrentDb.Execute
)
Les TempVars dans Access sont des variables disponibles globalement, pouvant être définies dans VBA ou à l'aide de macros. Ils peuvent être réutilisés pour plusieurs requêtes.
Exemple d'implémentation:
TempVars!MyTempVar = Me.MyTextbox.Value 'Note: .Value is required
DoCmd.RunSQL "INSERT INTO Table1(Field1) SELECT Field1 FROM Table2 WHERE ID = TempVars!MyTempVar"
TempVars.Remove "MyTempVar" 'Unset TempVar when you're done using it
La disponibilité de TempVars est identique à celle des valeurs des formulaires et des états: indisponible pour ADO et DAO, disponible pour d'autres utilisations.
Je recommande TempVars pour l'utilisation de paramètres lors de l'ouverture de formulaires ou de rapports plutôt que pour les noms de contrôle, car si l'objet qui l'ouvre se ferme, les TempVars restent disponibles. Je recommande d'utiliser des noms TempVar uniques pour chaque formulaire ou rapport afin d'éviter toute bizarrerie lors de l'actualisation de formulaires ou de rapports.
Tout comme TempVars, vous pouvez utiliser une fonction personnalisée et des variables statiques pour stocker et récupérer des valeurs.
Exemple d'implémentation:
Option Compare Database
Option Explicit
Private ThisDate As Date
Public Function GetThisDate() As Date
If ThisDate = #12:00:00 AM# Then
' Set default value.
ThisDate = Date
End If
GetThisDate = ThisDate
End Function
Public Function SetThisDate(ByVal NewDate As Date) As Date
ThisDate = NewDate
SetThisDate = ThisDate
End Function
et alors:
SetThisDate SomeDateValue ' Will store SomeDateValue in ThisDate.
DoCmd.RunSQL "INSERT INTO Table1(Field1) SELECT Field1 FROM Table2 WHERE [SomeDateField] = GetThisDate()"
De plus, une fonction unique avec un paramètre facultatif peut être créée pour définir et obtenir la valeur d'une variable statique privée:
Public Function ThisValue(Optional ByVal Value As Variant) As Variant
Static CurrentValue As Variant
' Define default return value.
Const DefaultValue As Variant = Null
If Not IsMissing(Value) Then
' Set value.
CurrentValue = Value
ElseIf IsEmpty(CurrentValue) Then
' Set default value
CurrentValue = DefaultValue
End If
' Return value.
ThisValue = CurrentValue
End Function
Pour définir une valeur:
ThisValue "Some text value"
Pour obtenir la valeur:
CurrentValue = ThisValue
Dans une requête:
ThisValue "SomeText" ' Set value to filter on.
DoCmd.RunSQL "INSERT INTO Table1(Field1) SELECT Field1 FROM Table2 WHERE [SomeField] = ThisValue()"
Les utilisations de DoCmd.SetParameter
étant plutôt limitées, je serai bref. Il vous permet de définir un paramètre à utiliser dans DoCmd.OpenForm
, DoCmd.OpenReport
et quelques autres instructions DoCmd
, mais cela ne fonctionne pas avec DoCmd.RunSQL
, filtres, DAO et ADO.
Exemple d'implémentation
DoCmd.SetParameter "MyParameter", Me.MyTextbox
DoCmd.OpenForm "MyForm",,, "ID = MyParameter"
Dans DAO, nous pouvons utiliser l'objet DAO.QueryDef
pour créer une requête, définir des paramètres, puis ouvrir un jeu d'enregistrements ou exécuter la requête. Vous définissez d'abord le SQL des requêtes, puis utilisez la collection QueryDef.Parameters
pour définir les paramètres.
Dans mon exemple, je vais utiliser des types de paramètres implicites. Si vous souhaitez les rendre explicites, ajoutez une déclaration PARAMETERS
à votre requête.
Exemple d'implémentation
'Execute query, unnamed parameters
With CurrentDb.CreateQueryDef("", "INSERT INTO Table1(Field1) SELECT Field1 FROM Table2 WHERE Field1 = ?p1 And Field2 = ?p2")
.Parameters(0) = Me.Field1
.Parameters(1) = Me.Field2
.Execute
End With
'Open recordset, named parameters
Dim rs As DAO.Recordset
With CurrentDb.CreateQueryDef("", "SELECT Field1 FROM Table2 WHERE Field1 = FirstParameter And Field2 = SecondParameter")
.Parameters!FirstParameter = Me.Field1 'Bang notation
.Parameters("SecondParameter").Value = Me.Field2 'More explicit notation
Set rs = .OpenRecordset
End With
Bien que cette option ne soit disponible que dans DAO, vous pouvez définir de nombreux éléments sur les jeux d'enregistrements DAO pour qu'ils utilisent des paramètres tels que les jeux d'enregistrements de formulaire, les jeux d'enregistrements de zone de liste et les jeux d'enregistrements de zone de liste déroulante. Toutefois, dans la mesure où Access utilise le texte, et non le jeu d'enregistrements, lors du tri et du filtrage, ces opérations peuvent s'avérer problématiques.
Vous pouvez utiliser des paramètres dans ADO à l'aide de l'objet ADODB.Command
. Utilisez Command.CreateParameter
pour créer des paramètres, puis ajoutez-les à la collection Command.Parameters
.
Vous pouvez utiliser la collection .Parameters
dans ADO pour déclarer explicitement des paramètres, ou transmettre un tableau de paramètres à la méthode Command.Execute
pour transmettre implicitement des paramètres.
ADO ne prend pas en charge les paramètres nommés. Bien que vous puissiez passer un nom, il n'est pas traité.
Exemple d'implémentation:
'Execute query, unnamed parameters
Dim cmd As ADODB.Command
Set cmd = New ADODB.Command
With cmd
Set .ActiveConnection = CurrentProject.Connection 'Use a connection to the current database
.CommandText = "INSERT INTO Table1(Field1) SELECT Field1 FROM Table2 WHERE Field1 = ? And Field2 = ?"
.Parameters.Append .CreateParameter(, adVarWChar, adParamInput, Len(Me.Field1), Me.Field1) 'adVarWChar for text boxes that may contain unicode
.Parameters.Append .CreateParameter(, adInteger, adParamInput, 8, Me.Field2) 'adInteger for whole numbers (long or integer)
.Execute
End With
'Open recordset, implicit parameters
Dim rs As ADODB.Recordset
Dim cmd As ADODB.Command
Set cmd = New ADODB.Command
With cmd
Set .ActiveConnection = CurrentProject.Connection 'Use a connection to the current database
.CommandText = "SELECT Field1 FROM Table2 WHERE Field1 = @FirstParameter And Field2 = @SecondParameter"
Set rs = .Execute(,Array(Me.Field1, Me.Field2))
End With
Les mêmes limitations que celles applicables aux jeux d’enregistrement DAO s’appliquent. Bien que cette méthode soit limitée à l'exécution de requêtes et à l'ouverture de jeux d'enregistrements, vous pouvez utiliser ces jeux ailleurs dans votre application.
J'ai construit une classe de constructeur de requêtes assez basique pour contourner le désordre de la concaténation de chaînes et pour gérer l'absence de paramètres nommés. Créer une requête est assez simple.
Public Function GetQuery() As String
With New MSAccessQueryBuilder
.QueryBody = "SELECT * FROM tblEmployees"
.AddPredicate "StartDate > @StartDate OR StatusChangeDate > @StartDate"
.AddPredicate "StatusIndicator IN (@Active, @LeaveOfAbsence) OR Grade > @Grade"
.AddPredicate "Salary > @SalaryThreshhold"
.AddPredicate "Retired = @IsRetired"
.AddStringParameter "Active", "A"
.AddLongParameter "Grade", 10
.AddBooleanParameter "IsRetired", False
.AddStringParameter "LeaveOfAbsence", "L"
.AddCurrencyParameter "SalaryThreshhold", 9999.99@
.AddDateParameter "StartDate", #3/29/2018#
.QueryFooter = "ORDER BY ID ASC"
GetQuery = .ToString
End With
End Function
La sortie de la méthode ToString () ressemble à ceci:
SELECT * FROM tblEmployees WHERE 1 = 1 AND (StartDate> # 3/29/2018 # OR StatusChangeDate> # 3/29/2018 #) AND (StatusIndicator IN ('A', 'L') OR Grade> 10) ET (Salaire> 9999,99) ET (Retraité = Faux) ORDER BY ID ASC;
Chaque prédicat est encapsulé dans des parenthèses pour gérer les clauses AND/OR liées, et les paramètres portant le même nom ne doivent être déclarés qu'une seule fois. Le code complet se trouve sur mon github et est reproduit ci-dessous. J'ai également un version for Oracle passthrough qui utilise les paramètres ADODB. Pour finir, j'aimerais intégrer les deux dans une interface IQueryBuilder.
VERSION 1.0 CLASS
BEGIN
MultiUse = -1 'True
END
Attribute VB_Name = "MSAccessQueryBuilder"
Attribute VB_GlobalNameSpace = False
Attribute VB_Creatable = True
Attribute VB_PredeclaredId = False
Attribute VB_Exposed = True
'@Folder("VBALibrary.Data")
'@Description("Provides tools to construct Microsoft Access SQL statements containing predicates and parameters.")
Option Explicit
Private Const mlngErrorNumber As Long = vbObjectError + 513
Private Const mstrClassName As String = "MSAccessQueryBuilder"
Private Const mstrParameterExistsErrorMessage As String = "A parameter with this name has already been added to the Parameters dictionary."
Private Type TSqlBuilder
QueryBody As String
QueryFooter As String
End Type
Private mobjParameters As Object
Private mobjPredicates As Collection
Private this As TSqlBuilder
' =============================================================================
' CONSTRUCTOR / DESTRUCTOR
' =============================================================================
Private Sub Class_Initialize()
Set mobjParameters = CreateObject("Scripting.Dictionary")
Set mobjPredicates = New Collection
End Sub
' =============================================================================
' PROPERTIES
' =============================================================================
'@Description("Gets or sets the query statement (SELECT, INSERT, UPDATE, DELETE), exclusive of any predicates.")
Public Property Get QueryBody() As String
QueryBody = this.QueryBody
End Property
Public Property Let QueryBody(ByVal Value As String)
this.QueryBody = Value
End Property
'@Description("Gets or sets post-predicate query statements (e.g., GROUP BY, ORDER BY).")
Public Property Get QueryFooter() As String
QueryFooter = this.QueryFooter
End Property
Public Property Let QueryFooter(ByVal Value As String)
this.QueryFooter = Value
End Property
' =============================================================================
' PUBLIC METHODS
' =============================================================================
'@Description("Maps a boolean parameter and its value to the query builder.")
'@Param("strName: The parameter's name.")
'@Param("blnValue: The parameter's value.")
Public Sub AddBooleanParameter(ByVal strName As String, ByVal blnValue As Boolean)
If mobjParameters.Exists(strName) Then
Err.Raise mlngErrorNumber, mstrClassName & ".AddBooleanParameter", mstrParameterExistsErrorMessage
Else
mobjParameters.Add strName, CStr(blnValue)
End If
End Sub
' =============================================================================
'@Description("Maps a currency parameter and its value to the query builder.")
'@Param("strName: The parameter's name.")
'@Param("curValue: The parameter's value.")
Public Sub AddCurrencyParameter(ByVal strName As String, ByVal curValue As Currency)
If mobjParameters.Exists(strName) Then
Err.Raise mlngErrorNumber, mstrClassName & ".AddCurrencyParameter", mstrParameterExistsErrorMessage
Else
mobjParameters.Add strName, CStr(curValue)
End If
End Sub
' =============================================================================
'@Description("Maps a date parameter and its value to the query builder.")
'@Param("strName: The parameter's name.")
'@Param("dtmValue: The parameter's value.")
Public Sub AddDateParameter(ByVal strName As String, ByVal dtmValue As Date)
If mobjParameters.Exists(strName) Then
Err.Raise mlngErrorNumber, mstrClassName & ".AddDateParameter", mstrParameterExistsErrorMessage
Else
mobjParameters.Add strName, "#" & CStr(dtmValue) & "#"
End If
End Sub
' =============================================================================
'@Description("Maps a long parameter and its value to the query builder.")
'@Param("strName: The parameter's name.")
'@Param("lngValue: The parameter's value.")
Public Sub AddLongParameter(ByVal strName As String, ByVal lngValue As Long)
If mobjParameters.Exists(strName) Then
Err.Raise mlngErrorNumber, mstrClassName & ".AddNumericParameter", mstrParameterExistsErrorMessage
Else
mobjParameters.Add strName, CStr(lngValue)
End If
End Sub
' =============================================================================
'@Description("Adds a predicate to the query's WHERE criteria.")
'@Param("strPredicate: The predicate text to be added.")
Public Sub AddPredicate(ByVal strPredicate As String)
mobjPredicates.Add "(" & strPredicate & ")"
End Sub
' =============================================================================
'@Description("Maps a string parameter and its value to the query builder.")
'@Param("strName: The parameter's name.")
'@Param("strValue: The parameter's value.")
Public Sub AddStringParameter(ByVal strName As String, ByVal strValue As String)
If mobjParameters.Exists(strName) Then
Err.Raise mlngErrorNumber, mstrClassName & ".AddStringParameter", mstrParameterExistsErrorMessage
Else
mobjParameters.Add strName, "'" & strValue & "'"
End If
End Sub
' =============================================================================
'@Description("Parses the query, its predicates, and any parameter values, and outputs an SQL statement.")
'@Returns("A string containing the parsed query.")
Public Function ToString() As String
Dim strPredicatesWithValues As String
Const strErrorSource As String = "QueryBuilder.ToString"
If this.QueryBody = vbNullString Then
Err.Raise mlngErrorNumber, strErrorSource, "No query body is currently defined. Unable to build valid SQL."
End If
ToString = this.QueryBody
strPredicatesWithValues = ReplaceParametersWithValues(GetPredicatesText)
EnsureParametersHaveValues strPredicatesWithValues
If Not strPredicatesWithValues = vbNullString Then
ToString = ToString & " " & strPredicatesWithValues
End If
If Not this.QueryFooter = vbNullString Then
ToString = ToString & " " & this.QueryFooter & ";"
End If
End Function
' =============================================================================
' PRIVATE METHODS
' =============================================================================
'@Description("Ensures that all parameters defined in the query have been provided a value.")
'@Param("strQueryText: The query text to verify.")
Private Sub EnsureParametersHaveValues(ByVal strQueryText As String)
Dim strUnmatchedParameter As String
Dim lngMatchedPoisition As Long
Dim lngWordEndPosition As Long
Const strProcedureName As String = "EnsureParametersHaveValues"
lngMatchedPoisition = InStr(1, strQueryText, "@", vbTextCompare)
If lngMatchedPoisition <> 0 Then
lngWordEndPosition = InStr(lngMatchedPoisition, strQueryText, Space$(1), vbTextCompare)
strUnmatchedParameter = Mid$(strQueryText, lngMatchedPoisition, lngWordEndPosition - lngMatchedPoisition)
End If
If Not strUnmatchedParameter = vbNullString Then
Err.Raise mlngErrorNumber, mstrClassName & "." & strProcedureName, "Parameter " & strUnmatchedParameter & " has not been provided a value."
End If
End Sub
' =============================================================================
'@Description("Combines each predicate in the predicates collection into a single string statement.")
'@Returns("A string containing the text of all predicates added to the query builder.")
Private Function GetPredicatesText() As String
Dim strPredicates As String
Dim vntPredicate As Variant
If mobjPredicates.Count > 0 Then
strPredicates = "WHERE 1 = 1"
For Each vntPredicate In mobjPredicates
strPredicates = strPredicates & " AND " & CStr(vntPredicate)
Next vntPredicate
End If
GetPredicatesText = strPredicates
End Function
' =============================================================================
'@Description("Replaces parameters in the predicates statements with their provided values.")
'@Param("strPredicates: The text of the query's predicates.")
'@Returns("A string containing the predicates text with its parameters replaces by their provided values.")
Private Function ReplaceParametersWithValues(ByVal strPredicates As String) As String
Dim vntKey As Variant
Dim strParameterName As String
Dim strParameterValue As String
Dim strPredicatesWithValues As String
Const strProcedureName As String = "ReplaceParametersWithValues"
strPredicatesWithValues = strPredicates
For Each vntKey In mobjParameters.Keys
strParameterName = CStr(vntKey)
strParameterValue = CStr(mobjParameters(vntKey))
If InStr(1, strPredicatesWithValues, "@" & strParameterName, vbTextCompare) = 0 Then
Err.Raise mlngErrorNumber, mstrClassName & "." & strProcedureName, "Parameter " & strParameterName & " was not found in the query."
Else
strPredicatesWithValues = Replace(strPredicatesWithValues, "@" & strParameterName, strParameterValue, 1, -1, vbTextCompare)
End If
Next vntKey
ReplaceParametersWithValues = strPredicatesWithValues
End Function
' =============================================================================