缓冲(Buffering)
Visual FoxPro 中的缓冲技术
当远端数据下载到客户端时,这些数据就被压入缓冲之中。在客户端用户对数据的各种移动并不反映到数据源,而是在用户确认对数据的变动后,才把各种变动以SQL描述的形式发送到后端。那么为什么不让Visual FoxPro直接一步一动的操控远程数据,就像在不使用缓冲技术控制Visual FoxPro本地数据那样。我想原因在于:
- 在 Client/Server 构架的应用中,数据库服务器需要同时处理许多客户的请求,如果有一个客户“直接”控制(锁定)它,多用户的系统就无从谈起了。
- Visual FoxPro 通过 ODBC 与远端数据库通讯,如果一步一动,两者之间的通讯肯定会成倍增加,这样既加重了网络负担又加重的数据库服务器的负担。
基于上述原因,Visual FoxPro在远程数据处理时强制使用缓冲技术。我们知道,在Visual FoxPro中缓冲技术与锁结合有四种选择:
- 保守式行缓冲。所谓“保守”,就是“编辑时锁定”的意思,“行缓冲”是指“只缓冲处理一笔使用者加以编辑的数据记录”。因此一旦使用这种模式,当编辑动作刚开始,数据源的对应数据记录便被锁定,而且在执行以下两项动作时,数据变动才会被发送:移动数据指针、执行TABLEUPDATE()函数。
由于在开始编辑时就锁定数据源的对应行,所以这种模式不被远程数据处理采用。 - 开放式行缓冲。所谓“开放”,就是“更新时锁定”的意思,“行缓冲”是指“只缓冲处理一笔使用者加以编辑的数据记录”。因此使用这种模式,只有在执行以下两项动作时,数据变动才会被发送,数据源对应行记录才被锁定:移动数据指针、执行TABLEUPDATE()函数。
- 保守式表缓冲。所谓“保守”,就是“编辑时锁定”的意思,“表缓冲”是指“缓冲处理整个使用者加以编辑的数据集(光标)”。因此一旦使用这种模式,当编辑动作刚开始,数据源的相关记录集便被锁定,而且在执行以下动作时,数据变动才会被发送:执行TABLEUPDATE()函数。
由于在开始编辑时就锁定数据源的整个对应表或是记录集,所以这种模式不被远程数据处理采用。 - 开放式行缓冲。所谓“开放”,就是“更新时锁定”的意思,“表缓冲”是指“缓冲处理整个使用者加以编辑的数据集(光标)”。因此使用这种模式,只有在执行TABLEUPDATE()函数时,数据变动才会被发送,数据源的相关记录集才被锁定。
好了,我们得到以下结论:在操控远程数据时,Visual FoxPro将对光标采用“开放式行缓冲”或“开放式表缓冲”,默认设置是“开放式行缓冲”。
以后在讨论远程数据处理时,不特别指出,行缓冲就是指开放式行缓冲,表缓冲是指开放式表缓冲。
在“开放式行缓冲”下,因为只对一条被编辑的记录开启缓冲,所以有两种方式可以确认编辑、发送更新:移动指针(在上面的例子中我们已经使用过了)、TABLEUPDATE()函数。不知您能否理解“指针移动确认更新”的意思?我是这样理解的:行缓冲只对一条被编辑的记录有用,如果移动指针,那就必须确认更新(如果数据有变动),因为如果不确认更新(释放缓冲区),Visual FoxPro便没法为下一行制定缓冲区了——记住:这是行缓冲。
“在开放式表缓冲”下,Visual FoxPro对整个记录集开启缓冲区,所以移动指针并不会确认更新。只有使用TABLEUPDATE()函数了。
乍一看,“开放式行缓冲”比“开放式行缓冲”需要更少的系统资源,好像是个好选择,我看不尽然:
- 有些Visual FoxPro的命令或函数会“不由自主”地移动指针,使得开发人员对更新的确认失去控制。
- 有时对数据的维护是成批的。
下面的代码说明了怎样控制缓冲:
USE VCustomers
CURSORSETPROP("Buffering", 3, "VCustomers")
*设定VCustomers的缓冲模式为“开放式行缓冲”。由于这时Visual FoxPro的默认设置,这一句可省略。
USE VOrders
CURSORSETPROP("Buffering", 5, "VOrders")
*设定VOrder的缓冲模式为“开放式表缓冲”。
以缓冲理解更新冲突
在图8中我在发生更新错误时提示:“原先Phone=030-0074321,现在Phone=00000,两者不等……”,那么这个原先是“什么时候”,“现在”又是怎样的概念?(假设进程一、二采用行缓冲模式、用“关键字和已更新字段”检测冲突)。
“原先”是指:进程一中视图被打开或是最近一次刷新成功时刻SQL Server数据表中的记录值。让我们先停下来,怎样才会刷新光标和缓冲呢?
- 远程视图光标被打开(无论使用行缓冲还是表缓冲)。
- 成功执行REQUERY()函数(无论使用行缓冲还是表缓冲)。
- 发送更新(无论成功与否)
您可以想象:进程一打开远程视图,Visual FoxPro自动把这个时刻SQL Server的数据值压入缓存中,这时进程一认为:我对SQL Server上数据的修改应建立在这个基础上,即Phone=030-0074321,如果这个基础不存在了,这发生更新错误。
在进程一还没有把它在客户端对Phone的修改发送到数据源的时候,进程二也读取了SQL Server的数据,注意这时进程二认为:我对SQL Server上数据的修改应建立在这个基础上,即Phone=030-0074321。于是进程二修改Phone为00000,并在进程一之前确认的数据变动,这时是不会发生更新冲突的,因为进程二修改数据的依据是成立的。
进程一慢慢吞吞地把数据改为了123456,发送更新。这时问题来了:进程一告诉SQL Server这样修改数据:
sp_executesql N'UPDATE dbo.Customers SET Phone=@P1 WHERE CustomerID=@P2 AND Phone=@P3', N'@P1 nvarchar(24),@P2 varchar(50),@P3 nvarchar(24)', N'123456 ', 'ALFKI', N'030-0074321'
我把参数填入,您就能很清楚地看到问题所在:
UPDATE dbo.Customers SET Phone='123456' WHERE CustomerID='ALFKI' AND Phone='030-0074321'
看到没有:Visual FoxPro自动的把缓冲里的Phone=030-0074321(在BROWSE 窗口中您已经把Phone改为了123456,Visual FoxPro“早有预谋”,把原始数据存在缓冲中,任你表面变化万次——我都不怕)拿出来并结合关键字作为更新依据,然而由于进程二已经修改了Phone的值,在SQL Server 中哪里还会有存在符合条件 CustomerID= 'ALFKI' AND Phone='030-0074321'的行了,只有CustomerID= 'ALFKI' AND Phone='00000'的记录行了。于是SQL Server 告诉 Visual FoxPro找不到目标记录,Visual FoxPro就对用户说:更新冲突。
所以,缓冲作用是就在这里:客户机与服务器通过 ODBC 这个翻译“传情达意”——但 ODBC 很苯——只能传一些SQL语句。事实上任何对数据的变动,都可归结为:Insert、Update、Delete。SQL语句与Visual FoxPro的命令函数有很大的不同——目标定位必须依靠条件语句(Where 子句)(Visual FoxPro可以很容易定位到第N行);缓冲为这些至关重要的定位条件提供了依据,没有缓冲就无法生成定位语句!
确认更新、放弃更新
确认更新
上文我们多次提到确认更新有基本上算是两种方式:移动指针、使用TABLEUPDATE()函数。移动指针只能在“开放式行缓冲“下使用,并且开发人员对法的可控性较差,一般用于交互式工具中,如上文我们使用过的SQL Server 的Enterprise Manager工具。这里我们只讨论TABLEUPDATE()函数。
在开放式行缓冲下使用TABLEUPDATE()函数:
- 语法:TABLEUPDATE(0[,lForce][,nWorkAear|cTableAlias])
- 返回值:更新成功——.T.,更新失败——.F.
- 必选参数:0。代表只更新当前记录到数据源——这里是记录缓冲,当然是:“只更新当前记录到数据源”。
- 可选参数——lForce。默认为.F.,指:如果发生更新错误就确认更新错误,本函数返回.F.;如果设此参数为.t., 表示发生更新错误时,以本客户端的新值为准,覆盖网络上被确认已经的其他用户的更新,如果覆盖成功,本函数返回.T.。
缺省表示本参数时取默认值。
该参数设为.T.的实质就是临时改变更新冲突的检测方式为“关键字段”,所以只要关键字不发生冲突,就不会发生更新冲突,本客户端的新值将覆盖其他用户做的变更。 - 可选参数——nWorkAear|cTableAlias。表示实行TABLEUPDATE()的工作区,缺省表示对当前工作区有效。
例如:
USE VCustomers
REPLACE PHONE WITH '123456'
?TABLEUPDATE(0)
*返回 .T.更新成功,反之失败。
USE VCustomers
REPLACE PHONE WITH '123456'
?TABLEUPDATE(0,.t.,'VCostomers')
*由于lForce设置为.t.,Visual FoxPro 临时修改更新检测方式为“关键字段”方式,所以只要关键字CustomerID不发生冲突,即使其他字段已经被其他用户修改,Visual FoxPro也不会检测,Visual FoxPro将强制覆盖其它用户做的修改。
*在“关键字段和已修改字段”的冲突检测方式下:Visual FoxPro向SQL Server发送如下语句:
sp_executesql N'UPDATE dbo.Customers SET Phone=@P1 WHERE CustomerID=@P2', N'@P1 nvarchar(24),@P2 varchar(50)', N'123456', 'ALFKI'
在开放式表缓冲下使用TABLEUPDATE()函数:
- 语法:TABLEUPDATE(nRows[,lForce][,nWorkAear|cTableAlias][,cErrorArray])
- 返回值:更新成功——.T.,更新失败——.F.
- 必选参数——nRows。可以有两种取值:1、2。假设用户对光标的第 1、2、4、5、7条记录作了修改,如果更新变动,第4条记录上将发生更新冲突。现在,执行本函数,Visual FoxPro将依次发送五条UPDATE-SQL描述:
设本参数为1时,执行到第4条记录时发生更新冲突,Visual FoxPro将停止发送的5、7条记录的更新描述,并使本函数返回.F.值,Visual FoxPro的记录指针停留在第四条记录上。
设本参数为2时,执行到第4条记录时发生更新冲突,Visual FoxPro将继续发送的5、7条记录的更新描述,并使本函数返回.F.值,Visual FoxPro的记录指针最终停在最后一条被修改的记录上(这里是第7条记录)。如果可选参数——cErrorArray存在,Visual FoxPro将把发生更新错误的记录号(RECNO())写入该数组,如果第5条记录也发生冲突,那么该数组将是一个一列两行的数组,cErrorArray[1]=4、cErrorArray[2]=5 - 可选参数——lForce。默认为.F.,指:如果发生更新错误就确认更新错误,本函数返回.F.;如果设此参数为.t., 表示发生更新错误时,以本客户端的新值为准,覆盖网络上被确认已经的其他用户的更新,如果覆盖成功,本函数返回.T.。
缺省表示本参数时取默认值。
该参数设为.T.的实质就是临时改变更新冲突的检测方式为“关键字段”,所以只要关键字不发生冲突,就不会发生更新冲突,本客户端的新值将覆盖其他用户做的变更。 - 可选参数——nWorkAear|cTableAlias。表示实行TABLEUPDATE()的工作区,缺省表示对当前工作区有效。
- 可选参数——cErrorArray。这是一个一列数组,且只有当必选参数nRows为2时有效,这时它记录着发生更新冲突的记录的记录号;如果没有发生任何更新冲突或是当必选参数nRows不为2时,本数组为一行一列,值为-1。
举个例子:(假设使用“关键字段和已修改字段”作为更新冲突检测方案)
USE VCustomers
CURSORSETPROP("Buffering", 5, "VCustomers")
REPLACE Phone with '9999' next 4
*将第1、2、3、4、条记录的Phone改为9999
BROWSE
*使用 SQL Server 的Query Analyzer 制造更新冲突
*启动 SQL Server Query Analyzer,登录Northwind 数据库
*输入如下语句并执行:
update customers set phone='00000' where customerid='ANATR' or customerid='ANTON'
情况一:TABLEUPDATE(1,.F.,'Vcustomers')
*返回Visual FoxPro
?TABLEUPDATE(1,.F.,'Vcustomers')
*由于记录 2更新时发生冲突,函数返回.F.
?recno('Vcustomers')
*指针停在第2条记录上
?Aerror(err)
用Aerror函数取得Visual FoxPro错误信息存入err数组中
?err(1)
*错误号:1585
?err(2)
*错误信息:“更新冲突”
*回到SQL Server Query Analyzer
*输入如下语句并执行:
select customerid,phone from customers
*您将看到:第一条记录Phone的值已经被Visual FoxPro的客户端修改了,值是:9999。而后三条记录没有发生变化。说明Visual FoxPro依次发送SQL描述到SQL Server时,遇到更新错误就停止继续往下工作。
事实上,查看 SQL Server 的 Profiler 工具也证明了以上论述:
sp_executesql N'UPDATE dbo.Customers SET Phone=@P1 WHERE CustomerID=@P2 AND Phone=@P3', N'@P1 nvarchar(24),@P2 varchar(50),@P3 nvarchar(24)', N'9999 ', 'ALFKI', N'bbbb '
sp_executesql N'UPDATE dbo.Customers SET Phone=@P1 WHERE CustomerID=@P2 AND Phone=@P3', N'@P1 nvarchar(24),@P2 varchar(50),@P3 nvarchar(24)', N'9999 ', 'ANATR', N'1234 '//此处发生更新错误,Visual FoxPro停止往下工作
情况二:TABLEUPDATE(1,.T.,'Vcustomers')
*返回Visual FoxPro
?TABLEUPDATE(1,.T.,'Vcustomers')
*函数返回.T.
?recno('Vcustomers')
*指针停在第4条记录上
*回到SQL Server Query Analyzer
*输入如下语句并执行:
select customerid,phone from customers
*您将看到:头四条记录Phone的值已经被Visual FoxPro的客户端修改了,值是:9999。按理说第2条记录被更新时会发生冲突,但由于Visual FoxPro临时变更了更新冲突的检测方案为“关键字段”,这样原本应该能检测到的冲突被忽略了,Visual FoxPro客户端的新值强行覆盖其它客户端的修改。
事实上,查看 SQL Server 的 Profiler 工具也证明了以上论述:
sp_executesql N'UPDATE dbo.Customers SET Phone=@P1 WHERE CustomerID=@P2', N'@P1 nvarchar(24),@P2 varchar(50)', N'9999 ', 'ALFKI'
sp_executesql N'UPDATE dbo.Customers SET Phone=@P1 WHERE CustomerID=@P2', N'@P1 nvarchar(24),@P2 varchar(50)', N'9999 ', 'ANATR'
sp_executesql N'UPDATE dbo.Customers SET Phone=@P1 WHERE CustomerID=@P2', N'@P1 nvarchar(24),@P2 varchar(50)', N'9999 ', 'ANTON'
sp_executesql N'UPDATE dbo.Customers SET Phone=@P1 WHERE CustomerID=@P2', N'@P1 nvarchar(24),@P2 varchar(50)', N'9999 ', 'AROUT'
情况三:TABLEUPDATE(2,.F.,'Vcustomers',Arry)
*返回Visual FoxPro
?TABLEUPDATE(2,.F.,'Vcustomers',Arry)
*参数nRows设为2,即使记录 2、3发生更新冲突,Visual FoxPro仍然继续往下执行,但参数函数返回.F.
?recno('Vcustomers')
*指针停在第4条记录上
?Aerror(err)
用Aerror函数取得Visual FoxPro错误信息存入err数组中
?err(1)
*错误号:1585
?err(2)
*错误信息:“更新冲突”
?Arry[1]
*2
?Arry[2]
*3
*Arry返回发生更新错误的记录号
*回到SQL Server Query Analyzer
*输入如下语句并执行:
select customerid,phone from customers
*您将看到:第一条、第四条记录Phone的值已经被Visual FoxPro的客户端修改了,值是:9999。而第二条、第三条记录没有发生变化。说明Visual FoxPro依次发送SQL描述到SQL Server时,遇到更新冲突忽略之,继续往下工作。
事实上,查看 SQL Server 的 Profiler 工具也证明了以上论述:
sp_executesql N'UPDATE dbo.Customers SET Phone=@P1 WHERE CustomerID=@P2 AND Phone=@P3', N'@P1 nvarchar(24),@P2 varchar(50),@P3 nvarchar(24)', N'9999 ', 'ALFKI', N'cccc '
sp_executesql N'UPDATE dbo.Customers SET Phone=@P1 WHERE CustomerID=@P2 AND Phone=@P3', N'@P1 nvarchar(24),@P2 varchar(50),@P3 nvarchar(24)', N'9999 ', 'ANATR', N'cccc '//发生更新冲突,Visual FoxPro继续往下执行
sp_executesql N'UPDATE dbo.Customers SET Phone=@P1 WHERE CustomerID=@P2 AND Phone=@P3', N'@P1 nvarchar(24),@P2 varchar(50),@P3 nvarchar(24)', N'9999 ', 'ANTON', N'cccc '//发生更新冲突,Visual FoxPro继续往下执行
sp_executesql N'UPDATE dbo.Customers SET Phone=@P1 WHERE CustomerID=@P2 AND Phone=@P3', N'@P1 nvarchar(24),@P2 varchar(50),@P3 nvarchar(24)', N'9999 ', 'AROUT', N'cccc '
情况四:TABLEUPDATE(2,.T.,'Vcustomers',Arry)
*返回Visual FoxPro
?TABLEUPDATE(2,.T.,'Vcustomers',Arry)
*但参数函数返回.T.
?recno('Vcustomers')
*指针停在第4条记录上
?Arry[1]
*-1
*没有发生更新错误
*回到SQL Server Query Analyzer
*输入如下语句并执行:
select customerid,phone from customers
*您将看到:所有记录Phone的值已经被Visual FoxPro的客户端修改了,值是:9999。按理说第2条、第3条记录被更新时会发生冲突,但由于Visual FoxPro临时变更了更新冲突的检测方案为“关键字段”,这样原本应该能检测到的冲突被忽略了,Visual FoxPro客户端的新值强行覆盖其它客户端的修改。
事实上,查看 SQL Server 的 Profiler 工具也证明了以上论述:
sp_executesql N'UPDATE dbo.Customers SET Phone=@P1 WHERE CustomerID=@P2', N'@P1 nvarchar(24),@P2 varchar(50)', N'9999 ', 'ALFKI'
sp_executesql N'UPDATE dbo.Customers SET Phone=@P1 WHERE CustomerID=@P2', N'@P1 nvarchar(24),@P2 varchar(50)', N'9999 ', 'ANATR'
sp_executesql N'UPDATE dbo.Customers SET Phone=@P1 WHERE CustomerID=@P2', N'@P1 nvarchar(24),@P2 varchar(50)', N'9999 ', 'ANTON'
sp_executesql N'UPDATE dbo.Customers SET Phone=@P1 WHERE CustomerID=@P2', N'@P1 nvarchar(24),@P2 varchar(50)', N'9999 ', 'AROUT'
放弃更新
如果要“放弃客户端对光标已经实施的变动”,该怎么办呢?这很简单,请使用TABLEREVERT()函数。这里有一个概念很重要:任何情况下执行本函数均不都会与远程数据源发生通讯,Visual FoxPro只是从缓冲中把原先的数值取回填写入光标中。
那么Visual FoxPro是怎样从缓冲中取回数据的呢?您可以用OLDVAL()函数得到相同的效果,它的用法这里暂不介绍!
在开放式行缓冲下使用TABLEREVERT()函数:
- 语法:TABLEUPDATE(.f.[,nWorkAear|cTableAlias])
- 返回值:1。如果当前记录没有被修改,则返回0。
- 可选参数——nWorkAear|cTableAlias。表示实行TABLEREVERT()的工作区,缺省表示对当前工作区有效。
例如:
USE VCustomers
CURSORSETPROP("Buffering", 3, "VCustomers")
REPLACE Phone with '9999'
?VCustomer.phone
*9999
?TABLEREVERT(.F.,'VCustomers')
*返回1
?VCustomer.phone
*123456
USE VCustomers
CURSORSETPROP("Buffering", 3, "VCustomers")
?TABLEREVERT(.F.,'VCustomers')
*返回0
在开放式表缓冲下使用TABLEREVERT()函数:
- 语法:TABLEUPDATE(lAllRows[,nWorkAear|cTableAlias])
- 返回值:放弃更新的记录数目
- 必选参数——lAllRows。默认值为.F.,表示对当前记录放弃更新;本参数设定为 .T.,放弃更新所有被修改过的记录。
- 可选参数——nWorkAear|cTableAlias。表示实行TABLEREVERT()的工作区,缺省表示对当前工作区有效。
USE VCustomers
CURSORSETPROP("Buffering", 5, "VCustomers")
REPLACE Phone with '9999' next 4
*将第1、2、3、4、条记录的Phone改为9999
BROWSE
go 2
?TABLEREVERT(.F.,'VCustomers')
*1
?TABLEREVERT(.T.,'VCustomers')
*3