【问题标题】:Building SQL strings in Access/VBA在 Access/VBA 中构建 SQL 字符串
【发布时间】:2010-12-20 14:27:00
【问题描述】:

有时,我不得不在 VBA 中构建一个 SQL 字符串并使用Docmd.RunSql() 执行它。我总是通过将变量连接到字符串中来构建这些字符串,例如:

Dim mysqlstring as String
mysqlstring = "INSERT INTO MyTable (Field1, Field2, Field3 ...) VALUES ("
mysqlstring = mysqlstring + Me.TextMyField1 + ", " 'parameter comments
mysqlstring = mysqlstring + Me.TextMyField2 + ", " 
mysqlstring = mysqlstring + Me.TextMyField3 + ", " 
...
mysqlstring = mysqlstring + ");"
Docmd.RunSql mysqlstring

VBA 似乎没有一元连接运算符(如 +=),虽然这看起来不太理想,但至少我可以评论我的每个参数并独立更改它们。它比一个怪物级联字符串更容易阅读和更改。但它似乎仍然是一种构建 SQL 字符串的糟糕方法。我有一个有大约 50 个参数在工作,所以有 50 行 mysqlstring = mysqlstring +...。不可爱。

顺便说一句,这排除了使用行继续来格式化字符串,因为有一个limit on the number of line-continuations you can use on a single string(提示:小于 50)。此外,VBA 不允许您在续行之后添加注释,grr!

直到最近,我还认为这是构建这些字符串的唯一方法。但最近我看到了一种不同的模式,将参数注入到我发布答案的字符串中,如this question (VB.NET),并想知道是否有一个等效于 VBA 的Parameters.AddWithValue(),或者这是否比字符串连接方法。所以我认为这值得提出自己的问题。也许我在这里缺少一些东西。

能否请一些 Access 专家阐明在 Access/VBA 中构建 SQL 字符串的最佳实践是什么?

【问题讨论】:

    标签: sql ms-access vba


    【解决方案1】:

    我有一个时间表应用程序,其中包含相当复杂的非绑定劳务交易输入表。里面有很多数据验证、费率计算等代码。我决定使用以下内容来创建我的 SQL 插入/更新字段。

    变量strSQLInsert、strSQLValues、strSQLUpdate是表单级字符串。

    以下多行:

    Call CreateSQLString("[transJobCategoryBillingTypesID]", lngJobCategoryBillingTypesID)
    

    接着是:

    If lngTransID = 0 Then
        strSQL = "INSERT into Transactions (" & Mid(strSQLInsert, 3) & ") VALUES (" & Mid(strSQLValues, 3) & ")"
    Else
        strSQL = "UPDATE Transactions SET " & Mid(strSQLUpdate, 3) & " WHERE transID=" & lngTransID & ";"
    End If
    
    conn.Open
    conn.Execute strSQL, lngRecordsAffected, adCmdText
    

    请注意,中间线删除了前导“,”。 lngTrans 是 autonumber primamy kay 的值。

    Sub CreateSQLString(strFieldName As String, varFieldValue As Variant, Optional blnZeroAsNull As Boolean)
    '    Call CreateSQLString("[<fieldName>]", <fieldValue>)
    
    Dim strFieldValue As String, OutputValue As Variant
    
        On Error GoTo tagError
    
        ' if 0 (zero) is supposed to be null
        If Not IsMissing(blnZeroAsNull) And blnZeroAsNull = True And varFieldValue = 0 Then
            OutputValue = "Null"
        ' if field is null, zero length or ''
        ElseIf IsNull(varFieldValue) Or Len(varFieldValue) = 0 Or varFieldValue = "''" Then
            OutputValue = "Null"
        Else
            OutputValue = varFieldValue
        End If
    
        ' Note that both Insert and update strings are updated as we may need the insert logic for inserting
        '    missing auto generated transactions when updating the main transaction
        ' This is an insert
        strSQLInsert = strSQLInsert & ", " & strFieldName
        strSQLValues = strSQLValues & ", " & OutputValue
        ' This is an update
        strSQLUpdate = strSQLUpdate & ", " & strFieldName & " = " & OutputValue
    
        On Error GoTo 0
        Exit Sub
    
    tagError:
    
        MsgBox "Error " & Err.Number & " (" & Err.Description & ") in procedure CreateSQLString of VBA Document Form_LabourEntry"
        Exit Sub
    End Sub
    

    我看到其他发帖人都在使用 Execute 方法。 DoCmd.RunSQL 的问题在于它可以忽略错误。以下任何一项都将显示查询收到的任何错误消息。如果使用 DAO,请使用 Currentdb.Execute strSQL,dbfailonerror.. 对于 ADO,请使用 CurrentProject.Connection.Execute strCommand, lngRecordsAffected, adCmdText 然后您可以删除 docmd.setwarnings 行。

    如果您要使用 docmd.setwarnings,请确保您也将 True 语句放入任何错误处理代码中。否则以后可能会发生奇怪的事情,尤其是在您使用该应用程序时。例如,如果您关闭对象,您将不再收到“您希望保存更改”消息。这可能意味着不需要的更改、删除或添加将保存到您的 MDB。

    这两种方法的性能也可能有显着差异。一篇帖子称 currentdb.execute 需要两秒钟,而 docmd.runsql 需要八秒钟。一如既往的 YMMV。

    【讨论】:

    • 如果使用 CurrentDB,而不是将 CurrentDB 分配给变量,则无法返回受影响的记录。
    • 确实如此,我一直忘记那个细节。我有一段代码可以处理这种情况。
    • 而且您不能使用 SELECT @IDENTITY 获取最后插入的自动编号 PK。
    【解决方案2】:

    除了@astander 所说的,您可以创建一个 querydef(带参数)并将其保存为数据库的一部分。

    例如

    Parameters dtBegin DateTime, dtEnd DateTime;
    INSERT into myTable (datebegin, dateend) values (dtBegin, dtEnd)
    

    假设你用名字myTableInsert保存它,你可以编写如下代码

    dim qd as QueryDef
    set qd = CurrentDB.QueryDefs("myTableInsert")
    qd.Parameters("dtBegin").Value = myTextFieldHavingBeginDate
    qd.Parameters("dtEnd").Value = myTextFieldHavingEndDate    
    qd.Execute
    

    注意:我没有测试过这段代码。但是,我猜应该是这样。
    希望这可以为您提供足够的信息以开始使用。

    【讨论】:

    • 有趣。保存的 querydef myTableInsert 会显示在查询列表中吗?
    • 是的。您必须使用带有 PARAMETERS 子句的 INSERT 查询来创建它,如上所示。
    • 正如我在帖子中所说,您不必创建查询,只需使用 CreateQueryDef 的 sql 参数即可创建临时查询。有关详细信息,请参阅提供的链接。
    • @Remou:我不知道,这和在代码中写SQL有什么不同。使用现有的 querydef,您的 SQL 是存储在数据库中的查询的一部分。我喜欢你的例子,但它仍然需要用 VB 代码编写一个大查询,我认为这是不需要的。我希望你读到这个:)
    【解决方案3】:
        Private Sub Command0_Click()
    Dim rec As Recordset2
    Dim sql As String
    Dim queryD As QueryDef
    
        'create a temp query def.
        Set queryD = CurrentDb.CreateQueryDef("", "SELECT * FROM [Table] WHERE Val = @Val")
        'set param vals
        queryD.Parameters("@Val").Value = "T"
        'execute query def
        Set rec = queryD.OpenRecordset
    End Sub
    

    【讨论】:

      【解决方案4】:

      正如其他人所说,首先使用参数可能会更好。不过……

      我也错过了连接运算符,已经习惯了 PHP 中的 .=。在少数情况下,我编写了一个函数来执行此操作,但并不特定于连接 SQL 字符串。这是我用于为 HTTP GET 创建查询字符串的代码:

        Public Sub AppendQueryString(strInput As String, _
             ByVal strAppend As String, Optional ByVal strOperator As String = "&")
          strAppend = StringReplace(strAppend, "&", "&amp;")
          strInput = strInput & strOperator & strAppend
        End Sub
      

      还有一个我称之为的例子:

        AppendQueryString strOutput, "InventoryID=" & frm!InventoryID, vbNullstring
        AppendQueryString strOutput, "Author=" & URLEncode(frm!Author)
      

      ...等等。

      现在,为了构建 SQL WHERE 子句,您可以考虑将类似的东西作为 Application.BuildCriteria 的包装器:

        Public Sub ConcatenateWhere(ByRef strWhere As String, _
            strField As String, intDataType As Integer, ByVal varValue As Variant)
          If Len(strWhere) > 0 Then
             strWhere = strWhere & " AND "
          End If
          strWhere = strWhere & Application.BuildCriteria(strField, _
             intDataType, varValue)
        End Sub
      

      你可以这样称呼它:

        Dim strWhere As String
      
        ConcatenateWhere strWhere,"tblInventory.InventoryID", dbLong, 10036
        ConcatenateWhere strWhere,"tblInventory.OtherAuthors", dbText, "*Einstein*"
        Debug.Print strWhere
        strSQL = "SELECT tblInventory.* FROM tblInventory"
        strSQL = strSQL & " WHERE " & strWhere
      

      ...Debug.Print 会输出这个字符串:

        tblInventory.InventoryID=10036 AND tblInventory.OtherAuthors Like "*Einstein*"
      

      它的变体可能对您更有用,即,您可能希望有一个可选的串联运算符(因此您可以有 OR),但我可能会通过构建一系列 WHERE 字符串并将它们与在代码中逐行进行 OR,因为您可能需要仔细放置括号以确保正确执行 AND/OR 优先级。

      现在,这些都没有真正解决 INSERT 语句的 VALUES 连接问题,但我质疑您在 Access 应用程序中实际插入文字值的频率。除非您使用未绑定的表单来插入记录,否则您将使用表单来插入记录,因此根本不需要 SQL 语句。因此,对于 VALUES 子句,似乎在 Access 应用程序中您不应该经常需要它。如果您发现自己需要编写这样的 VALUES 子句,我建议您没有正确使用 Access。

      也就是说,你可以使用这样的东西:

        Public Sub ConcatenateValues(ByRef strValues As String, _
            intDatatype As Integer, varValue As Variant)
          Dim strValue As String
      
          If Len(strValues) > 0 Then
             strValues = strValues & ", "
          End If
          Select Case intDatatype
            Case dbChar, dbMemo, dbText
              ' you might want to change this to escape internal double/single quotes
              strValue = Chr(34) & varValue & Chr(34)
            Case dbDate, dbTime
              strValue = "#" & varValue & "#"
            Case dbGUID
              ' this is only a guess
              strValues = Chr(34) & StringFromGUID(varValue) & Chr(34)
            Case dbBinary, dbLongBinary, dbVarBinary
              ' numeric?
            Case dbTimeStamp
              ' text? numeric?
            Case Else
              ' dbBigInt , dbBoolean, dbByte, dbCurrency, dbDecimal, 
              '   dbDouble, dbFloat, dbInteger, dbLong, dbNumeric, dbSingle
              strValue = varValue
          End Select
          strValues = strValues & strValue
        End Sub
      

      ...这将连接您的值列表,然后您可以连接到整个 SQL 字符串(在 VALUES() 子句的括号之间)。

      但正如其他人所说,首先使用参数可能会更好。

      【讨论】:

        【解决方案5】:

        FWIW,我使用了稍微不同的格式,使用 Access 的换行符“_”。我还使用连接运算符“&”。主要原因是为了可读性:

        Dim db as Database: Set db = Current Db
        Dim sql$
        sql= "INSERT INTO MyTable (Field1, Field2, Field3 ...Fieldn) " & _
             "VALUES (" & _
             Me.TextMyField1 & _
             "," & Me.TextMyField2 & _
             "," & Me.TextMyField3 & _
             ...
             "," & Me.TextMyFieldn & _
             ");"
        db.Execute s
        Set db = nothing
        

        【讨论】:

          【解决方案6】:

          我会使用上面的方法,将每个参数放在单独的行上,这样调试和添加都很方便。

          但是,如果您真的不喜欢这种方式,那么您可以查看参数查询。稍不灵活,但在某些情况下稍快。

          或者另一种方法是定义一个公共函数用于插入该表并将值作为参数传递给它。

          不过,我会坚持你所拥有的,但如果 VBA 能理解,那就太好了 =+

          【讨论】:

          • 参数(在 SQL 代码和数据访问中间件中)比性能更有优势,例如强数据类型、默认参数值、SQL 注入保护、转义特殊字符等。此外,我确信在某些情况下 Access 数据库引擎 PROCEDURE 的性能实际上比动态 SQL 更差。
          • 同意参数查询确实有很多好处。我只提到了事物的性能方面作为可能的副作用,因为访问将保留查询计划,而不必像使用动态生成的 SQL 语句那样解决它。话虽如此,使用现代硬件,我怀疑您会注意到任何差异,因此有很多理由使用参数查询,但性能非常低
          • 我建议编辑您的帖子以更改“我将使用上述方法”的措辞,以包括发布者的姓名。根据问题的查看方式、最新、最旧和投票方式,答案的顺序可能会发生变化。
          • 对不起,托尼,我对这个网站有点陌生,而且我已经习惯了一个顺序不变的普通论坛。我将更改我的发帖风格以按名称引用帖子
          • 凯文,我听到了。我自己是 Fidonet BBS/NNTP 的老手,所以这个在线论坛的东西需要一点时间来适应。
          【解决方案7】:

          我过去做过的一件事是创建一个系统,用于解析 SQL 代码以查找参数并将参数存储在表中。我会在 Access 之外编写我的 MySQL 查询。然后我所要做的就是从 Access 打开文件,并且每次我想运行它时它都可以随时更新。

          这是一个非常复杂的过程,但如果您有兴趣,我很乐意在下周重新开始工作时挖掘代码。

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2021-02-21
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2017-03-10
            • 1970-01-01
            相关资源
            最近更新 更多