Excel VBA 学习总结 - 通用ADO数据访问模型

  ADO是基于OLE DB的数据访问技术。它不直接与数据交互,而是把这个任务交给了OLE DB,这么做带来了相当好的可扩展性和适应性。它提供了编程语言和统一数据访问方式;它允许开发人员编写访问数据的代码而不用关心数据库是如何实现的,而只用关心数据库的连接。由于ADO是基于COM实现的,所以基本上任何语言都可以使用这种数据访问技术,VBA也不例外。

一、ADO对象模型

  ADO对象库中主要有9个对象,即:Connection、Command、Recordset、Record、Field、Error、Property、Parameter和Stream。呈现的形式基本是5大可以独立创建的基本对象,4大对象集合,如下图所示。

Objects.png

这9个对象中以Connection、Command、Recordset是最为常用的,很多情况下只需要这三个对象即可完成数据的读取和操作,对于一些很简单的应用,甚至使用它们中的任何一个就可以独立完成任务。下面是各个对象/集合的简略说明:

Connection:代表与数据源的连接与操作环境,基本任何的操作都是针对特定的Connection完成的。

常用属性:ConnectionString(最重要),ConnectionTimeout,State(连接的状态),Errors

常用方法:Open,Execute,Close

常用事件:ExecuteComplete,ConnectComplete

lightbulb.gif虽然Command对象与Recordset对象都可以在需要的时候自己隐式的创建一个Connection对象,但是对于需要多次执行命令或查询的场景,还是需要提供一个公共的Connection对象(这个对象的创建与销毁都是需要时间的)来共用。而且对于同一个连接字符串,ADO会采用连接池(存放Connection对象)的方式减少资源的浪费。

lightbulb.gifConnectionString中主要需要设置Provider,Data Source,Initial Catalog,User ID,Password,Integrated Security等值,这些大家应该都很熟悉了。当然有些值(像Provider,Mode这种)也可以在Connection的相关属性中设置。

lightbulb.gifConnection需要先执行Open方法打开后,才能Execute一些命令,最后需要使用Close关闭(通常为了保险起见,需要先检查State,再关闭)以释放资源。

Command:代表执行的添加、修改,删除、查询数据源的命令。

常用属性:ActiveConnection(设置连接字符串),CommandText(核心属性),CommandType,Parameters(一般是存储过程的参数)

常用方法:CreateParameter,Execute(可以有返回值)。

Recordset:代表执行查询命令后的结果集。

常用属性:ActiveConnection(设置连接字符串),BOF,EOF,CursorLocation,Filter,Sort,State,Fields(返回数据的类型)

常用方法:Open,Close,Move,MoveFirst,MoveNext,Find,NextRecordset

lightbulb.gif通常配合Range对象的CopyFromRecordset方法获取数据,但是这个方法只能获得值,一般需要匹配Fields属性获取列信息。

lightbulb.gif如果需要精确控制每行每列的值,可以使用RecordCount,Fields以循环的形式获取结果集中的每个值。

For i = 1 To rst.RecordCount
For j = 0 To rst.Fields.Count - 1
     Sheet1.Cells(i + 1, j + 1) = rst.Fields(j)
Next j
   rst.MoveNext
Next i

lightbulb.gif如果返回的结果集为空,则BOF与EOF都为True。通常它们也可以配合Fields精确控制结果集中的每个值。

lightbulb.gifFind方法基本上是支持与SQL中Where语句基本相同的语法。

Error与Errors:代表与数据源相关的操作的详细错误信息,Errors是Connection对象的属性。

Parameter与Parameters:代表基于参数化查询或存储过程的 Command 对象相关联的参数或自变量,Parameters是Command对象的属性。

Field与Fields:代表使用普通数据类型的数据的列,Fields是Recordset对象的属性。

Property与Properties: 代表ADO 对象的描述或控制对象的行为,分为内置属性(通过对象直接调用)和动态属性(通过集合使用 MyObject.Properties(0) 或 MyObject.Properties("Name") 语法来引用)。

Record:代表记录集中的一行、或文件系统的一个文件或一个目录。

Stream:用于读写以及处理二进制数据或文本流。


二、ADO编程模型

  ADO 的目标是访问、编辑和更新数据源,而编程模型体现了为完成该目标所必需的系列动作的顺序。ADO提供类和对象完成以下活动:

  • 连接到数据源 (Connection对象),并可选择开始一个事务。

  • 可选择创建对象来表示 SQL 命令 (Command)。

  • 可选择在 SQL 命令中指定列、表和值作为变量参数 (Parameter)。

  • 执行命令 (使用Command、Connection 或 Recordset对象完成)。

  • 如果命令按行返回,则将行存储在缓存中 (使用Recordset对象)。

  • 可选择创建缓存视图,以便能对数据进行排序、筛选和定位 (使用Recordset对象)。

  • 通过添加、删除或更改行和列编辑数据 (使用Recordset对象)。

  • 在适当情况下,使用缓存中的更改内容来更新数据源 (使用Recordset对象)。

  • 如果使用了事务,则可以接受或拒绝在完成事务期间所作的更改;结束事务 (Connection)。

  • 释放相关对象(通常是把对象设为Nothing,别忘了Set关键字)。

lightbulb.gif不管是否显式的使用了Connection对象,这个对象在整个访问数据库的过程中,是始终存在的。

lightbulb.gif其实除了连接字符串以及数据库特有的一些特性(比如SqlServer支持存储过程)外,访问各种数据库的基本流程和处理的语句,包括SQL语句都是差不多的。


三、ADO实践

ADO对象创建

  对于COM对象的创建方式,大家应该很熟悉了,有两种方式:

后期绑定:使用CreateObject方法。

Dim cnn As Object, rst As Object
Set cnn = CreateObject( " ADODB.Connection ")
Set rst = CreateObject( " ADODB.Recordset ")

前期绑定:先引用“Microsoft ActiveX Data Objects 2.x Library”(尽量选择高版本),然后直接就可以使用了。下面两种写法都可以:

Dim cnn1 As ADODB.Connection
Set cnn1 = New ADODB.Connection

Dim cnn2 As New ADODB.Connection

lightbulb.gif前期绑定能更好的利用VBE(或者说是VBIDE)的Intellisense。


  使用ADO访问各种类型的数据库,基本上除了连接字符串不同,专有特性不同,使用ADO的其它过程基本都差不多,所以下面几种类型数据文件的操作,重点都是介绍各自不同的地方。

使用ADO访问Access中的数据

连接字符串示例:

复制代码
' Provider和Data Source是必须的
'
Mode控制访问数据库方式,本例中是排他访问
Public Const AccessConnection As String = _
"Provider= Microsoft.ACE.OLEDB. 12.0;" & _
"Data Source=C:\Files\Northwind 2007.accdb;" & _
"Mode=Share Exclusive;" & _
"User ID=Admin;" & _
"Password=password"
复制代码

在本例中,使用的是Access 2007,所以使用的Provider是“Microsoft.ACE.OLEDB.12.0”。下面是一个伪代码例子,基本上涵盖了所有的步骤:

复制代码
Public Sub PlainTextQuery()
' 省去了定义的部分
 sConnect = "Provider=Microsoft.ACE.OLEDB. 12.0;" & _
   "Data Source=C:\Files\Northwind 2007.accdb"  
 sSQL = " SELECT..."

Set rsData = New ADODB.Recordset
 rsData.Open sSQL, sConnect, adOpenForwardOnly, adLockReadOnly, adCmdText

If Not rsData.EOF Then
   Sheet1.Range("A2").CopyFromRecordset rsData
   rsData.Close

With Sheet1.Range("A1:B1")
     .Value = Array("Company", "Contact Name")
     .Font.Bold = True
End With
' 调整列宽
   Sheet1.UsedRange.EntireColumn.AutoFit
Else
   rsData.Close
MsgBox " Error: No records returned.", vbCritical
End If
' 销毁对象
Set rsData = Nothing
End Sub
复制代码

lightbulb.gif像上面所说那样,解决一个问题,可以使用不同的对象。这里仅使用Recordset就解决了所有的问题,但是当需要使用Connection或者Command对象的时候,毫不犹豫的使用它们。

lightbulb.gif时刻谨记,对象的创建是需要开销的,当可以重用一个对象的时候(例如Command对象,当执行不同的命令时,只需要把CommandText换掉就可以了),毫不犹豫的使用它。

lightbulb.gif使用Insert,Update,Delete语句后,可以查询影响的行数来确定操作是否成功了,如果失败了,可以使用Err.Raise来引发一个错误。

lightbulb.gifAccess中支持在数据库中产生和存储SQL语句,想了解更多的可以参看其它资料。

lightbulb.gifSQL语句中,可以使用IIF简单的进行一些替换处理。


使用ADO访问SqlServer中的数据

连接字符串示例:

复制代码
' 常见的两种SqlServer连接字符串形式
Public Const SQLConnection1 As String = _
" Provider=SQLOLEDB; " & _
" Data Source=ComputerName\SQLServerName; " & _
" Initial Catalog=Northwind; " & _
" User ID=User;Password=password; " & _
" Network Library=dbmssocn "

Public Const SQLConnection2 As String = _
" Provider=SQLOLEDB; " & _
" Data Source=ComputerName\SQLServerName; " & _
" Initial Catalog=Northwind; " & _
" Integrated Security=SSPI "
复制代码

lightbulb.gif除了连接字符串外,Sqlserver由于有存储过程(Stored Procedures),所以在Command对象的CommandText中就可以放入存储过程的名字,然后使用CreateParameter方法创建参数,并通过Parameters.Append方法加入到参数集合中使用。这种调用方式效率比较高,当多次需要调用一个存储过程的时候,推荐这么使用。

复制代码
Set gobjCmd = New ADODB.Command
Set gobjCmd.ActiveConnection = gobjConn
' 与文本形式的SQL语句最大的不同就是这里
gobjCmd.CommandText = " spInsertShippers "
gobjCmd.CommandType = adCmdStoredProc

' 添加参数,注意第一个参数总是返回值
gobjCmd.Parameters.Append _
gobjCmd.CreateParameter( " @RETURN_VALUE ", adInteger, _
adParamReturnValue, 0)
gobjCmd.Parameters.Append _
gobjCmd.CreateParameter( " @CompanyName ", adVarWChar, _
adParamInput, 40)
gobjCmd.Parameters.Append _
gobjCmd.CreateParameter( " @Phone ", adVarWChar, _
adParamInput, 24)

' 给参数赋值
gobjCmd.Parameters( " @CompanyName ").Value = " Air Carriers "
gobjCmd.Parameters( " @Phone ").Value = " (206) 555-1212 "

' 执行存储过程
gobjCmd.Execute Recordsaffected:=lNumAffected, Options:=adExecuteNoRecords

' 验证信息
If lNumAffected <> 1 Then
 Err.Raise Number:=vbObjectError + 1024, _
   Description:= " Error executing Command object. "

' 获取返回的主键信息
lKeyValue = gobjCmd.Parameters( " @RETURN_VALUE ").Value
Debug.Print " The key value of the new record is: " & CStr(lKeyValue)
复制代码

lightbulb.gif由于存储过程会作为Connection的动态方法存在,所以简单使用存储过程,特别是只使用很少次的时候,也可以像下面例子这样,直接调用,不过这种方式效率比较低,还是推荐使用上面Command包装的调用方法。

objConn.spGetCustomerNames "UK", rsData

lightbulb.gif需要注意的是无论何种情况下,Parameters集合的第一个元素都是代表存储过程的返回值,不管实际上这个存储过程有没有返回值。

lightbulb.gifSqlserver是支持返回多个结果集的,下面是典型的结果集操作:

复制代码
' 假设这个存储过程只返回2个结果集
Do While Not rsData.EOF
' 执行相关赋值
 rsData.MoveNext
Loop

' 切换不同的数据集
Set rsData = rsData.NextRecordset

Do While Not rsData.EOF
' 执行相关赋值,例如给下拉列表赋值
' ddShippers.AddItem rsData.Fields(1).Value
 rsData.MoveNext
Loop

' 这里注意使用NextRecordset关闭和销毁了Recordset,不需要额外的操作了
Set rsData = rsData.NextRecordset
复制代码

lightbulb.gif使用Connection的时候,通常的目标是尽快访问数据库,尽快再关闭连接。然而很多时候这些数据保存在Recordset中一直使用是可以提高程序效率的,这个时候我们需要的是非连接型的Recordset。这个在ADO中是支持的,你可以从数据库中获得Recordset,然后关闭连接,在内存中保存Recordset并一直使用,然后也可以重新连接更新数据,甚至可以保存Recordset中的数据到别的地方。下面描述的就是一个非连接型Recordset的设置方式:

复制代码
szSQL = " SELECT CustomerID, CompanyName, ContactName FROM Customers "
' 创建非连接型Recordset的步骤
'
1) 创建Recordset对象.
Set grsData = New ADODB.Recordset
' 2) 游标位置设为Client.
grsData.CursorLocation = adUseClient
' 3) 游标类型设为Static.
grsData.CursorType = adOpenStatic
' 4) 锁的类型设为batch optimistic.
grsData.LockType = adLockBatchOptimistic
' 5) 打开记录集.
grsData.Open szSQL, gobjConn, , , adCmdText
' 6) 把Recordset的ActiveConnection设为Nothing.
Set grsData.ActiveConnection = Nothing

Sheet1.Range( " A1 ").CopyFromRecordset grsData
复制代码

lightbulb.gif注意上面的grsData是定义为Public类型的:Public grsData As ADODB.Recordset。获得到这个对象以后,就可以使用Filter,Sort等手段,获取需要的数据。


  不用怀疑,利用ADO去处理下面两种非关系型数据(但是是格式化的数据)那是非常方便的,因为SQL语句是强大的,可以完成很多的查询,筛选功能。

使用ADO访问Excel中的数据

连接字符串示例:

ExcelConnect = " Provider=Microsoft.ACE.OLEDB.12.0; " & _
" Data Source=C:\Files\Sales.xlsx; " & _
" Extended Properties=Excel 12.0; "

lightbulb.gif注意访问Access与Excel的连接字符串中,Provider是相同的;但是Excel需要多加上Extended Properties,这里的版本号随着Excel的版本不同而不同。

lightbulb.gif使用的SQL语句与Access中基本相同,除了以下几点:

复制代码
' 使用Sheet名字作为表名的时候需要加$符号和方括号
'
如果Sheet名字中有空格的,名字两边可以用单引号括起来,例如:['My Sheet'$]
sSQL1 = " SELECT * FROM [Sheet1$] "
' 如果表名是Sheet级别的名字,可以把这个表名接在$符号后面
sSQL2 = " SELECT * FROM [Sheet1$SheetLevelName]; "
' 如果表名是一个Range范围,也直接加在$符号后面
sSQL3 = " SELECT * FROM [Sales$A1:E89]; "
' 如果表名是一个Workbook级别的名字,可以直接使用
sSQL4 = " SELECT * FROM BookLevelName; "
复制代码

lightbulb.gif注意了,OLE DB默认认为Worksheet中的第一行是列名,而不是数据。如果不是这种情况,那么需要告诉OLE DB,第一行也是数据,这个时候需要在连接字符串中的Extended Properties中加上“HDR=No”。如下列所示:

sConnect = " Provider=Microsoft.ACE.OLEDB.12.0; " & _
" Data Source=C:\Files\Sales.xlsx; " & _
" Extended Properties=""Excel 12.0;HDR=No""; "
' 注意上面的引号数目


使用ADO访问Text中的数据

对于含有大量格式化的文本数据,例如csv文件,使用ADO简直就是如鱼得水,你能充分享受SQL带来的强大便捷性。

连接字符串示例:

sConnect = " Provider=Microsoft.ACE.OLEDB.12.0; " & _
" Data Source=C:\Files\; " & _
" Extended Properties=Text; "

lightbulb.gif注意了,Provider与访问Excel是一样的,不同的是,为了方便处理多个这样的文件,连接字符串中不需要提供文件名字;而在SQL语句中的表名,使用的是文件名。如下面的例子:

sSQL = " SELECT * FROM Sales.csv WHERE Type='Art'; "

lightbulb.gif同样的,OLE DB默认认为文件中的第一行是列名,而不是数据。如果不是这种情况,那么需要告诉OLE DB,第一行也是数据,这个时候需要在连接字符串中的Extended Properties中加上“HDR=No”,参考上面的设置。


  到这里,基本的ADO操作已经总结完了,但是ADO的功能绝对不仅仅如此,它还包括另外一大块内容,那就是RDS编程模型。RDS模型解决的是网络数据访问问题,这个在Excel中比较少见,这里就不重点介绍了,有兴趣的可以自己琢磨琢磨看。