链接表能删了:Access 直连 SQL Server,DAO 绑窗体 + ADO 参数查询完整代码

摘要: 后台是 SQL Server 却不想用链接表?本文介绍 DAO 直连绑定窗体和 ADO 参数化查询两种方案,配完整可运行代码,直接拿走用。access开发|access培训|access框架|请添加edonsoft。

Hi,大家好!

上一篇讲了 Access 和 SQL Server 的差别。有读者看完之后问了一个很实际的问题:后台已经是 SQL Server,前端 Access 用的是链接表,但现在不想用链接表了,有没有别的办法让窗体继续能查数据、能编辑?

这个需求我遇到过不止一次,原因也各不相同——有的是网络环境里链接表刷新太慢,有的是服务器迁移了链接路径失效,还有的是想做更严格的权限控制,不想让 Access 直接"看到"表结构。

办法有两种。一种是 DAO 直连:用 DBEngine.OpenDatabase 在 VBA 里打开 ODBC 连接,把记录集直接绑给窗体,新增、修改、删除全部照常,体验和链接表几乎没差别。另一种是 ADO 参数化查询:用 ADODB.Command 带参数跑 SQL,把结果填进列表框或子窗体,更适合复杂筛选、存储过程调用,以及需要控制事务的保存操作。两种都给完整代码。

在这里插入图片描述

先说 DAO 直连

DAO(Data Access Objects)是 Access 的原生数据访问接口,很多人不知道它其实也能不靠链接表直接连 ODBC 数据源。做法是用 DBEngine.OpenDatabase 打开一个 ODBC 连接,然后拿到的 DAO.Recordset 直接赋给窗体的 Recordset 属性,窗体就有数据了。

这种方式最大的好处是:窗体保持绑定状态,新增、修改、删除照常用,Access 的导航按钮、记录锁都还在,几乎和链接表的使用体验一样,只是数据源换成了 ODBC 直连。

DAO 方案完整代码

新建一个标准模块,命名 modSQLConn,把下面的连接字符串函数放进去,后面窗体代码会用到:

Option Compare Database
Option Explicit

' 返回 SQL Server 无 DSN 连接字符串
' 根据实际情况修改 SERVER、DATABASE
Public Function SQLConnStr() As String
    SQLConnStr = "ODBC;" & _
                 "DRIVER={ODBC Driver 18 for SQL Server};" & _
                 "SERVER=SQL01;" & _
                 "DATABASE=SalesDb;" & _
                 "Trusted_Connection=Yes;" & _
                 "Encrypt=Yes;" & _
                 "TrustServerCertificate=Yes;"
End Function

然后打开需要绑定的窗体(设计视图),把窗体的记录源清空,在窗体模块里写:

Option Compare Database
Option Explicit

' 注意:必须声明在模块顶部,不能放在 Form_Load 里
' 生命周期和窗体绑定,窗体关闭前不能释放
Private mDb As DAO.Database
Private mRs As DAO.Recordset

Private Sub Form_Load()
    Dim sql As String

    ' 打开 ODBC 直连,不用预先建 DSN
    ' dbDriverNoPrompt:连接失败直接报错,不弹驱动选择框
    Set mDb = DBEngine.OpenDatabase( _
        "", dbDriverNoPrompt, False, SQLConnStr())

    ' 写你真正需要的查询,必须包含主键列,否则记录集不可更新
    sql = "SELECT OrderID, CustomerID, OrderDate, Amount, Remark " & _
          "FROM dbo.Orders " & _
          "ORDER BY OrderDate DESC;"

    ' dbOpenDynaset:动态集,支持编辑
    ' dbSeeChanges:表有 IDENTITY 自增列时必须加,否则新增报错
    Set mRs = mDb.OpenRecordset(sql, dbOpenDynaset, dbSeeChanges)

    ' 把记录集绑给窗体,完成后窗体控件自动按字段名匹配
    Set Me.Recordset = mRs
End Sub

Private Sub Form_Unload(Cancel As Integer)
    ' 窗体关闭时释放资源,顺序不能反
    If Not mRs Is Nothing Then
        mRs.Close
        Set mRs = Nothing
    End If
    If Not mDb Is Nothing Then
        mDb.Close
        Set mDb = Nothing
    End If
End Sub

窗体里的文本框控件名字只要和查询字段名一致(不区分大小写),绑定自动生效,不需要手动设控件来源,但是你的控件一定要添加控件来源

有三个地方容易出问题,写这段代码之前先说清楚。

mDbmRs 必须声明在窗体模块顶部,不能放在 Form_Load 里。放进过程里就成了局部变量,Form_Load 跑完就释放,窗体打开之后数据随时会变成空白,或者弹出"对象无效"的错误。这是我见过最多人踩的地方,而且报错时机不固定,有时候立刻报,有时候要等用户翻几页记录才出。

查询必须包含主键,且查询本身可更新。两表联接、带 GROUP BY、带 DISTINCT 的查询基本上都是只读的,这种结果集赋给窗体之后能看数据,但改不了。如果窗体只需要显示,问题不大;如果需要编辑,就得把查询拆开,或者改用 ADO 加存储过程来保存。

dbSeeChanges 这个参数,SQL Server 表有 IDENTITY 自增列时必须加。不加的话,新增一条记录之后 Access 找不到刚插入的那行,会弹"找不到记录"。加上之后 Access 在 INSERT 完成后会自动定位到新行,这个问题就消失了。

加筛选条件

如果窗体需要按条件筛选(比如按订单日期范围),不要拼接 SQL 字符串,改用 Recordset.Filter

Private Sub btnFilter_Click()
    Dim d1 As String
    Dim d2 As String

    ' 取文本框里的日期,转成 SQL Server 认识的格式
    d1 = Format(Me.txtDateFrom, "yyyy-mm-dd")
    d2 = Format(Me.txtDateTo, "yyyy-mm-dd")

    ' Filter 条件用字段名,日期用单引号括起来
    mRs.Filter = "OrderDate >= '" & d1 & "' AND OrderDate <= '" & d2 & "'"

    ' 用筛选后的克隆集重新绑窗体
    Set Me.Recordset = mRs.OpenRecordset()
End Sub

如果筛选条件变化很大,比如字段都不固定,更简单的做法是重新执行 mDb.OpenRecordset,用新 SQL 替换旧的,再重新赋给 Me.Recordset
在这里插入图片描述

再说 ADO 参数化查询

ADO(ActiveX Data Objects)连接 SQL Server 时走 OLE DB 或 ODBC,写法和连接其他数据库基本一样。我一般在这几种情况下选 ADO 而不是 DAO:筛选条件多、带多个参数的查询;需要调用 SQL Server 存储过程;执行写入操作时需要拿回影响行数或者输出参数。

ADO 的一个重要习惯是参数化——用 ? 占位符传值,不把变量直接拼进 SQL 字符串。日期格式、单引号转义这些问题直接绕开,SQL 注入的风险也没有了。我见过不少人图省事用 "WHERE CustomerID = " & Me.cboCustomer 这种拼法,字段是数字还好,一旦遇到字符串或者日期,调试起来很麻烦。

ADO 公共模块

用 ADO 之前要先在 Access 引用库里勾上 Microsoft ActiveX Data Objects。VBE 菜单 → 工具 → 引用,找到 Microsoft ActiveX Data Objects 6.1 Library(或者 2.8,装了什么版本就选哪个),勾上确定。没有这一步,代码里的 ADODB.Connection 会报"用户自定义类型未定义"。

新建标准模块 modADO

Option Compare Database
Option Explicit

' 建立 ADO 连接,成功返回 ADODB.Connection,失败返回 Nothing
' 调用方负责关闭和释放
Public Function ADO_Connect() As ADODB.Connection
    Dim conn As ADODB.Connection

    Set conn = New ADODB.Connection

    ' OLE DB Provider for SQL Server
    ' MSOLEDBSQL 是微软 2018 年后推荐的新驱动,需独立安装:
    '   https://learn.microsoft.com/zh-cn/sql/connect/oledb/download-oledb-driver-for-sql-server
    ' 如果没装,换成 SQLNCLI11(SQL Server 2012+ 自带):
    '   Provider=SQLNCLI11;
    '
    ' OLE DB Windows 集成验证用 Integrated Security=SSPI
    ' (不是 ODBC 的 Trusted_Connection=Yes,那个 OLE DB 不认)
    ' 改用 SQL 账号的话替换为:UID=sa;PWD=yourpwd;
    conn.ConnectionString = _
        "Provider=MSOLEDBSQL;" & _
        "Server=SQL01;" & _
        "Database=SalesDb;" & _
        "Integrated Security=SSPI;" & _
        "TrustServerCertificate=yes;"

    On Error GoTo ConnErr
    conn.Open
    Set ADO_Connect = conn
    Exit Function

ConnErr:
    Set conn = Nothing
    Set ADO_Connect = Nothing
    MsgBox "连接 SQL Server 失败:" & Err.Description, vbCritical
End Function

如果机器上没有 MSOLEDBSQL,也可以换成 ODBC 方式:"Provider=MSDASQL;DRIVER={ODBC Driver 18 for SQL Server};SERVER=SQL01;...",两种写法功能上没区别,Driver 18 装了就能用。

参数化查询 Demo:按客户和日期范围查询订单

' 查询订单,结果填入列表框 lstOrders
' lstOrders 的列数需要提前设好,列宽也要配好
Private Sub btnQuery_Click()
    Dim conn  As ADODB.Connection
    Dim cmd   As ADODB.Command
    Dim rs    As ADODB.Recordset
    Dim rows  As String

    Set conn = ADO_Connect()
    If conn Is Nothing Then Exit Sub

    Set cmd = New ADODB.Command
    cmd.ActiveConnection = conn

    ' 参数用 ? 占位,不拼字符串
    cmd.CommandText = _
        "SELECT OrderID, CustomerName, OrderDate, Amount " & _
        "FROM dbo.Orders " & _
        "WHERE CustomerID = ? " & _
        "  AND OrderDate BETWEEN ? AND ? " & _
        "ORDER BY OrderDate DESC;"
    cmd.CommandType = adCmdText

    ' 按顺序追加参数:类型、方向、大小、值
    ' adInteger, adDate, adDate
    cmd.Parameters.Append cmd.CreateParameter("@CustID", adInteger, adParamInput, , CLng(Me.cboCustomer))
    cmd.Parameters.Append cmd.CreateParameter("@D1", adDate, adParamInput, , CDate(Me.txtDateFrom))
    cmd.Parameters.Append cmd.CreateParameter("@D2", adDate, adParamInput, , CDate(Me.txtDateTo))

    Set rs = cmd.Execute

    ' 用 ValueList 方式填列表框
    ' 也可以改用 rs 直接赋给子窗体的 Recordset
    rows = ""
    Do While Not rs.EOF
        rows = rows & rs("OrderID") & ";" & _
                      rs("CustomerName") & ";" & _
                      Format(rs("OrderDate"), "yyyy-mm-dd") & ";" & _
                      Format(rs("Amount"), "#,##0.00") & ";"
        rows = rows & Chr(10)
        rs.MoveNext
    Loop

    rs.Close
    conn.Close
    Set rs = Nothing
    Set cmd = Nothing
    Set conn = Nothing

    Me.lstOrders.RowSourceType = "Value List"
    Me.lstOrders.RowSource = rows
End Sub

参数化执行 Demo:保存一条订单

' 保存窗体上的订单数据到 SQL Server
' 成功返回 True,失败返回 False
Public Function SaveOrder( _
    ByVal customerID As Long, _
    ByVal orderDate As Date, _
    ByVal amount As Currency, _
    ByVal remark As String _
) As Boolean

    Dim conn As ADODB.Connection
    Dim cmd  As ADODB.Command

    Set conn = ADO_Connect()
    If conn Is Nothing Then
        SaveOrder = False
        Exit Function
    End If

    Set cmd = New ADODB.Command
    cmd.ActiveConnection = conn
    cmd.CommandText = _
        "INSERT INTO dbo.Orders (CustomerID, OrderDate, Amount, Remark) " & _
        "VALUES (?, ?, ?, ?);"
    cmd.CommandType = adCmdText

    cmd.Parameters.Append cmd.CreateParameter("@CustID",    adInteger,  adParamInput, ,     customerID)
    cmd.Parameters.Append cmd.CreateParameter("@Date",      adDate,     adParamInput, ,     orderDate)
    cmd.Parameters.Append cmd.CreateParameter("@Amount",    adCurrency, adParamInput, ,     amount)
    cmd.Parameters.Append cmd.CreateParameter("@Remark",    adVarWChar, adParamInput, 500,  remark)

    On Error GoTo SaveErr
    cmd.Execute
    conn.Close
    Set cmd = Nothing
    Set conn = Nothing
    SaveOrder = True
    Exit Function

SaveErr:
    MsgBox "保存失败:" & Err.Description, vbCritical
    If Not conn Is Nothing Then conn.Close
    Set cmd = Nothing
    Set conn = Nothing
    SaveOrder = False
End Function

调用的地方很简单:

Private Sub btnSave_Click()
    If SaveOrder(Me.cboCustomer, Me.txtDate, Me.txtAmount, Me.txtRemark) Then
        MsgBox "保存成功", vbInformation
        Me.txtAmount = Null
        Me.txtRemark = Null
    End If
End Sub

调用 SQL Server 存储过程

如果保存逻辑放在 SQL Server 存储过程里——比如要做库存扣减、写操作日志、或者要保证多张表同时写入——ADO 也可以直接调,把 CommandType 换成 adCmdStoredProc 就行。

假设 SQL Server 端有这样一个存储过程:

CREATE PROCEDURE dbo.usp_AddOrder
    @CustomerID INT,
    @OrderDate  DATE,
    @Amount     DECIMAL(18,2),
    @Remark     NVARCHAR(500),
    @NewOrderID INT OUTPUT   -- 返回新生成的订单号
AS
BEGIN
    SET NOCOUNT ON;
    INSERT INTO dbo.Orders (CustomerID, OrderDate, Amount, Remark)
    VALUES (@CustomerID, @OrderDate, @Amount, @Remark);
    SET @NewOrderID = SCOPE_IDENTITY();
END

VBA 这边这样调:

Public Function CallAddOrder( _
    ByVal customerID As Long, _
    ByVal orderDate  As Date, _
    ByVal amount     As Currency, _
    ByVal remark     As String, _
    ByRef newOrderID As Long _
) As Boolean

    Dim conn As ADODB.Connection
    Dim cmd  As ADODB.Command

    Set conn = ADO_Connect()
    If conn Is Nothing Then
        CallAddOrder = False
        Exit Function
    End If

    Set cmd = New ADODB.Command
    cmd.ActiveConnection = conn
    cmd.CommandText = "dbo.usp_AddOrder"
    cmd.CommandType = adCmdStoredProc   ' 改这里

    ' 输入参数
    cmd.Parameters.Append cmd.CreateParameter("@CustomerID", adInteger,  adParamInput,  ,    customerID)
    cmd.Parameters.Append cmd.CreateParameter("@OrderDate",  adDate,     adParamInput,  ,    orderDate)
    cmd.Parameters.Append cmd.CreateParameter("@Amount",     adCurrency, adParamInput,  ,    amount)
    cmd.Parameters.Append cmd.CreateParameter("@Remark",     adVarWChar, adParamInput,  500, remark)
    ' 输出参数:adParamOutput,不传值,执行后从这里读回来
    cmd.Parameters.Append cmd.CreateParameter("@NewOrderID", adInteger,  adParamOutput, ,    0)

    On Error GoTo SPErr
    cmd.Execute
    newOrderID = cmd.Parameters("@NewOrderID").Value   ' 读取存储过程返回的新订单号

    conn.Close
    Set cmd = Nothing
    Set conn = Nothing
    CallAddOrder = True
    Exit Function

SPErr:
    MsgBox "调用存储过程失败:" & Err.Description, vbCritical
    If Not conn Is Nothing Then conn.Close
    Set cmd = Nothing
    Set conn = Nothing
    CallAddOrder = False
End Function

这种写法,Access 前端不需要知道存储过程里写了什么,库存怎么扣、日志怎么写、哪几张表要联动,都在 SQL Server 里处理,VBA 只管传参数、拿结果。
在这里插入图片描述

两种方案怎么选

我自己的习惯是这样区分的:

场景 推荐方案
窗体需要连续编辑多条记录,新增、修改、删除频繁 DAO 直连绑定
筛选条件复杂、带多个参数的查询 ADO 参数化查询
需要调用存储过程,或有事务、返回输出参数 ADO Command
只读报表、统计汇总 ADO 查询结果填子窗体或列表框
核心业务录入,有严格的校验和审计要求 非绑定窗体 + ADO 调存储过程保存

两种方案在同一个项目里混用很常见。比如查询列表用 ADO,点进某条记录打开编辑窗体用 DAO 绑定——查询这边灵活,编辑这边省事,各取所长。

还有一种更轻量的方式是传递查询(Pass-through Query),在 Access 查询设计器里直接建,连接字符串写 SQL Server ODBC 地址,SQL 在服务器端跑,结果返回给 Access。适合只读查询或者不需要在 VBA 里动态拼参数的场景。这个我之前专门写过,这里不重复了。

去掉链接表之后,DAO 方案改动最小,和原来链接表绑窗体的体验几乎没差别,迁移起来也快。ADO 的价值在往后走——等你开始需要存储过程、输出参数、事务控制,那套代码不用大改,加参数就行。
在这里插入图片描述

posted @ 2026-07-22 09:54  edonsoft  阅读(1)  评论(0)    收藏  举报