第十七章:共享访问程序设计

如果创建的应用程序在网络环境中的多台计算机上运行,或者一个表单的多个实例对相同的数据进行访问,这时就要进行共享访问程序设计。共享访问意味着不仅要为多个用户使用和共享数据提供更有效的方法,并且在必要时要对访问进行限制。

Visual FoxPro 支持如下功能:对数据的共享或独占访问;锁定选项;数据工作期;记录缓冲和表缓冲以及事务处理。尽管这些功能主要用在共享环境里,但在单用户环境下也可以使用。

本章内容要点:

控制对数据的访问

因为访问数据是在文件内进行,所以有效的数据管理首先从控制这些文件的环境开始。需要选择访问数据的方式,还要选择在什么时间、如何限制对数据的访问。

访问数据

在共享环境中,可以用两种方式访问数据:从独占文件中访问或者从共享文件中访问。若打开一个共享访问的表,其他用户也能对该文件进行访问;若打开一个独占访问的表,那么其他用户就不能对该文件进行读写操作。独占访问不具备在网络环境中共享数据的许多优点,因而应该避免使用。

以独占访问的方式使用表

打开一个文件最严格的限制方式就是独占方式。通过界面打开一个表时,默认情况下是独占使用的。也可以利用 Visual FoxPro 命令明确地申明以独占方式打开一个表。

若要打开一个独占使用的表

下列命令要求以独占方式打开一个表:

如果在一个共享表中执行上述命令,Visual FoxPro 将返回出错信息:“文件必须以独占方式打开”。

可以使用 FLOCK( ) 函数限制对表的访问。如果使用 FLOCK( ) 锁定表,则其他用户不能对该表进行写操作,但可以读该表。

以共享访问的方式使用表

以共享方式打开一个表时,多个机器可同时访问该表。通过界面打开表时,可以不理会 SET EXCLUSIVE 默认的 ON 设置,而明确地用 Visual FoxPro 命令打开一个表供共享使用。

若要打开一个共享使用的表

当在一个共享表中添加或更改数据时,必须首先锁定要更改的记录或整个表。对一个以共享方式打开的表或记录,可通过下列方式进行锁定:

与共享表相关的备注和索引文件也是以相同的共享方式打开的。

如果应用程序所使用的表只用于查找数据,而且该应用程序的所有用户都要访问它,那么将该表标记为只读可以提高性能。

锁定数据

如果要共享访问文件,必须通过锁定表和记录来对这种访问进行管理。锁定不同于访问权限,它既可以对数据进行长期控制,也可以短期控制。Visual FoxPro 提供自动和人工两种锁定方式。

选择记录或表锁定

无论是自动记录锁定还是人工记录锁定,目的都是为了防止两个用户同时写一个记录。表锁定用来防止其他用户在表中进行写入操作,但允许读取整个表。由于表锁定阻止其他用户更新表中记录,因而很少使用。

选择自动或人工锁定

除了选择记录锁定或者表锁定,还可以选择自动锁定或者人工锁定。许多 Visual FoxPro 命令在执行之前都会自动锁定一个记录或一个表,如果成功锁定了记录或表,则执行该命令,然后再解锁。

自动锁定记录和表的命令

命令 锁定范围
ALTER TABLE 整个表
APPEND 表头
APPEND BLANK 表头
APPEND FROM 表头
APPEND FROM ARRAY 表头
APPEND MEMO 当前记录
BLANK 当前记录
BROWSECHANGEEDIT 一旦开始编辑字段,锁定对象为当前记录和相关表中别名字段的所有记录
CURSORSETPROP( ) 取决于参数
DELETE 当前记录
DELETE NEXT 1 当前记录
DELETE RECORD n 记录 n
删除多个记录的 DELETE 整个表
DELETE - SQL 当前记录
GATHER 当前记录
INSERT 整个表
INSERT - SQL 表头
MODIFY MEMO 编辑开始时,锁定当前记录
READ 当前记录和别名字段的所有记录
RECALL 当前记录
RECALL NEXT 1 当前记录
RECALL RECORD n 记录 n
恢复多个记录的 RECALL 整个表
REPLACE 当前记录和别名字段的所有记录
REPLACE NEXT 1 当前记录和别名字段的所有记录
REPLACE RECORD n 记录 n 和别名字段的所有记录
REPLACE 替换多个记录 整个表和别名字段的所有记录
SHOW GETS 当前记录和别名字段引用的所有记录
TABLEUPDATE( ) 取决于缓冲
UPDATE 整个表
UPDATE - SQL 整个表

记录锁定的特点

锁定记录的命令比锁定表的命令破坏性小。锁定一个记录时,其他用户仍然可以添加或删除其他记录。如果记录或表已经被其他用户锁定,或者表已经被其他用户以独占方式打开,则锁定记录或表的操作失败。如果不能锁定记录,则尝试锁定当前记录的命令将返回出错信息:“其他用户正在使用记录”。

BROWSECHANGEEDITMODIFY MEMO 命令只有在编辑记录时才锁定记录。如果正在编辑来自相关表记录中的字段,那么该相关记录可能会被锁定。如果当前记录或任一相关记录已被其他用户锁定,则锁定失败。如果锁定成功,则可以编辑该记录;在移到另一个记录或激活其他窗口时,记录解锁。

表头和表锁定的特点

一些 Visual FoxPro 命令锁定整个表,而有些命令则只锁定表头。表锁定命令比表头锁定命令限制更严厉。锁定表头时,其他用户不能添加或删除记录,但可以修改字段内的数据。

当发出 APPEND BLANK 命令时,用户可以共享表而不发生冲突,但是当其他用户也向该表追加一个空白记录时,就会出错。将得到这样一个错误信息:“其他用户正在使用文件”。这个信息表明有两个或多个用户在同时执行 APPEND BLANK 命令。如果不能锁定一个表时,却企图去锁定整个表,则将返回错误信息:“其他用户正在使用文件”。要取消正在进行的锁定操作,可按 ESC 键。

示例:自动锁定

在下面的示例中,即使 customer 表是以共享方式打开的,用户为了从其他表追加记录也将自动锁定整个表。

SET EXCLUSIVE OFF
USE customer
APPEND FROM oldcust FOR status = "OPEN"

人工锁定

可以用锁定函数人工锁定一个记录或一个表。

若要人工锁定一个记录或一个表

RLOCK( ) 函数等同于 LOCK( ) 函数,都可以锁定一个或多个记录;FLOCK( ) 锁定一个文件。LOCK( ) 和 RLOCK( ) 函数可以用于锁定表头。如果把 0 作为记录编号提供给 LOCK( ) 或 RLOCK( ),而且测试表明表头未锁定,则该函数将锁定表头并返回“真”(.T.)。

如果锁定了一个记录或一个表,那么一定要尽快解锁,以方便其他用户访问。解锁可使用 UNLOCK 命令。

这些人工锁定函数可以完成如下操作:

如果锁定记录或表的操作失败,SET REPROCESS 命令和当前错误处理例程将确定是否再次尝试锁定。SET REPROCESS 会影响不成功的锁定结果,因此可以使用 SET REPROCESS 来控制锁定尝试的次数以及时间。

示例:人工锁定

下例以共享方式打开 customer 表,并用 FLOCK( ) 函数锁定该表。如果该表锁定成功,则 REPLACE ALL 将更新该表中的所有记录,UNLOCK 对该文件解锁;如果因为其他用户已经锁定该文件或文件中的记录,所以不能锁定该表,将显示一条信息。

SET EXCLUSIVE OFF
SET REPROCESS TO 0
USE customer    && 打开共享表
IF FLOCK()
 REPLACE ALL contact ;    && 替换并解锁
  WITH UPPER(contact) 
 UNLOCK   
ELSE  && 输出信息
WAIT "File in use by another." WINDOW NOWAIT
ENDIF

解锁数据

在共享环境下锁定记录或文件并完成了相应的数据操作之后,应及时解锁。有几种解锁的办法。有时,只需简单地移动到下一个记录就能解锁,其他情况则需要明确的命令。

要解锁被自动锁定的记录,只需移动记录指针,甚至在设置 MULTILOCKS ON 的情况下也是如此。而对于人工锁定的记录,则必须明确地对记录解锁,仅仅移动记录指针是不够的。

下表说明了对人工和自动的记录锁定和表锁定进行解锁的各命令:

命令 效果
UNLOCK 解锁当前工作区内的记录和文件。
UNLOCK ALL 对当前工作期内所有的工作区解锁。
SET MULTILOCKS OFF 建立新锁定的同时自动解锁当前锁定。
FLOCK( ) 在锁定文件之前解锁文件中的所有记录。
CLEAR ALLCLOSE ALL
USEQUIT
对所有记录和文件解锁。
END TRANSACTION 对所有的自动锁定项解锁。
TABLEUPDATE( ) 在更新表之前解锁所有锁定项。

注意 如果记录在 UDF 中被自动锁定,那么当移开记录指针然后又移回该记录时,锁定将被解除。可以使用表缓冲避免这个问题。

使用数据工作期

为确保共享环境中的每个用户都具有安全、正确的环境副本,确保表单的多个实例能独立操作,Visual FoxPro 提供了数据工作期。

数据工作期是对当前动态工作环境的描述。可以将数据工作期看成是一个小型的数据环境,这个环境运行在同一机器上一个开放的 Visual FoxPro 工作期内。每个数据工作期包括:

请考虑在多用户应用程序的各自工作站同时打开相同的表单时,会发生什么,这样可以很容易理解数据工作期的概念。这种情况下,每个机器运行一个独立的 Visual FoxPro 工作期,因此有自己的工作区设置以及代表打开的基表、索引和关系的临时表。

但是,如果在一台机器上,同一个 Visual FoxPro 工作期,打开单个项目中的同一个表单的多个实例,则这些表单共享默认的数据工作期,它代表了单独的一个动态工作环境。在同一个 Visual FoxPro 工作期中打开的开启表单的每一实例都使用相同的工作区设置,并且在一个表单实例中移动工作区中的记录指针会自动影响相同表单的其他实例。

使用私有数据工作期

如果想更多地控制表单的多个实例,可以使用私有数据工作期。当表单使用私有数据工作期时,Visual FoxPro 为应用程序创建的表单、表单集或工具栏控件的每个实例新建一个数据工作期。每个私有数据工作期包括:

可用数据工作期数目只受可用的系统内存和磁盘空间的限制。

通过设置表单的 DataSession 属性来实现私有数据工作期。DataSession 属性有两个设置:

在默认情况下,表单的 DataSession 属性设置为 1。

若要使用私有数据工作期

请选择下列选项之一:

当表单使用的是私有数据工作期时,在单个的机器上,一个 Visual FoxPro 工作期中打开表单的每个实例都使用自己的数据环境。使用私有数据工作期类似于在不同的工作站上同时运行同一个表单。

等价的多个数据工作期

识别数据工作期

每个私有数据工作期是单独识别的。可以在“数据工作期”窗口中查看每个数据工作期的内容。也可以通过在 Load 事件代码中的命令改变数据工作期的说明。

使用 DataSessionID 运行时刻属性,可以查看每个数据工作期的识别号。下面的示例显示名为 frmMyForm 的表单的 DataSessionID 属性。

DO FORM frmMyForm
? frmMyForm.DataSessionID

如果使用 NAME 子句激活表单,可使用该表单的名称访问 DataSessionID 属性,如下面的代码所示:

DO FORM MyForm NAME one
? one.DataSessionID

DataSessionID 属性只是用来识别特殊的数据工作期。不要更改一个表单实例的 DataSessionID 属性,因为更改 DataSessionID 属性时,与数据绑定的控件会丢失它们的数据源。

使用多个表单实例更新数据

私有数据工作期生成各自独立的工作区,工作区包含了表单的开启表、索引和关系的独立备份,表单的每个备份引用了相同的基表和索引文件。当用户从表单的一个实例中更新记录时,更新该表单引用的基表。当定位到更改的记录时,就可以看到表单另一个实例所作的更改。

如果在一个私有数据工作期对记录或表进行了锁定,其他私有数据工作期就不能再进行锁定。例如,数据工作期 1 的用户锁定了一个记录,数据工作期 2 的用户就不能锁定这个记录了。如果数据工作期 1 的用户以独占方式打开了一个表,数据工作期 2 的用户就不能打开这个表了。通过遵守其他数据工作期所作的锁定,Visual FoxPro 可以保护基表更新的完整性。

定制数据工作期的环境

由于数据工作期控制着某些 SET 命令的范围,可以在单一的 Visual FoxPro 工作期中使用私有数据工作期以建立自定义的 SET 命令设置。

例如,SET EXACT 命令,控制比较不同长度字符串时使用的规则,作用于当前数据工作期。SET EXACT 命令的默认设置为 OFF 时,规定表达式右边直到末尾的每个字符都完全匹配时,两个表达式才算相等。通过在默认的数据工作期中将 SET EXACT 命令设置为 OFF,可以使用“模糊”搜索或相似查找;但是,应用程序可能包含特殊的表单,它需要精确匹配。可以将需要精确匹配的表单的 DataSession 属性设置为 2,使用私有数据工作期,然后将 SET EXACT 命令设置为 ON。由于仅在使用私有数据工作期的表单时,使用 SET 命令,则当为一个特定的表单启用自定义工作期设置时,可以保留整个 Visual FoxPro 工作期设置。

使自动私有数据工作期的设置无效

在使用表单的私有数据工作期时,在一个表单中对数据所作的更改不会自动地体现到相同表单的其他实例中。如果想让表单的所有实例都访问相同的数据,并且立即反映对公共数据所作的更改,则可以取消自动数据工作期的设置。

若要使自动数据工作期的设置无效

这两个命令都将启用命令窗口以及项目管理器所控制的默认数据工作期。

缓冲访问数据

如果希望在更新过程中保护数据,可以使用缓冲技术。在多用户环境中,Visual FoxPro 的记录缓冲和表缓冲技术可以保护对单个记录或多个记录所做的数据更新以及数据维护操作。缓冲区可以自动测试、锁定以及解锁记录或表。

借助缓冲技术,可以很容易地检测并解决数据更新操作过程中所遇到的冲突:当前记录被复制到一个由 Visual FoxPro 进行管理的内存或磁盘区域,其他用户仍然可以同时访问原来的记录。当离开该记录或以编程方式更新该记录时,Visual FoxPro 将准备锁定该记录,确认其他用户没有做修改,然后写入编辑结果。在试图更新数据时,还必须解决冲突,若有冲突则应防止编辑结果写入原来的表。

选择缓冲方法

在启用缓冲之前,应对数据环境进行评估,根据应用程序的编辑需要、记录以及表的类型和大小,根据信息的使用与更新方式以及其他因素,选择缓冲方法和锁定选项。记录和表的类型和大小、如何使用和更新信息、以及其他参数。一旦启用了缓冲后,则在废止缓冲或关闭表之前一直保持有效。

Visual FoxPro 有提供两种缓冲:记录缓冲和表缓冲。

Visual FoxPro 记录缓冲和表缓冲

选择锁定方式

Visual FoxPro 以两种锁定方式提供缓冲:保守式和开放式。这两种方式决定了一个或多个记录何时被锁定,何时被解锁以及解锁的方式。

保守式缓冲

在多用户环境中,保守式缓冲能防止其他用户在对某一特定记录或表正进行修改时访问它。保守式缓冲为单个记录的修改提供最安全的工作环境,但是会降低用户的操作速度。这种缓冲方式非常类似于 FoxPro 以前版本的标准锁定机制,此外它还带有内在数据缓冲等更多的好处。

开放式缓冲

开放式缓冲是更新记录的有效方法,因为锁定只在写记录时生效,这样在多用户环境中使单个用户独占系统的时间最少。在视图上使用记录或表缓冲时,Visual FoxPro 将强制使用开放式锁定。

由函数 CURSORSETPROP( ) 设置的 Buffering 属性值决定了缓冲和锁定的方法。

下表列出了 Buffering 属性的有效值:

若要启用 请使用此缓冲值
无缓冲。默认值。 1
保守式记录锁定。它锁定当前记录,在记录指针移动或发出 TABLEUPDATE( ) 命令后更新。 2
开放式记录锁定。它一直等到记录指针移动,然后锁定并更新。 3
保守式表锁定。它锁定当前记录,在发出 TABLEUPDATE( ) 命令之后更新。 4
开放式表锁定。它一直等到发布 TABLEUPDATE( ) 命令,然后锁定并更新已编辑的记录。 5

对于表,Buffering 的默认值为 1;对视图,默认值为 3。如果使用缓冲访问远程数据,则 Buffering 属性可以是 3(开放式记录缓冲),或 5(开放式表缓冲)。有关在远程表中访问数据的详细内容,请参阅《用户指南》中的第六章“查询和更新多表”

注释 对所有除 1 以外的缓冲方式,设置 MULTILOCKS 为 ON。

启用记录缓冲

可用 CURSORSETPROP( ) 函数启用记录缓冲。

若要在当前数据区启用保守式记录锁定

Visual FoxPro 在指针位置锁定记录。如果锁定成功,Visual FoxPro 将该记录放入缓冲区并允许编辑。在移动记录指针或发出 TABLEUPDATE( ) 命令时,Visual FoxPro 将把缓冲记录写入原来的表中。

若要在当前数据区启用开放式记录锁定

Visual FoxPro 将指针位置所在的记录写入缓冲区并允许编辑。当移动记录指针或发出 TABLEUPDATE( ) 命令时,Visual FoxPro 对该记录进行锁定。如果锁定成功,Visual FoxPro 将对磁盘上的记录的当前值与原来的缓冲区值进行比较。如果两值相同,则编辑结果写入原来的表;如果两值不同,则 Visual FoxPro 发出错误信息。

启用表缓冲

可用 CURSORSETPROP( ) 函数启用表缓冲。

若要在当前数据区对多个记录启用保守式锁定

Visual FoxPro 在记录指针位置锁定记录。如果锁定成功,Visual FoxPro 将把该记录放入缓冲区并允许编辑。使用 TABLEUPDATE( ) 命令将把缓冲记录写入原来的表。

若要在当前数据区对多个记录启用开放式锁定

在发出 TABLEUPDATE( ) 命令之前,Visual FoxPro 把记录写入缓冲区并允许编辑,然后 Visual FoxPro 对缓冲区内的每一记录执行下列操作:

在启用表缓冲之后,Visual FoxPro 只在 TABLEUPDATE( ) 命令之后更新。

在表缓冲区中追加和删除记录

可以在启用表缓冲之后追加和删除记录。追加的记录将添加到缓冲区末尾。要访问缓冲区中所有记录,包括追加的记录,请使用 RECNO( ) 函数。RECNO( ) 函数根据追加到表缓冲区中的记录情况返回对应的负值。例如,如果启用了表缓冲,编辑记录 7、8、9,然后追加三个记录,则此时缓冲区将包含 RECNO( ) 的值为 7、8、9、- 1、- 2 和 - 3。

编辑、追加记录后的缓冲区

只能使用 TABLEREVERT( ) 命令从缓冲区中删除追加记录。对于任何追加的记录,在维护序列时,TABLEUPDATE( ) 和 TABLEREVERT( ) 都删除 RECNO( ) 为负值的记录。

编辑、删除一个追加的记录,然后再追加一个记录后的缓冲区

在使用表缓冲时,可以用带负的 RECNO( ) 值的 GO 命令来访问指定的追加记录。例如,对于前面的示例,可以键入:

GO 7      && 转到第一个缓冲记录
GO -3      && 转到第六个缓冲记录(第三个追加记录)

若要追加记录到表缓冲区

追加的记录具有序列递增的负 RECNO( ) 值。

若要从表缓冲区中移去追加的记录

  1. 使用带负值的 GO 命令将记录指针定位到要删除的记录。

  2. 使用 DELETE 命令将该记录加上删除标记。

  3. 使用 TABLEREVERT( ) 函数将该记录从缓冲区中移去。

注释 TABLEREVERT( ) 函数也影响被删除和更改的行的状态。

若要从表缓冲区中移去所有追加的记录

TABLEREVERT( ) 把追加的记录从表缓冲区中移去,而不将这些记录写入表。TABLEUPDATE( ) 命令将所有当前位于缓冲区的记录写入一个表,包括已打上删除标记的记录。

更新数据

可以使用缓冲、事务处理或视图更新数据。

使用缓冲进行更新

在选择缓冲方法和锁定类型之后,就可启用记录或表缓冲。

若要启用缓冲

请选择下列选项之一:

然后将进行更新操作的代码放在控件适当的方法程序代码中。

要把编辑结果写入原来的表,可以使用 TABLEUPDATE( )。由于规则限制,造成对表的更新操作失败之后,若要取消编辑结果,可以使用 TABLEREVERT( ) 命令。TABLEREVERT( ) 即使在没有明确启用表缓冲的情况也是有效的。

下面的示例说明在启用保守式记录缓冲的情况下,如何更新记录。

使用记录缓冲和表缓冲进行更新的示例

代码 注释
OPEN DATABASE testdata
USE customers
= CURSORSETPROP('Buffering', 2)         
在表单的 Init 代码中,打开表并启用保守式记录缓冲。
lModified = .F.
FOR nFieldNum = 1 TO FCOUNT()         
   IF GETFLDSTATE(nFieldNum) = 2      
      lModified = .T.
      EXIT
   ENDIF
ENDFOR

遍历字段,检查是否有字段进行了修改。

注释 此代码可以在“保存”或“更新”命令按钮的 Click 事件中。

IF lModified
   nResult = MESSAGEBOX;
      ("Record has been modified. Save?", ;
      4+32+256, "Data Change")
定位下一个已修改的记录。
   IF nResult = 7                  
      = TABLEREVERT (.F.)            
   ENDIF
ENDIF
显示当前值并为用户提供选项,以便还原对当前字段所做的修改。
SKIP                           
IF EOF()
   =MESSAGEBOX( "already at bottom")
   SKIP -1
ENDIF 
THISFORM.Refresh



SKIP 确保最后一个修改内容也已写入。

使用事务处理管理更新

即便使用缓冲,事情有时也会出错。如果希望保护更新操作,并从作为一个单位的整段代码中还原回来,可以使用事务处理。

在应用程序中添加事务处理所提供的保护机制超过了记录缓冲和表缓冲提供的保护功能,它将整段代码作为一个受保护的、可恢复的单元。可以嵌套使用事务处理,并用它保护已操作的缓冲更新。Visual FoxPro 事务处理只能用于数据库中的表和视图。

包装代码段

事务处理类似一层外包装,用于高速缓存对内存或硬盘的数据更新操作,而不直接对数据库进行更新。实际的数据库更新在事务处理结束之后进行。如果由于某种原因,系统不能执行对数据库的更新操作,就可以回滚整个事务处理,而不执行任何更新。

注释 在同一数据工作期的事务处理中,事务处理外面的缓冲更新操作将被忽略。

控制事务处理的命令

Visual FoxPro 提供了三个命令一个函数来控制事务处理:

若要 请使用此命令
初始化一个事务处理。 BEGIN TRANSACTION
确定当前事务处理的等级。 TXNLEVEL( )
取消最近一条 BEGIN TRANSACTION 语句以来所做的全部修改。 ROLLBACK
锁定记录,提交最近一条 BEGIN TRANSACTION 语句以来对数据库中表所做的全部修改,然后解锁记录。 END TRANSACTION

可以用事务处理缓冲对以下各项的修改:表、结构化的 .cdx 文件以及与数据库内表相关的备注文件。涉及内存变量和其他对象的操作不属于事务处理,因此不能回滚或提交这些操作。

注释 使用远程表中存储的数据时,事务处理命令控件只更新视图临时表的本地副本中的数据;对远程基表的更新不起作用。若要对远程表启用人工事务处理,请使用 SQLSETPROP( ),然后用 SQLCOMMIT( )SQLROLLBACK( ) 控制事务处理。

一般来讲,除非缓冲 TABLEUPDATE( ) 调用,事务处理最好和记录缓冲一起使用,而不与表缓冲一起使用。如果在事务处理中使用了 TABLEUPDATE( ) 命令,那么可以回滚一个失败的更新操作,找到失败的原因,然后重试 TABLEUPDATE( ) 命令而不丢失任何数据。这样,就确保了更新操作一定是所谓的“要么全都做,要么全不做”的操作。

尽管在正常情况下,简单的事务处理过程能够提供安全的数据更新操作,但是它并不能提供对系统失败的完全保护。如果在处理 END TRANSACTION 命令期间断电或产生其他系统中断,则数据更新仍将失败。

请使用下列事务处理代码模板:

BEGIN TRANSACTION   
* 更新数据
IF lSuccess = .F. && 出错
   ROLLBACK
ELSE && 执行修改
   * 确认数据
   IF && 出错
      ROLLBACK
   ELSE 
      END TRANSACTION
   ENDIF
ENDIF

使用事务处理

下列规则适用于事务处理:

事务处理完成了下列锁定动作:

嵌套事务处理

对于分隔在不同并发进程中的表,嵌套事务处理可以为表的更新操作提供逻辑组。事务处理命令 BEGIN TRANSACTION ... END TRANSACTION 不需要在同一函数或过程中。下列规则适用于嵌套事务处理:

请注意下面的示例,因为在嵌套事务处理中所做的修改没有写入磁盘,而写入事务处理缓冲区,内层事务处理将覆盖以前的事务处理对同一 STATUS 字段所做的修改:

BEGIN TRANSACTION &&  事务处理 1
   UPDATE EMPLOYEE ; &&  第一次修改
      SET STATUS = "Contract" ;
      WHERE EMPID BETWEEN 9001 AND 10000
   BEGIN TRANSACTION &&  事务处理 2
      UPDATE EMPLOYEE ;
         SET STATUS = "Exempt" ;
         WHERE HIREDATE > {^1998-01-01}  &&  改写
   END TRANSACTION &&  事务处理 2
END TRANSACTION &&  事务处理 1

下面的嵌套事务处理示例删除一条客户记录及其所有相关发票。如果在 DELETE 命令执行过程中出错,则事务处理回滚。这个示例表明了如何对表更新操作进行分组,从而防止部分更新以及并发冲突。

下面的示例说明在启用保守式记录缓冲的情况下,如何更新记录

代码 注释
DO WHILE TXNLEVEL( ) > 0
   ROLLBACK
ENDDO
脱离其他事务处理。
CLOSE ALL
SET MULTILOCKS ON
SET EXCLUSIVE OFF
建立缓冲环境。
OPEN DATABASE test
USE mrgtest1
=CURSORSETPROP('buffering',5)
GO TOP


启用开放式表缓冲。
REPLACE fld1 WITH "changed"
SKIP
REPLACE fld1 WITH "another change"
=MESSAGEBOX("modify first field of both records on another machine")
更改一个记录。

更改另一个记录。
BEGIN TRANSACTION
lSuccess = TABLEUPDATE(.T.,.F.)
开始事务处理 1,并且非强制的更新所有已修改的记录
IF lSuccess = .F.
   ROLLBACK
   =AERROR(aErrors)
   DO CASE 
   CASE aErrors[1,1] = 1539            
   ...
   CASE aErrors[1,1] = 1581            
   ...
   CASE aErrors[1,1] = 1582            
如果更新失败,回滚事务处理。

由 AERROR( ) 获得错误信息。
确定失败原因。
如果是触发器失败,进行相应处理。

如果是字段不接受 null 值,进行相应处理。

如果是违反了字段规则,进行相应处理。
   CASE aErrors[1,1] = 1585            
      nNextModified = getnextmodified(0)
      DO WHILE nNextModified <> 0
         GO nNextModified
         =RLOCK()
         FOR nField = 1 to FCOUNT()
            cField = FIELD(nField)
            if OLDVAL(cField) <> ;
               CURVAL(cField)
如果一个记录被另一个用户更改,则定位第一个已修改的记录。
循环遍历所有已修改的记录。

从第一个记录开始。

锁定每个记录以确保可以进行更新。
检查每个字段的修改。

比较缓冲值和磁盘上的值,然后出现对话框。

               nResult = MESSAGEBOX;
               ("Data has been changed ;
               by another user. Keep ;
               changes?",4+48, ;
               "Modified Record")
               IF nResult = 7
                  =TABLEREVERT(.F.)
                  UNLOCK record ;
                     nNextModified
               ENDIF
如果用户响应“No”,则还原一个记录
并解锁。
               EXIT
            ENDIF
         ENDFOR
脱离“FOR nField...”循环。
      ENDDO
获得下一个已修改的记录。
      BEGIN TRANSACTION
      =TABLEUPDATE(.T.,.T.)
      END TRANSACTION
      UNLOCK
开始事务处理 2 并强制性地更新所有未还原记录。

结束事务处理 2。
解锁。
   CASE aErrors[1,1] = 1700   
   ...
   CASE aErrors[1,1] = 1583    
   ...
   CASE aErrors[1,1] = 1884    
   ...
   OTHERWISE
      =MESSAGEBOX( " Unknown error;
      message: " + STR(aErrors[1,1]))
   ENDCASE
如果记录正被其他用户使用,进行相应处理。

如果违反行规则,进行相应处理。

如果违反唯一索引规则,进行相应处理。

否则,出现对话框。
ELSE
   END TRANSACTION
ENDIF

结束事务处理 1。

保护远程更新

在对远程表进行数据更新的过程中,事务处理可以避免系统造成的错误。下面的示例使用了事务处理来缓冲对远程表的数据写入操作。

远程表事务处理示例

代码 注释
hConnect = CURSORGETPROP('connecthandle')
SQLSETPROP(hConnect, 'transmode',
DB_TRANSMANUAL)
获得连接句柄。
启用人工事务处理。
lSuccess = TABLEUPDATE(.T.,.F.)
IF lSuccess = .F
   =SQLROLLBACK (hConnect)
非强制更新所有记录。
如果更新失败,
回滚临时表关于连接的事务处理。
   =AERROR(aErrors)
   DO CASE 
   CASE aErrors[1,1] = 1539            
   ...
   CASE aErrors[1,1] = 1581            
   ...
   CASE aErrors[1,1] = 1582            
   ...
   CASE aErrors[1,1] = 1585            
      nNextModified = GETNEXTMODIFIED(0)
      DO WHILE nNextModified <> 0
         GO nNextModified
从 AERROR( ) 中获得错误信息。

如果是触发器失败,进行相应处理。

如果是字段不接受 null 值,进行相应处理。

如果是违反了字段有效性规则,进行相应处理。

如果是记录已被其他用户修改,进行相应处理。

从第一个记录开始,循环遍历所有已修改字段。
         FOR nField = 1 to FCOUNT()
            cField = FIELD(nField)
            IF OLDVAL(cField) <> ;
               CURVAL(cField)
               nResult = MESSAGEBOX;
               ("数据已经被其他用户 ;
               修改。 ;
               保留修改吗?",4+48,;
               "被修改的记录")
检查每个字段是否更改。

比较缓冲值和磁盘上的值,然后显示对话框。
               IF nResult = 7
                  =TABLEREVERT(.F.)
               ENDIF
               EXIT
            ENDIF
         ENDFOR
         nNextModified = ;
         GETNEXTMODIFIED(nNextModified)
      ENDDO
如果用户响应“No”,
还原一个记录。

脱离“FOR nField...”循环。


获得下一个已修改的记录。
      =TABLEUPDATE(.T.,.T.)
      =SQLCOMMIT(hConnect)
   CASE aErrors[1,1] = 109
   ...
   CASE aErrors[1,1] = 1583      
   ...
   CASE aErrors[1,1] = 1884   
   ...
   OTHERWISE
强制更新所有未还原记录。
提交事务处理。
如果记录正由其他用户使用,进行相应处理。

如果违反行规则,进行相应处理。

如果违反唯一索引规则,进行相应处理。

否则,出现对话框。
      =MESSAGEBOX( ;
      "Unknown error message: " ;
      + STR(aErrors[1,1]))
   ENDCASE
ELSE
   =SQLCOMMIT(hConnect)
ENDIF


提交事务处理(结束事务处理)。

性能管理

如果有一个正在工作的多用户应用程序,可以采用下面的方法提高程序性能:

将临时文件放在本地驱动器上

Visual FoxPro 在默认的 Temp 文件夹下创建自己的临时文件。文本编辑工作期也临时地创建被编辑文件的备份(一个 .bak 文件)。

如果本地工作站的硬盘上有足够的自由空间,将临时文件放在这个本地盘或 RAM 驱动器上可以提高程序性能。重定向这些文件到本地盘或 RAM 驱动器上,可以减少访问网络驱动器的次数,从而提高性能。

也可以在配置文件 Config.fpw 中使用 EDITWORK、SORTWORK、PROGWORK 及 TMPFILES 语句,为这些文件指定其他存放位置。有关管理文件的详细内容,请参阅《安装指南》中的第四章“对系统进行优化”

在排序和索引文件之间选择

当一个表中的数据相对稳定时,处理没有设置排序顺序的表会提高性能,这并不意味着已经排序整理过的表就不能或不应该利用索引文件 -- 象 SEEK 命令就需要一个索引,而且在定位记录时速度相当快。但是,使用 SEEK 完成对记录的定位后,就应该关闭索引。

安排对文件的独占访问

对于在其他用户不访问数据时运行的命令,象夜间的更新操作,可以通过以独占方式打开文件以提高性能。当文件以独占方式打开时,由于 Visual FoxPro 不需要测试记录或文件锁定的状态,因此性能得到提高。

控制锁定文件的时间

要减少用户对表或记录写操作的竞争,就必须减少锁定记录或锁定表的时间。可以通过在编辑完成之后而不是编辑过程中锁定记录来做到这一点。开放式行缓冲的锁定时间最短。

有关提高性能的详细内容,请参阅《安装指南》中的第四章“对系统进行优化”。有关提高客户/服务器应用程序性能的详细内容,可参考第二十二章“优化客户/服务器性能”

使用视图管理更新

可以使用 Visual FoxPro 视图具有的更新冲突管理技术来管理多用户对数据的访问。通过使用 WhereType 属性,视图控制发送到基于视图的基表的内容。可以为本地和远程视图设置该属性。WhereType 属性提供了四种设置:

通过选择四种设置之一,可控制 Visual FoxPro 如何生成发送到视图基表的 SQL Update 语句的 WHERE 子句。可以使用“视图设计器”的“更新条件”选项卡来选择需要的设置,或者使用 DBSETPROP( ) 函数设置一个视图定义的 WhereType 属性。要想更改一个活动视图临时表的 WhereType 设置,请使用 CURSORSETPROP( )

例如,假设有一个简单的基于 Customer 表的远程视图,它包含七个字段:cust_id, company, phone, fax, contact, title, 和 timestamp。视图的主关键字是 cust_id

“更新条件”选项卡显示视图中可更新的字段

如要让用户可以从视图中更改公司的联系人和他们的头衔。可以只设置这两个字段是可更新的:contact_name contact_title。但是,当公司的其他方面的情况发生改变时,例如公司地址,如果想要使这些改变通过协调器来传输,这个协调器可以判断改变对公司的的影响,例如是否客户的销售地区也将改变。既然视图可以传送更新,当然可以根据愿望选择 WhereType。

现在假设改变了一个客户的 contact 字段中的名字,但是却不想更改另一个可更新字段 title 中的值。对于这种情况,下面一节将讨论 WhereType 设置是如何影响 WHERE 子句的,WHERE 子句用 Visual FoxPro 生成,其作用是将新的联系人姓名传送到基表中去。

只比较关键字段

使用 DB_KEY 设置对更新的限制最小。用来更新远程表的 WHERE 子句只包含用 KeyField 或 KeyFieldList 属性指定的主关键字段。如果基表中的主关键字段的值在检索记录之后没有被更改或删除,更新将正常进行。

对于前面的示例,Visual FoxPro 会准备一个带有 WHERE 子句的更新语句,它将 cust_id 字段与基表的 cust_id 字段进行比较:

WHERE OLDVAL(customer.cust_id) = CURVAL(customer_remote_view.cust_id)

当更新语句传送到基表时,只验证基表的关键字段。

将视图中的关键字段与基表的相应字段进行比较

比较视图中的关键字段和已更改字段

默认的 DB_KEYANDMODIFIED 设置比 DB_KEY 多了一些限制。DB_KEYANDMODIFIED 只将关键字段和在视图中更改的可更新字段与基表相应的字段进行比较。如果在视图中更改了一个字段,但该字段不是可更新字段,则字段不与基表中的数据比较。

用来更新基表的 WHERE 子句包含用 KeyField 属性指定的主关键字段和其他在视图中更改的任意字段。对于前面的示例,Visual FoxPro 会准备一个更新语句,此语句将比较 cust_id 字段(因为是关键字段)及 contact 字段(因为联系人姓名已被更改)。即使 title 字段是可更新的,更新语句也不包含 title 字段,因为没有对其作过修改。

将关键字段和在视图中更改的字段与基表的相应字段进行比较

比较关键字段和所有可更新字段

DB_KEYANDUPDATABLE 设置将关键字段以及视图中任何可更新字段(不管是否被修改)与基表相对应的字段进行比较。如果字段是可更新的,即使在视图中并没有更改它,但如果在基表中更改了这个字段,更新就会失败。

用来更新基表的 WHERE 子句包含用 KeyField 或 KeyFieldList 属性指定的主关键字段和其他任何可更新字段。对于前面的示例,Visual FoxPro 会准备一个更新语句,它将 cust_idcontact title 字段中的值同基表中的相应的字段进行比较。

视图中的所有可更新字段都将与基表的相应字段进行比较

比较基表记录中所有字段的时间戳

DB_KEYANDTIMESTAMP 设置对更新的限制最多,并且只有在基表中具有时间戳字段时才能使用该设置。Visual FoxPro 将基表记录中的当前时间戳与当数据被提取到视图中时的时间戳进行比较。如果基表记录的任何字段被改变了,即使它不是试图更改的字段,甚至不是视图中的一个字段,更新也会失败。

在示例中,Visual FoxPro 准备一个更新语句,它将 cust_id 字段中的值和时间戳字段中的值与基表记录行中这些字段的值进行比较。

将视图记录的时间戳与基表记录中的时间戳进行比较

为了能够在多表视图中使用 DB_KEYANDTIMESTAMP 设置成功地更新数据,必须在每个可更新表的视图中包含时间戳字段。例如,在视图中有三个表,但是却只想更新其中的两个,而且选择了 DB_KEYANDTIMESTAMP 设置,必须从两个可更新表中将时间戳字段带入到结果集合中。可以使用 CompareMemo 属性的逻辑值来确定在冲突检查中是否包含备注字段。

管理冲突

无论选择了缓冲、事务处理还是视图,都必须在更新过程中管理冲突

管理缓冲冲突

在一个多用户环境中,通过精心选择打开、缓冲并锁定数据的时间和方式,可以更高效地进行数据更新操作。应该减少访问记录或表时发生冲突的时间,同时必须预测到不可避免的冲突将导致什么样的后果,并对这种冲突进行管理。冲突一般在一个用户试图锁定一个当前正被其他用户锁定的记录或表时发生,两个用户不能同时锁定同一记录或表。

应用程序中应包含管理冲突的例程。如果没有冲突例程,系统将死锁。死锁通常在这种情况下发生:第一个用户已经锁定一个记录或表,现在试图去锁定已被第二个用户锁定的记录,而第二个用户又反过来试图锁定第一个用户锁定的记录。尽管这种情况很少发生,但记录或表被锁定的时间越长,这种死锁的可能性就越大。

出错处理例程

无论是设计一个多用户的应用程序还是为一个单用户系统添加网络支持服务,都要求找到错误并对其进行处理。使用 Visual FoxPro 记录缓冲和表缓冲可以部分简化这种工作。

如果试图锁定一个已被其他用户锁定的记录或表,Visual FoxPro 将返回出错信息。可使用 SET REPROCESS 自动处理不成功的锁定操作。此命令与 ON ERROR 例程和 RETRY 命令组合,可以继续或取消锁定的操作。

下面的示例展示了如何使用 SET REPROCESS 命令自动地重处理一个失败操作。

使用 SET REPROCESS 和 ON ERROR 管理用户冲突的示例

代码 注释
ON ERROR DO err_fix WITH ERROR(),MESSAGE()
SET EXCLUSIVE OFF   
SET REPROCESS TO AUTOMATIC
USE customer
IF !FILE('cus_copy.dbf')
   COPY TO cus_copy
ENDIF
出错时本例程运行。
以非独占方式打开文件。
自动重处理不成功的锁定。
打开表。

必要时,把 customer 表记录追加到 cus_copy 表。
DO app_blank
DO rep_next
DO rep_all
DO rep_curr
DO add_recs
主例程从此处开始。
这些命令是在程序中可以执行的代码示例
ON ERROR
主例程在此处结束。
PROCEDURE app_blank      
   APPEND BLANK
RETURN
ENDPROC
追加空白记录的例程。
PROCEDURE rep_next
   REPLACE NEXT 1 contact WITH ;
      PROPER(contact)
RETURN
ENDPROC
替换当前记录数据的例程。
PROCEDURE rep_all      
   REPLACE ALL contact WITH ;
      PROPER(contact)
   GO TOP
RETURN
ENDPROC
替换所有记录数据的例程。
PROCEDURE rep_curr      
   REPLACE contact WITH PROPER(contact)
RETURN
ENDPROC
替换当前记录数据的例程。
PROCEDURE add_recs      
   APPEND FROM cus_copy
RETURN
ENDPROC
从其他文件中追加记录的例程。

下面的示例表明一个出错处理过程,此过程在用户按下 ESC 键时启动。

使用 ESC 键的出错处理示例

代码 注释
PROCEDURE err_fix
   PARAMETERS errnum, msg
遇到错误并在用户从等待状态下退出时,本程序被调用。
DO CASE
   CASE errnum = 108      
      line1 = "File cannot be locked. "
      line2 = "Try again later..."
   CASE errnum = 109 .OR. errnum = 130
      line1 = "Record cannot be locked. "
      line2 = "Try again later."
   OTHERWISE            
      line1 = msg + " "
      line2 = ;
         "See your system administrator."
ENDCASE
判断此错误属于何种错误。
是“其他用户正在使用文件”吗?


还是“其他用户正在使用记录”?


或未知?


=MESSAGEBOX( line1 + line2, 48, "Error!" )
RETURN                  
在对话框中显示出错信息,并在框中加上感叹号和“确定”。

检测并解决冲突

在数据更新操作过程中,特别是在共享环境下,您可能希望确定哪些字段已经更改,确定已更改字段的原有值或当前值是什么。Visual FoxPro 的缓冲和 GETFLDSTATE( )GETNEXTMODIFIED( )OLDVAL( )CURVAL( ) 函数可以提供这些功能。通过它们来确定哪些字段已经更改;查找已更改数据;比较当前值、原有值及已编辑的值。这样,可以决定如何处理错误或冲突。

若要检测字段中所做的更改

GETFLDSTATE( ) 可以对非缓冲数据进行操作,但在启用了记录缓冲的情况下,此函数更有效。例如,将 GETFLDSTATE( ) 用于一个表单的 Skip 按钮代码中。移动记录指针时,Visual FoxPro 将检查记录中所有字段的状态,如下例所示:

lModified = .F.
FOR nFieldNum = 1 TO FCOUNT( ) && 检查所有字段
   if GETFLDSTATE(nFieldNum) = 2  && 已修改
      lModified = .T.
      EXIT && 可以在此处插入一个
   ENDIF && 请参阅下一个示例
ENDFOR

若要检测并定位缓冲数据中已更改的记录

GETNEXTMODIFIED( ) 以 0 作为参数,查找第一个已修改的记录。如果其他用户对缓冲表进行了修改,则缓冲区中 TABLEUPDATE( ) 命令遇到任何修改都将导致冲突。可以用 CURVAL( )OLDVAL( )MESSAGEBOX( ) 计算冲突值并解决冲突,CURVAL( ) 返回磁盘上记录的当前值,而 OLDVAL( ) 返回记录位于缓存内的值。

若要确定缓冲字段的原有值

OLDVAL( ) 返回一个缓冲字段的值。

若要确定磁盘上一个缓冲字段的当前值

CURVAL( ) 返回一个在编辑操作完成前缓冲字段在磁盘中的当前值。

在共享环境中,可以创建一个出错处理过程,比较当前值与原有值,并决定是接受当前的修改还是更早一些的修改。

下面的示例使用 GETNEXTMODIFIED( )、CURVAL( ) 和 OLDVAL( ) 函数,在更新操作过程中给用户提供带提示信息的选择。这个示例从检测第一个修改的记录开始,可以包含在表单的“更新”或“保存”按钮代码中。

“更新”或“保存”按钮的 Click 事件代码

代码 注释
DO WHILE GETNEXTMODIFIED(nCurRec) <> 0   
   GO nCurRec
   = RLOCK( )
循环遍历缓冲区.。

锁定已修改的记录。
   FOR nField = 1 TO FCOUNT(cAlias)   
      cField = FIELD(nField)
      IF OLDVAL(cField) <> CURVAL(cField)
         nResult = MESSAGEBOX("Data was ;
            changed by another user. ;
            Keep changes?", 4+48+0, ;
            "Modified Record")
查找冲突。

比较原有值和磁盘上的当前值,请用户指示如何处理冲突。


         IF nResult = 7   
            = TABLEREVERT(.F.)   
            UNLOCK RECORD nCurRec   
         ENDIF
      ENDIF
   ENDFOR
   nCurRec = GETNEXTMODIFIED(nCurRec)
ENDDO
如果用户选择 'No',则还原此记录,然后解锁。




查找下一个已修改的记录。
= TABLEUPDATE(.T., .T.)   
对所有记录更新。

使用备注字段检查冲突

使用 CompareMemo 属性来控制何时使用备注字段检查更新冲突。这个视图和临时表属性确定了在更新的 WHERE 子句中是否包含备注字段(备注或通用型)。默认设置是“真”(.T.),即在 WHERE 子句中包含备注字段。如果这个属性设置为“假”(.F),不论 UpdateType 如何设置,在更新的 WHERE 子句中都不会包含备注字段。

当 CompareMemo 属性设置为假时,备注字段上的开放式冲突检查是不可用的。如要使用备注字段的冲突检查,请将 CompareMemo 属性设置为真。

管理冲突的规则

管理在多用户环境中的冲突需要有扩展和可重用的代码。一个完整的冲突管理例程进行如下操作:

有关冲突管理例程的示例,请参阅位于 Visual Studio …\Samples\Vfp98\Classes 目录下的 Samples.vcx 中的 datachecker 类。将该类添加到表单中,在将缓冲数据写入表的操作之前调用 CheckConflicts 方法程序,例如使用行缓冲移动记录指针,关闭表,或者发布 TABLEUPDATE( ) 函数。