web-dev-qa-db-fra.com

Comment utiliser les paramètres dans VBA dans les différents contextes de Microsoft Access?

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 SQL
  • Jeux d'enregistrements DAO
  • Jeux d'enregistrements ADODB
  • Formulaires et rapports, ouverts avec DoCmd.OpenForm et DoCmd.OpenReport, à l'aide de la concaténation de chaînes dans l'argument WhereCondition
  • Des agrégats de domaine tels que DLookUp qui utilisent la concaténation de chaînes

Les 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

9
Erik A

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.


Utilisation de valeurs de formulaires et de rapports en tant que paramètres

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)


Utiliser TempVars comme paramètres

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. 


Utilisation de fonctions personnalisées (UDF) en tant que paramètres

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()"

Utilisation de DoCmd.SetParameter

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"

Utiliser DAO

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.


Utiliser ADO

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.

20
Erik A

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

' =============================================================================
1
FolkCoder