如何在 Microsoft Access 的不同上下文中使用 VBA 中的参数?

How do I use parameters in VBA in the different contexts in Microsoft Access?

提问人:Erik A 提问时间:3/27/2018 最后编辑:Erik A 更新时间:7/8/2020 访问量:13742

问:

我从 bobby-tables.com 等来源阅读了很多关于SQL注入和使用参数的信息。但是,我正在 Access 中使用一个复杂的应用程序,它有很多动态 SQL,在各种地方都有字符串串联。

它包含以下我想要更改的内容,并添加参数,以避免错误并允许我处理带有单引号的名称,例如 Jack O'Connel。

它使用:

  • DoCmd.RunSQL执行 SQL 命令
  • DAO 记录集
  • ADODB 记录集
  • 表单和报表,以 和 开头,在参数中使用字符串连接DoCmd.OpenFormDoCmd.OpenReportWhereCondition
  • 像这样的域聚合使用字符串连接DLookUp

查询的结构大多如下:

DoCmd.RunSQL "INSERT INTO Table1(Field1) SELECT Field1 FROM Table2 WHERE ID = " & Me.SomeTextbox

对于这些不同类型的查询,我有哪些选项可以使用参数?

这个问题旨在作为一个资源,对于我经常如何使用参数评论各种帖子

SQL VBA MS-ACCESS

评论

3赞 Andre 3/27/2018
这个问题专门关于使用参数,但可能值得注意的是,您可以使用 Gustav 的 CSql() 函数安全地使用字符串连接。
0赞 AllAboutMike 9/7/2023
据我所知,CSql 函数只是帮助您将不同类型的格式化为合适的字符串表示形式。它似乎根本没有防范SQL注入。

答:

32赞 7 revs, 2 users 79%Erik von Asmuth #1

在查询中使用参数的方法有很多种。我将尝试为其中的大多数提供示例,以及它们在哪些地方适用。

首先,我们将讨论 Access 独有的解决方案,例如表单、报表和域聚合。然后,我们将讨论 DAO 和 ADO。


使用窗体和报表中的值作为参数

在 Access 中,可以直接在 SQL 代码中使用窗体和报表上控件的当前值。这限制了对参数的需求。

您可以通过以下方式引用控件:

Forms!MyForm!MyTextbox用于窗体上的简单控件

Forms!MyForm!MySubform.Form!MyTextbox用于子窗体上的控件

Reports!MyReport!MyTextbox用于报表上的控件

示例实现:

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

这可用于以下用途:

使用 时,普通查询(在 GUI 中)、表单和报告记录源、表单和报告过滤器、域聚合和DoCmd.RunSQLDoCmd.OpenFormDoCmd.OpenReport

不适用于以下用途:

使用 DAO 或 ADODB 执行查询时(例如,打开记录集,CurrentDb.Execute)


使用 TempVar 作为参数

Access 中的 TempVar 是全局可用的变量,可以在 VBA 中设置,也可以使用宏进行设置。它们可以重用于多个查询。

示例实现:

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

TempVar 的可用性与表单和报表中的值的可用性相同:不适用于 ADO 和 DAO,可用于其他用途。

我建议 TempVars 在打开窗体或报表时使用参数,而不是引用控件名称,因为如果打开它的对象关闭,TempVars 将保持可用。我建议对每个窗体或报表使用唯一的 TempVar 名称,以避免在刷新窗体或报表时出现奇怪情况。


使用自定义函数 (UDF) 作为参数

与 TempVars 非常相似,可以使用自定义函数和静态变量来存储和检索值。

示例实现:

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

然后:

SetThisDate SomeDateValue ' Will store SomeDateValue in ThisDate.
DoCmd.RunSQL "INSERT INTO Table1(Field1) SELECT Field1 FROM Table2 WHERE [SomeDateField] = GetThisDate()"

此外,还可以创建一个带有可选参数的函数,用于设置和获取私有静态变量的值:

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

要设置值,请执行以下操作:

ThisValue "Some text value"

要获取该值,请执行以下操作:

CurrentValue = ThisValue

在查询中:

ThisValue "SomeText"  ' Set value to filter on.
DoCmd.RunSQL "INSERT INTO Table1(Field1) SELECT Field1 FROM Table2 WHERE [SomeField] = ThisValue()"

使用 DoCmd.SetParameter

的用途相当有限,所以我会很简短。它允许您设置用于 和 其他一些语句的参数,但它不适用于 、过滤器、DAO 和 ADO。DoCmd.SetParameterDoCmd.OpenFormDoCmd.OpenReportDoCmdDoCmd.RunSQL

示例实现

DoCmd.SetParameter "MyParameter", Me.MyTextbox
DoCmd.OpenForm "MyForm",,, "ID = MyParameter"

使用 DAO

在 DAO 中,我们可以使用该对象创建查询、设置参数,然后打开记录集或执行查询。首先设置查询的 SQL,然后使用集合设置参数。DAO.QueryDefQueryDef.Parameters

在我的示例中,我将使用隐式参数类型。如果要显式使用它们,请向查询添加 PARAMETERS 声明

示例实现

'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

虽然这仅在 DAO 中可用,但您可以为 DAO 记录集设置许多内容,以使它们使用参数,例如表单记录集、列表框记录集和组合框记录集。但是,由于 Access 在排序和筛选时使用文本而不是记录集,因此如果这样做,这些内容可能会有问题。


使用 ADO

可以通过对象在 ADO 中使用参数。用于创建参数,然后将其追加到集合中。ADODB.CommandCommand.CreateParameterCommand.Parameters

可以使用 ADO 中的集合显式声明参数,或将参数数组传递给方法以隐式传递参数。.ParametersCommand.Execute

ADO 不支持命名参数。虽然可以传递名称,但不会对其进行处理。

示例实现:

'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

与打开 DAO 记录集相同的限制适用。虽然这种方式仅限于执行查询和打开记录集,但您可以在应用程序中的其他位置使用这些记录集。

评论

1赞 Gustav 3/29/2018
不错的综述。但我认为你错过了我最喜欢的方法:使用自定义函数的选项,该函数返回您使用同一函数或另一个(子)函数设置的静态变量的值。很像 TempVars。
0赞 Erik A 3/29/2018
@Gustav 随意编辑它。我把它标记为一个社区维基,所以每个人都可以做出贡献,没有人会获得代表。我没有经常使用这种技术,所以你可能比我更了解细节。
0赞 Gustav 3/30/2018
不,这对我来说是新的。但是我已经插入了该部分。
0赞 iDevlop 9/5/2018
即使在“纯 Access”上下文中,如果链接了指向 Oracle 或 SQL Server 的表,其中一些方法也会对性能产生巨大的负面影响。更多:对于 Oracle,在使用 Oracle 的 ODBC 驱动程序时,在查询中引用表单字段根本不起作用,而在使用 Microsoft 的 Oracle 驱动程序时可能有效。
1赞 Erik A 1/22/2020
我从未尝试过,但我不这么认为。Afaik DAO 直通查询按原样传递给服务器,没有参数解析(实际上,根本没有在 Access/DAO 端解析)。请考虑改用 ADO。如果您有特定问题,这可能是一个很好的单独问题
0赞 FolkCoder 3/30/2018 #2

我构建了一个相当基本的查询生成器类,以解决字符串连接的混乱问题,并处理命名参数的缺失。创建查询相当简单。

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

ToString() 方法的输出如下所示:

SELECT * FROM tblEmployees WHERE 1 = 1 AND (StartDate > #3/29/2018# OR StatusChangeDate > #3/29/2018#) AND (StatusIndicator IN ('A', 'L') OR Grade > 10) AND (Salary > 9999.99) AND (Retired = False) ORDER BY ID ASC;

每个谓词都包装在 parens 中以处理链接的 AND/OR 子句,并且具有相同名称的参数只需声明一次。完整的代码在我的 github 上,并在下面复制。我还有一个使用 ADODB 参数的 Oracle 直通查询版本。最后,我想将两者都包装在 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赞 Erik A 3/30/2018
此类不能解决字符串串联的许多问题。仍然存在的问题包括:对 null 值的不正确处理、对带引号的字符串的错误处理以及对具有 dd-mm-yyyy 格式的区域设置中的日期的错误处理。此外,如果存在名称重叠的参数,则使用会导致问题,例如 和。虽然我很欣赏这种努力,但在目前的形式下,我更喜欢任何一天的真实参数。此外,DAO 还支持命名参数。不过,我很欣赏这种努力。您可以在 Code Review 上查看此类课程。Replace@a@age