第5章 VB高效数据访问
5.1 规则5-1:高效访问的基础:往返开销、SQL语句和数据提供者
5.1.1 使往返开销最少
# 数据库、远程DCOM对象或HTTP网络通信等请求-响应过程的步骤:
1) 创建请求。确定内容,并以正确的格式包装。
2) 发送消息。调用必要的协议发送消息给服务器,通常涉及对一些系统服务的调用。
3) 接收消息。服务器接收、识别消息,并转发给适当的程序处理。
4) 处理请求。服务器程序处理请求,完成动作。
5) 服务器方重复步骤1)至4),将响应送回请求方。
# 减少往返次数将使程序的效率得到显著提高。
# 具体到VB数据库交互过程的步骤:
1) 生成SQL查询。创建SQL语句。
2) 通过 ADO Connection 发送查询请求。需要创建 ADO Connection 对象并调用 Open 方法打开连接,再调用 Execute 发送请求。
3) 数据库通过已建立的连接接收请求。
4) 数据库处理 SQL 语句。首先编译成二进制代码,其次需要进行代码优化,然后执行代码。
5) 数据库以 Recordset 形式返回结果。结果按照 Tabular Data Stream (TDS) 格式返回客户端,TDS 由 ADO 接收,并转换成 Recordset 对象。这一步还将包括4个步骤。
# 为减少往返开销,可能需要提前在客户端缓存大量数据以备检索;或者把数据库查询暂存,再成批发送。
5.1.2 确定发送SQL查询的最好方法
# 联系紧密并连续执行的多个查询,可以一次发送。这样数据库可以将这些查询作为整体进行优化。
# 利用存储过程,省去编译和优化的步骤。
5.1.3 选择合适的提供者
# 尽可能使用 OLEDB Provider 而不是 ODBC 连接数据库。
# 默认的Provider值是 Provider=MSDASQL (OLEDB Provider for ODBC)
5.2 规则5-2:不要过分封装数据访问
# 掌握了面向对象技术之后,不应当再用窗体和BAS模块包含所有的函数过程,而应该为应用程序中的业务对象建模。
5.2.1 纯粹的面向对象技术
# 完全应用面向对象风格建模时,可能会将对每一个数据库表的访问封装在一个单独的类中,这些类都应该包含用来创建和释放数据库连接的逻辑,同时也要包含读取数据表和修改数据表的逻辑。
## RentVideo client
Public Sub RentVideo(sCustID As String, sVideoID As String, Optional iRentalTerms As Integer = 3)
Dim rCustomers As New CCustomers
Dim rRentals As New CRentals
Dim rViedos As New CVideos
‘** connect to customers, check standing, and disconnect
rCustomers.Connect
If Not rCustomers.Standing(sCustID) Then
Err.Raise vbObjectError + 2049, _
“RentVideo.CheckStanding", _
“Customer not in good standing.”
End If
rCustomers.Disconnect
‘** connect to videos, check stock, decrement stock, and disconnect
rVideos.Connect
If Not rVideos.InStock(sVideoID) Then
Err.Raise vbObjectError + 2050, _
“RentVideo.CheckStock", _
“Video currently not in stock.”
End If
rVideos.DecrementStock sVideoID
rVideos.Disconnect
‘** connect to rentals, add new record, and disconnect
rRentals.Connect
rRentals.AddRental sCustID, sVideoID
rRentals.Disconnect
End Sub
5.2.2 追求纯粹OOD效果的不足之处
# 给每个数据表都设计相应的类,虽然提高了代码的可维护性,但系统性能会大大下降。
## 未能充分利用数据库引擎自身的优化功能
## 过多的使用珍贵系统资源(数据库连接等)
## 数据库往返次数太多
5.2.3 解决办法:使用存储过程
# 使用存储过程编写需要访问多个表的业务逻辑,客户端只需要一次数据库访问就可以完成任务。
## /* Stored Procedure CheckOutVideo */
CREATE PROCEDURE CheckOutVideo @ID_Cus varchar(10), @ID_Vid varchar(10)
AS
/* Set nocount attribute to allow immediate error trapping */
/* sqloledb 提供者默认为每个存储过程提供多个结果记录集,
而且引发的错误不会明确地传递到客户端。要捕获错误,需要使用记录集的
NextRecordset 方法,检查与每个结果记录集一同保存的错误信息 */Set nocount on
DECLARE @Quantity int
SELECT @Quantity = vid_Instock FROM tbl_Videos
WHERE id_Video = @ID_Vid
DECLARE @Standing int
SELECT @Standing = cus_GoodStanding FROM tbl_Customers
WHERE id_Customer = @ID_Cus
IF @Standing = 1
BEGIN
IF @Quantity > 0
BEGIN
UPDATE tbl_Video SET vid_InStock = @Quantity - 1 WHERE id_Video = @ID_Vid
INSERT INTO tbl_Rentals (idRental, id_Customer, id_Video,
ren_RentedOn, ren_DueBack) VALUES (NEWID(), @ID_Cus, @ID_Vid,
GetDate(), DateAdd(dayofyear, 3, GetDate()) )
END
ELSE
RAISEERROR(’Video currently not in stock.’, 11, 1)
/* You may choose, instead of using RAISEERROR, simply to
return a value that your client app will understand to be a
failure condition, thus avoiding having exceptions over the network */
END
END
ELSE
RAISEERROR(’Customer no in good standing.’, 11, 1)
GO
## 客户端 CDataAccessPublic Sub RentVideo(sCustID As String, sVideoID As String)
On Error GoTo hErr
Dim rConn As New ADODB.Connection
rConn.Open “<connection String>”
‘** call stored procedure, passing in customer ID and video ID
rConn.Execute “EXEC CheckOutVideo ‘” & sCustID & “‘,’” & sVideoID & “‘”
rConn.Close
Exit Sub
hErr:
‘** close connection on a failure condition
rConn.Close
‘** report error to user
Err.Raise vbObjectError + 2049, _
“CDataAccess.RentVideo", Err.Description
End Sub
# 存储过程也可以使用输出参数,或者输出记录集返回数据。用 ADO Command 对象和 Parameters 集合来处理这种情况。
# 缺点:减弱封装性;业务逻辑被分割在代码和数据库当中,不便于维护;编写和维护存储过程的工具功能有限。
5.2.4 如果需要多个数据库服务器该如何处理
# 存储过程只能访问其所在的数据库服务器范围内的数据。虽允许查询另一个数据库,但无法利用第二个服务器上的存储过程,且这样可能带来移植、维护的困难。
# 建模时,对每一个数据库连接,用一个业务对象来对应。(以合理的最小粒度为实体建模)
5.3 规则5-3:切莫将数据库连接当作数据成员
# 早期的 VB 程序通常使用与窗体生命期相同的数据库连接,来完成窗体内的数据库访问工作。
# 在有大量用户的系统中,使用连接缓冲池可以有效地节约数据库连接。ODBC 3.0 (ODBC.DLL) 和 OLEDB 2.0 (MTXDM.DLL) 以上都包含了连接缓冲的功能。
# ADO Connection 对象不仅仅是底层的真正数据连接的代理,物理连接只有在 Open 之后才会创建。(晚分配,早释放)
# 除非有特殊需要,ADO 连接应该在局部声明,在请求数据之前打开连接,完成后尽早关闭。不要通过共享 ADO 连接对象来模拟连接缓冲。
5.4 规则5-4:死锁是常见的——防错性程序开发
5.4.1 锁定
# 锁的类型与作用
拥有的锁的类型|请求读锁|请求写锁
无|许可|许可
读锁|许可|拒绝
写锁|拒绝|拒绝
# 系统中存在的锁越多,一个事务所需要的数据正被另一个事务锁定而造成阻塞的几率也就越大。
5.4.2 串行化事务和锁管理器
# 串行化事务:所有事务排入先进先出队列。并发性差。
# SQL Server 的锁管理器可以通过设定隔离等级实现不同的锁定模式。
## 默认值 Read Commited 。T1 的读锁在完成读操作之后释放,而不是在事务的生命期中一直存在。如果 T2 修改了 T1 已经读取过的记录,可能影响 T1 的运行。
## 隔离等级 Serializable 时,T2 不能修改 T1 读取的记录。
5.4.3 死锁
# 死锁的发生条件:
## 事务 T1 开始,并获得一些数据锁
## 事务 T2 开始,并获得自己的数据锁
## 事务 T1 要求对已被 T2 锁定的数据加锁以便完成事务
## 事务 T2 要求对已被 T1 锁定的数据加锁以便完成事务
# 同一数据库锁管理器控制之下的数据记录同时被不同事务请求,可以判定死锁。如果请求的数据库记录属于不同的锁管理器,需要在锁管理器之间交换信息。
# 防止死锁的方案:
## 让事务串行化运行。有损并行性。
## 在运行事务之前获取所有所需的锁。开销过高。
# MTS/COM+ 使用超时来检测死锁。MTS 事务超时只能全局设置。COM+允许全局或者对组件配置。
# MTS 在第一个方法调用之后开始计时,即使不需要锁。COM+ 在请求数据锁之后才开始计时。
5.4.4 在应用程序设计中尽量减小死锁的几率
# 尽可能晚获取,早释放的同时,尽量快地处理事务。
参考 p207, Priciples of Transaction Processing for the Systems Professional (Philip Bernstein and Eric Newcomer, Morgan Kaufmann Publishing, 1997)
5.4.5 将事务运行时间降到最短
# 减少到服务器的往返次数。(#5.1,#5.2)
# 不要反复读同一数据,而是在第一次读取数据后将其留在内存以提高访问速度,因为数据已经被锁定。
5.4.6 将锁定时间降至最短
# 可以重新设计对象,把事务性方法放在一个对象中,设置其在 Serializable 模式下运行。这样执行非事务性方法就不会造成不必要的数据锁定。
# 组织好代码,可以在读取数据库之前进行的数据确认等工作一定要先完成,不要浪费获取数据锁之后的时间。
5.5 规则5-5:尽可能使用firehose ( firehorse? )游标
# CursorType CursorLocation
|ForwardOnly|Static|KeySet|Dynamic
Server|Y|Y|Y|Y
Client|N|Y|N|N
# firehose 游标:
CursorType=adForwardOnly; CursorLocation=asUseServer; LockType=adLockReadOnly; CacheSize=1
允许调用 MoveFirst,重新执行查询。
# 使用 firehorse 游标,仅仅将 Recordset 作为读取数据的方式。使用
rCN.Execute “UPDATE …”
来更新数据。
# 更新 Memo 或者 BLOB 字段需要使用 RecordSet 的 Update 方法。
5.6 规则5-6:作出正确的数据搜索决策(避免滥用 SelectSingleNode )
# 通过 XML 实现 SQL Server 与 ADO 之间的数据交换在某些情况下可能不是好的方式。
5.6.1 Seek-and-Find 组件
# 搜索数据库中的记录的几个方案:
## 将数据引入本地 XML 文件,使用 SelectSingleNode 方法查找。
## 将数据留在 SQL Server 上,使用 Transact-SQL 选择记录行。
## 将数据引入本地 ADO 记录集,并使用 Recordset.Find。
5.6.2 了解解决具体问题需选用哪一种方法
# 记录集有数十万、上百万时适合用服务器搜索
# 数据来自非数据库源时考虑用 XML 方案
# 数据集大小一般,全部传递到客户端负担不太重,可以通过断开连接的 ADO Recordset 来搜索