链接表能删了: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
窗体里的文本框控件名字只要和查询字段名一致(不区分大小写),绑定自动生效,不需要手动设控件来源,但是你的控件一定要添加控件来源。
有三个地方容易出问题,写这段代码之前先说清楚。
mDb 和 mRs 必须声明在窗体模块顶部,不能放在 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 的价值在往后走——等你开始需要存储过程、输出参数、事务控制,那套代码不用大改,加参数就行。

浙公网安备 33010602011771号