链接表能删了: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 端有这样一个存储过程:
CREATEPROCEDUREdbo.usp_AddOrder@CustomerIDINT,@OrderDateDATE,@AmountDECIMAL(18,2),@RemarkNVARCHAR(500),@NewOrderIDINTOUTPUT-- 返回新生成的订单号ASBEGINSETNOCOUNTON;INSERTINTOdbo.Orders(CustomerID,OrderDate,Amount,Remark)VALUES(@CustomerID,@OrderDate,@Amount,@Remark);SET@NewOrderID=SCOPE_IDENTITY();ENDVBA 这边这样调:
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 的价值在往后走——等你开始需要存储过程、输出参数、事务控制,那套代码不用大改,加参数就行。