第六章 什么是数据集
Delphi 4中有四种类型的标准数据集构件,分别是TTable、TQuery、TStoredProc和TClientDataSet。这些数据集构件都是从一个共同的基类TDataSet继承下来的,其中,只有TClientDataSet是直接从TDataSet继承下来的,而TTable、TQuery、TStoredProc的直接上级是TDBDataSet,TDBDataSet的上级是TBDEDataSet,TBDEDataSet 的上级才是TDataSet。这几个类之间的继承关系可以用图6.1来表示。
图6.1 数据集的继承关系
TDataSet是所有数据集的抽象基类,它的大部分属性和方法是虚拟的或抽象的。所谓虚拟的方法,是指这些方法可以被派生类重载。所谓抽象的方法,是指这些方法只有声明,没有定义,派生类必须给出定义后才能调用这些方法,不同的派生类可以有不同的定义。
由于TDataSet中包含抽象的方法,您不能直接创建它的实例,否则会引起运行期错误。
如果从功能上划分,TDataSet的属性和方法可以分为这么几大块:打开和关闭数据集、浏览记录、编辑数据、书签管理、控制连接、访问字段、记录缓冲区管理、过滤、事件。
6.1 打开和关闭数据集
在对数据集进行任何操作之前,首先要打开数据集。要打开数据集,可以把Active属性设为True,例如:
CustTable.Active := True;
也可以调用Open函数,例如:CustQuery.Open;
要关闭数据集,可以把Active属性设为False或者调用Close函数。
6.2 数据集的状态
数据集的状态(State属性)决定了当前能够对数据集进行的操作,例如,当数据集已经关闭,它的状态是dsInactive,此时就不能访问数据集的任何数据。
6.2.1 State属性
State属性是只读的,下面列出了State属性可能的值:
.dsInactive数据集已关闭,不能访问它的数据;
.dsBrowse数据集已打开,可以浏览数据但不能修改数据;
.dsEdit此时为编辑状态,可以修改数据;
.dsInsert此时可以插入一条新的记录;
.dsSetKey只适用于TTable和TClientDataSet,此时可以设置范围和键值,并且可以调用GotoKey函数;
.dsCalcFields正在处理OnCalcFields事件,此时不能修改非计算字段的值;
.dsCurValue内部使用;
.dsNewValue内部使用;
.dsOldValue内部使用;
.dsFilter正在进行过滤操作。
当一个数据集刚刚打开的时候,它的State属性被设为dsBrowse,以后,State属性的值会随着应用程序的操作自动变化。
要使数据集进入dsBrowse、dsEdit、dsInsert或dsSetKey状态,就得调用相应的方法。
例如,要使数据集CustTable进入dsInsert状态,程序示例如下:
Procedure TForm1.InsertButtonClick(Sender: TObject);
Begin
CustTable.Insert;{进入dsInsert状态}
AddressPromptDialog.ShowModal;
If AddressPromptDialog.ModalResult := mrOK then
CustTable.Post; {恢复为dsBrowse状态}
Else
CustTable.Cancel; {恢复为dsBrowse状态}
End;
从上面这个例子可以看出,有些操作会使数据集自动变成dsBrowse状态,例如,调用Post函数如果成功的话,数据集就恢复为dsBrowse状态,如果调用Post没有成功,数据集仍然保持原来的状态。调用Cancel也能使数据集恢复为dsBrowse状态。
如果把Active属性设为False,或者调用Close,将使数据集进入dsInactive状态。例如,下面两行代码是等价的:
CustTable.Active := False;
CustTable.Close;
有些状态如dsCalcFields、dsCurValue、dsNewValue、dsOldValue和dsFilter不能被应用程序所控制,而是由数据集本身根据需要自动设置的。例如,当正在处理OnCalcFields事件时,就自动进入dsCalcFields状态。当退出处理OnCalcFields事件的句柄时,数据集自动恢复成原先的状态。
当数据集的状态发生改变时,会触发TDataSource构件的OnStateChange事件,如果这个TDataSource构件的DataSet属性指定了这个数据集的话。
下面详细介绍数据集的各种状态以及怎样进入这些状态。
6.2.2 dsInactive状态
当数据集已关闭时,就处于dsInactive状态。此时,不能访问它的任何数据。
要使数据集进入dsInactive状态,可以把Active属性设为False,或者调用Close。在数据集将要关闭之前,会触发BeforeClose事件。当数据集刚刚关闭,会触发AfterClose事件。如果在数据集处于dsEdit或dsInsert状态时调用Close,应当在处理BeforeClose事件的句柄中提示用户是认可还是取消。程序示例如下:
Procedure CustTable.VerifyBeforeClose(DataSet: TDataSet)
Begin
If (CustTable.State = dsEdit) or (CustTable.State = dsInsert) then
Begin
If MessageDlg('认可修改吗?', mtConfirmation, mbYesNo, 0) = mrYes then
CustTable.Post;
ElseCustTable.Cancel;
End;
End;
6.2.3 dsBrowse状态
当一个数据集刚刚打开的时候,数据集总是处于dsBrowse状态,此时,可以显示数据集中的记录,但不能编辑和插入记录。
dsBrowse状态可以认为是数据集的基本状态,在此状态下,可以进入其他状态。例如,调用Insert或Append函数将使数据集的状态从dsBrowse变成dsInsert(当然,这还取决于其他因素,如CanModify属性的值),调用SetKey将使数据集从dsBrowse变成dsSetKey状态。
TDataSet有两个方法可以使数据集回到dsBrowse状态,一个是Cancel,它将取消当前正在进行的编辑、插入、搜索等操作,使数据集回到dsBrowse状态。另一个是Post,它将试图把修改了的数据保存到数据集中,如果成功的话,数据集将回到dsBrowse状态,如果没有成功,数据集仍然保持原先的状态。
6.2.4 dsEdit状态
如果应用程序要修改数据集的数据,必须首先进入dsEdit状态。要进入dsEdit状态,可以调用Edit。不过,调用Edit并不能保证一定能进入dsEdit状态,这还取决于CanModify属性的值。如果这个属性返回True的话,表示数据集是可以读和写的。
对于TTable构件来说,如果ReadOnly属性设为True,CanModify属性肯定返回False。对于TQuery构件来说,如果RequestLive属性设为False,CanModify属性肯定返回False。
即使数据集进入了dsEdit状态,也并不意味着用户就一定能修改数据,数据控件的ReadOnly属性还必须设为False。此外,对于SQL数据库来说,用户能不能修改数据还取决于用户有没有修改数据的权限。
要从dsEdit状态返回到dsBrowse状态,可以调用Cancel、Post或Delete函数,其中,如果Post和Delete没有调用成功的话,就仍然保持dsEdit状态。
在数据控件中,当用户修改了数据后把输入焦点移走,就相当于调用Post函数,将使数据集回到dsBrowse状态。
6.2.5 dsInsert状态
如果应用程序要插入新的记录,必须首先进入dsInsert状态。要进入dsInsert状态,可以调用Insert或Append函数。不过,调用Insert或Append并不能保证一定能进入dsInsert状态,这还取决于CanModify属性的值是否返回True。
即使数据集进入了dsInsert状态,也并不意味着用户就一定能插入记录,数据控件的ReadOnly属性还必须设为False。此外,对于SQL数据库来说,用户能不能插入记录还取决于用户有没有修改数据的权限。
要从dsInsert状态返回到dsBrowse状态,可以调用Cancel、Post或Delete函数,其中,如果Post没有调用成功的话,就仍然保持dsInsert状态。
在数据控件中,当用户修改了数据后把输入焦点移走,就相当于调用Post,将使数据集回到dsBrowse状态。
6.2.6 dsSetKey状态
可以调用Locate、Lookup在数据集中搜索特定的记录。对于TTable构件来说,还可以调用GotoKey、GotoNearest、FindKey或FindNearest在表格中搜索特定的记录。在调用上述方法之前,必须首先使数据集进入dsSetKey状态。要使数据集进入dsSetKey状态,可以调用SetKey。调用了上述方法后,数据集又回到dsBrowse状态。
另外,可以对数据集进行过滤。对于TTable构件来说,还可以预先设置范围。在进行过滤和范围操作前,也要首先进入dsSetKey状态。
6.2.7 dsCalcFields状态
当OnCalcFields事件被触发时,就会使数据集进入dsCalcFields状态。在处理OnCalcFields事件的句柄中,应当给出“计算字段”的值。
在dsCalcFields状态下,除了“计算字段”外,应用程序不能修改其他字段的值,因为如果其他字段的值发生变化,又会导致OnCalcFields事件被触发,从而导致无限循环。
当OnCalcFields事件处理完毕,数据集又回到dsBrowse状态。
6.2.8 dsFilter状态
当OnFilterRecord事件被触发时,就会使数据集进入dsFilter状态。在此状态下,不允许修改数据集的记录,否则,过滤就无法正确执行。
当OnFilterRecord事件处理完毕,数据集就回到dsBrowse状态。
6.2.9 dsNewValue、dsOldValue或dsCurValue
在允许缓存更新的情况下,当用户修改数据集的记录时,数据集有可能会进入dsNewValue、dsOldValue或dsCurValue状态。在这三种状态下,可以通过字段(TField)的NewValue、OldValue或CurValue属性来访问当时的值。
上述三个状态是由Delphi 4内部使用的,应用程序无法主动进入上述三种状态。
6.3 浏 览 记 录
每个活动的数据集都有一个指针,指向当前记录。很多对数据集的操作都是针对当前记录,许多数据控件也只显示当前记录的数据,因此,在数据库应用程序中知道当前记录的位置是非常重要的。
数据库应用程序往往要改变当前记录的位置,这时候就要用到下面这些方法:
.First使第一条记录成为当前记录;
.Last使最后一条记录成为当前记录;
.Next使下一条记录成为当前记录;
.Prior使前一条记录成为当前记录;
.MoveBy使距离当前记录若干行的记录成为当前记录。
此外,Delphi 4中有一个TDBNavigator构件,专门用来浏览记录,它把上述方法用按钮来实现。
除了上述方法外,TDataSet中还有两个只读的布尔类型的属性用来判断当前记录的位置,一个是Bof,如果这个属性返回True,表示现在已到了数据集的开始位置。另一个是Eof,如果这个属性返回True,表示现在已到了数据集的末尾。
6.3.1 First和Last
调用First函数能够使数据集的第一条记录成为当前记录,并且把Bof属性设为True。如果第一条记录已经是当前记录了,First就什么也不干。程序示例如下:
CustTable.First;
调用Last函数能够使数据集的最后一条记录成为当前记录,并且把Eof属性设为True。如果最后一条记录已经是当前记录,Last就什么也不干。程序示例如下:
CustTable.Last;
用TDBNavigator构件实现的导航器上有两个按钮,分别对应着First和Last。
6.3.2 Next和Prior
调用Next函数能够使下一条记录成为当前记录。如果当前记录已经是数据集的最后一条记录,Next就什么也不干。程序示例如下:
CustTable.Next;
调用Prior能够使前一条记录成为当前记录。如果当前记录已经是数据集的第一条记录,Prior就什么也不干。程序示例如下:
CustTable.Prior;
6.3.3 MoveBy
调用MoveBy函数使数据集中的另一条记录成为当前记录,该记录距原先的当前记录若干行。MoveBy需要传递一个参数,指定相隔的行数,正数表示向记录编号增大的方向移动,负数表示向记录编号减小的方向移动。程序示例如下:
CustTable.MoveBy(-2);
MoveBy返回实际移动的行数,返回值与传递给MoveBy的参数有可能不同。
注意:在多用户环境下,其他用户有可能正在修改、插入或删除记录,这样,一条记录原来距当前记录是5行,现在有可能变为4行和6行,甚至该记录都不存在了,因为其他用户修改该记录的数据或删除了该记录。
6.3.4 Eof和Bof属性
TDataSet有两个只读的属性Eof和Bof,分别用于判断是否到了数据集的末尾和开头。在遍历数据集的所有记录时经常要用到这两个属性。
如果Eof属性返回True,表示现在已到了数据集的末尾。
进行下列操作时会把Eof属性设为True:
.打开一个空的数据集;
.调用了Last;
.调用了Next,而现在已经在数据集的最后一条记录;
.调用了SetRange,而范围是无效的。
除了上述情况外,Eof属性都将返回False。
Eof属性通常用在一个循环中,每调用一次Next就要判断一下Eof属性,以避免对不存在的记录进行操作。程序示例如下:
CustTable.DisableControls;
Try
CustTable.First;
While not CustTable.EOF Do
Begin
...
CustTable.Next;
End;
Finally
CustTable.EnableControls;
End;
上述代码同时演示了在遍历数据集的所有记录时怎样暂时禁止数据控件跟着刷新。在遍历数据集的所有记录前应当调用DisableControls禁止刷新,这样能够加快遍历的速度,因为刷新也是要花时间的。遍历结束后,应当调用EnableControls恢复刷新。
EnableControls最好在Try...Finally结构的Finally部分调用,这样能保证即使在遍历时出现异常,也能保证刷新能得到恢复。
如果Bof属性返回True,表示现在已到了数据集的开头。
进行下列操作时会把Bof属性设为True:
.打开一个非空的数据集;
.调用了First;
.调用了Prior,而现在已经在数据集的第一条记录。
除了上述情况外,Bof属性都将返回False。与Eof属性一样,Bof属性通常用在一个循环中,每调用一次Prior就要判断一下Bof属性,以避免对不存在的记录进行操作。程序示例如下:
CustTable.DisableControls;
Try
While not CustTable.BOF Do
Begin
...
CustTable.Prior;
End;
Finally
CustTable.EnableControls;
End;
6.4 书 签
书签的作用是在数据集的某个位置做一个标记,以后可以快速方便地回到那个位置。TDataSet中提供了若干个属性和方法用于管理书签。
如果读Bookmark属性,返回当前记录的书签。如果写Bookmark属性,它能使一个指定的书签成为当前书签。
TDataSet中有关书签的几个函数都是虚拟的,TDataSet的派生类TBDEDataSet重新定义了这些方法,包括:
.BookmarkValid判断某个书签是否合法;
.CompareBookmarks比较两个书签是否相同;
.GetBookmark创建一个书签来标记当前记录;
.GotoBookmark回到用GetBookmark标记的位置;
.FreeBookmark删除一个书签。
要创建一个书签,首先要声明一个TBookmark类型的变量,然后调用GetBookmark函数创建一个标记当前记录的书签。TBookmark类型的变量实际上是一个无类型的指针。
在调用GotoBookmark之前,最好先调用BookmarkValid判断书签是否合法,因为书签标记的记录有可能已删掉。如果BookmarkValid返回True,说明书签是合法的,可以调用GotoBookmark跳转到书签标记的位置。
可以调用CompareBookmarks比较两个书签是否相同,如果两个书签不同,这个函数就返回1。如果两个书签相同或者都是NIL,这个函数就返回0。
GotoBookmark需要传递一个参数,即书签。
FreeBookmark用于删除一个书签。当一个书签已用不到时,应当及时删除它,因为书签也是一种资源。
下面这段代码演示了书签的用法:
Procedure DoSomething (const Tbl: TTable)varBookmark: TBookmark;
Begin
Bookmark := Tbl.GetBookmark;
Tbl.DisableControls;
Try
Tbl.First;
While not Tbl.EOF Do
Begin
...
Tbl.Next;
End;
Finally
Tbl.GotoBookmark(Bookmark);
Tbl.EnableControls;
Tbl.FreeBookmark(Bookmark);
End;
End;
6.5 搜索特定的记录
可以调用Locate和Lookup函数在数据集中搜索特定的记录。
Locate用于在数据集中定位一条特定的记录,并使该记录成为当前记录。Locate需要传递三个参数,第一个是KeyFields参数,用于指定要按哪些字段搜索,第二个是KeyValues参数,用于指定每个字段相应的值,第三个是Options参数,用于设置搜索选项。
下面这个例子搜索Company字段的值为“Professional Ltd.”的记录:
var
LocateSuccess: Boolean;
SearchOptions: TLocateOptions;
Begin
SearchOptions := [loPartialKey];
LocateSuccess := CustTable.Locate('Company', 'Professional Ltd.',SearchOptions);
End;
如果Locate找到了一条符合条件的记录,就把该记录变为当前记录,并返回True。如果Locate没有找到匹配的记录,就返回False。
对于Locate来说,KeyFields参数指定的字段越多,搜索的条件就越精确。如果KeyFields参数需要指定多个字段,彼此之间要用分号分开。由于字段的数据类型可能各不相同,因此,KeyValues是一个Variant类型的参数。如果KeyFields参数指定了多个字段,KeyValues参数必须是一个可变类型的数组。程序示例如下:
With CustTable Do
Locate('Company;Contact;Phone', VarArrayOf(['Sight Diver','P']), loPartialKey);
Lookup与Locate非常相似,也是在数据集中搜索特定的记录。不同的是,如果找到匹配的记录,Lookup能返回该记录中若干个字段的值。
Lookup需要传递三个参数,第一个是KeyFields参数,用于指定要按哪些字段搜索,第二个是KeyValues参数,用于指定每个字段相应的值,第三个是ResultFields参数,用于指定要返回哪些字段的值。
下面这个例子在CustTable中搜索Company字段的值为“Professional Ltd.”的记录,并返回Company、Contact、Phone等字段的值:
var
LookupResults: Variant;
Begin
With CustTable Do
LookupResults := Lookup('Company', 'Professional Divers, Ltd.', 'Company;Contact; Phone');
End;
如果ResultFields参数指定了多个字段,Lookup返回一个可变类型的数组。如果没有找到匹配的记录,Lookup将返回一个空的数组。程序示例如下:
var
LookupResults: Variant;
Begin
With CustTable Do
LookupResults := Lookup('Company; City', VarArrayOf(['Sight Diver',
'Christiansted']), 'Company; Addr1; Addr2; State; Zip');
End;
6.6 过 滤
一个应用程序往往只对数据集的部分记录感兴趣,例如,可能只对一个客户表中来自广东的客户感兴趣。这种情况下,可以用过滤技术把符合特定条件的记录过滤出来。
不过,对于一个字段很多的数据集来说,最好还是使用查询。
6.6.1 允许过滤
要对数据集进行过滤,首先要指定过滤条件,并设置FilterOptions属性设置有关选项(可选),然后把Filtered属性设为True。以后如果不想进行过滤,只要把Filtered属性设为False。
要指定过滤条件有两种方式:一是设置Filter属性,二是在处理OnFilterRecord事件的句柄中给出过滤条件。
Filter属性适合于在运行期使用,它能够动态地指定过滤条件,能够根据需要改变过滤条件。不过,Filter属性是一个字符串,过滤条件相对比较简单。虽然可以用运算符构成复合的条件表达式,但只限于几个常见的运算符。更主要的是,用Filter属性指定的表达式中只能出现数据集中已有的字段名和常量,不能出现其他数据。
而OnFilterRecord事件则灵活得多,它能够在设计期就指定好过滤条件。而在处理OnFilterRecord事件的句柄中,可以任意指定过滤条件,过滤条件可以很复杂。
6.6.2 Filter属性
Filter属性是一个字符串,可以这样设置Filter属性:
Dataset1.Filter := '''State'' = ''CA''';
也可以这样设置Filter属性:
Dataset1.Filter := Edit1.Text;
上面这行代码允许让用户自己输入过滤条件。甚至还可以把上述两行代码结合起来:
Dataset1.Filter := '''State'' = ' + Edit1.Text;
设置了过滤条件后,只要把Filtered属性设为True,过滤即有效。
可以用比较和逻辑运算符构造复合的过滤条件,这些运算符包括:
. <小于;
. >大于;
. >=大于等于;
. <=小于等于;
.=等于;
. <>不等于;
.AND两边的表达式都必须为True;
.NOT表达式不能为True;
.OR两个表达式只要有一个为True。
下面这个例子用AND运算符限制CustNo字段必须大于1400且小于1500:
(CustNo > 1400) AND (CustNo < 1500);
注意:在Filtered属性设为True的情况下,用户修改、插入的记录有可能不符合过滤的条件,这时候,会拒绝接受与过滤条件矛盾的记录。
6.6.3 OnFilterRecord事件
在Filtered属性设为True的情况下,数据集中的每条记录都会触发OnFilterRecord事件,这样,就有机会决定是否要把记录过滤。
处理OnFilterRecord事件的句柄中有一个布尔类型的Accept参数,把这个参数设为True表示接受此记录,把这个参数设为False表示把此记录过滤掉。程序示例如下:
Procedure TForm1.Table1FilterRecord(DataSet: TDataSet;
var Accept: Boolean);
Begin
Accept := DataSet['State'] = 'CA';
End;
上面这个例子的意思是,只有State字段的值为CA的记录才被接受。
注意:由于数据集的每条记录都会触发OnFilterRecord事件,因此,处理OnFilterRecord事件的代码要尽可能地简短,尤其是对一个有很多条记录的大型数据集。
有时候,程序需要按多种不同的过滤条件进行过滤,可以建立多个处理OnFilterRecord事件的句柄,然后在运行期动态地切换事件句柄,程序示例如下:
DataSet1.OnFilterRecord := NewYorkFilter;Refresh;
6.6.4 设置过滤选项
FilterOptions属性用于设置过滤的选项。这个属性是一个集合,可以是空集(默认),也可以包含下列元素:
.foCaseInsensitive比较字符串时忽略大小写;
.foPartialCompare对于字符串类型的字段必须全字匹配,不允许部分匹配。
例如,为了在比较State字段时忽略大小写,可以这样设置:
FilterOptions := [foCaseInsensitive];
Filter := '''State'' = ''CA''';
6.6.5 在过滤后的数据集中浏览记录
过滤后的数据集实际上是原来的数据集的一个子集。TDataSet提供了四个方法用于在过滤后的数据集中浏览记录,它们是:
.FindFirst使过滤后的数据集中的第一条记录成为当前记录;
.FindLast使过滤后的数据集中的最后一条记录成为当前记录;
.FindNext使过滤后的数据集中的下一条记录成为当前记录;
.FindPrior使过滤后的数据集中的前一条记录成为当前记录。
上述四个方法如果调用成功,就返回True,否则,就返回False。可以检查一个只读的Found属性,看看上次调用是否成功。
如果通过Filter属性或OnFilterRecord事件设置了过滤条件,而Filtered属性设为False,调用上述四个方法时会自动暂时允许过滤,然后移动当前记录的位置,最后又禁止过滤。换句话说,上述四个方法可以不理会Filtered属性是怎样设置的。
如果没有设置过滤条件,上述四个方法即相当于First、Last、Next和Prior。
6.7 修 改 数 据
TDataSet中提供了一些方法用于在数据集中更新、插入和删除记录,它们是:
.Edit使数据集进入dsEdit状态;
.Append在数据集的末尾添加一条记录;
.Insert在数据集的当前位置插入一条记录;
.Post试图把用户对数据的修改写到数据集中;
.Cancel取消用户对数据的修改,使数据集回到dsBrowse状态;
.Delete删除当前记录。
6.7.1 进入dsEdit状态
要编辑数据集的记录,首先要进入dsEdit状态。要进入dsEdit状态,调用Edit函数。不过,调用Edit不一定会使数据集进入dsEdit状态,还取决于CanModify属性的值。
一旦数据集进入了dsEdit状态,用户就可以在数据控件上修改当前记录的值。当用户把输入焦点从当前记录上移走,即相当于调用了Post函数。程序示例如下:
With CustTable Do
Begin
Edit;
FieldValues['CustNo'] := 1234;
Post;
End;
要取消当前未决的修改,用户可以按ESC键或单击用TDBNavigator构件实现的导航器上的Cancel按钮。
在使用缓存更新技术(CachedUpdates属性设为True)的情况下,调用Post只是把数据写到缓存中,而不是直接写到数据集中。要把缓存中的数据写到数据集中,需调用ApplyUpdates函数。
6.7.2 插入新的记录
要在数据集中插入新的记录,首先要进入dsInsert状态。要进入dsInsert状态,可以调用Insert或Append函数。不过,调用Insert或Append不一定会使数据集进入dsInsert状态,还取决于CanModify属性的值。
一旦进入了dsInsert状态,用户就可以在数据控件(一般是TDBGrid)中插入一条新的记录,并给这条记录输入数据。
如果要通过编程来插入新的记录,就要注意Insert和Append的区别。Insert将把一条新的记录插入到当前记录的前面,而Append将把一条新的记录添加到数据集的末尾。
插入了新的记录后,应当调用Post或在CachedUpdates属性设为True的情况下调用ApplyUpdates把新的记录写到数据集中。
如果数据集是已建立了索引的Paradox或dBASE表,新记录将自动移到恰当的位置。
如果数据集没有建立索引,新记录就插入到数据集的当前位置(Insert)或末尾(Append)。
6.7.3 删除记录
调用Delete函数将删除当前记录,并且使数据集回到dsBrowse状态。如果窗体上有TDBNavigator构件的话,用户可以单击导航器上的“Delete”按钮删除当前记录。当前记录被删除后,下一条记录就成为当前记录。
如果删除的本来就是最后一条记录,则前一条记录成为当前记录。
6.7.4 修改整条记录
除了TDBGrid和TDBNavigator外,大部分数据控件只能工作于数据集的一个或几个字段,而不是整条记录。
不过,TDataSet提供了若干个方法可以直接修改整条记录而不是单独的字段,这些方法包括:
.AppendRecord类似于Append,但可以给字段赋值,不需要调用Post;
.InsertRecord类似于Insert,但可以给字段赋值,不需要调用Post;
.SetFields对当前记录的字段赋值,需要显式地调用Post。
上述三个方法都要传递一个TVarRec类型的数组作为参数,该数组的每一个元素对应着一个字段的值。如果数组的元素个数小于数据集的字段个数,剩下字段的值就是NULL。
对于没有建立索引的数据集来说,AppendRecord把一条新的记录加到数据集的末尾。对于已建立索引的数据集来说,新记录将自动移到一个恰当的位置。
SetFields用于对当前记录的字段赋值。在调用SetFields之前,首先要调用Edit,使数据集进入dsEdit状态。调用了SetFields后,需要显式地调用Post函数。
调用SetFields时,如果您只想对部分字段赋值,让其他字段的值保持不变,可以用NULL或NIL去赋值。
假设一个数据集中有五个字段,分别是Name、Capital、Continent、Area和Population,可以这样对它们赋值:
CountryTable.InsertRecord(['Japan', 'Tokyo', 'Asia']);
上述程序在数据集中插入了一条新的记录,并且对前三个字段赋了值。现在可以再次对当前记录赋值,不过,这次只想对Area字段和Population字段赋值,程序就要这样写:
With CountryTable Do
Begin
If Locate('Name', 'Japan', loCaseInsensitive) then
Begin
Edit;
SetFields(NIL, NIL, NIL, 344567, 164700000);
Post;
End;
End;
注意:此处要用NIL而不是NULL,否则,前三个字段会被设为空。
6.8 事 件
TDataSet的事件主要分为两大类,一类是Before系列,另一类是After系列,列表如下:
.BeforeOpen,AfterOpen发生在数据集打开前后;
.BeforeClose,AfterClose发生在数据集关闭前后;
.BeforeInsert,AfterInsert发生在插入了一条新的记录前后;
.BeforeEdit,AfterEdit 发生在进入dsEdit状态前后;
.BeforePost,AfterPost 发生在写数据集的前后;
.BeforeCancel,AfterCancel发生在取消修改的前后;
.BeforeDelete,AfterDelete发生在删除记录的前后。
此外,当数据集中增加了一条新的记录时就会触发OnNewRecord事件,当“计算字段”的值需要重算时将触发OnCalcFields事件。
Before系列的事件常常用来中止操作。例如,当调用Delete函数试图删除当前记录时,在当前记录将要删除前会触发BeforeDelete事件,可以在处理BeforeDelete事件的句柄中调用Abort或触发一个异常放弃删除当前记录,程序示例如下:
Pocedure TForm1.TableBeforeDelete (Dataset: TDataset)
Begin
If MessageDlg('Delete This Record?', mtConfirmation, mbYesNoCancel, 0) <> mrYes Then Abort;
End;
After系列的事件往往用来在状态栏上通知用户,程序示例如下:
Procedure TForm1.Table1AfterDelete(DataSet: TDataSet);
Begin
StatusBar1.SimpleText := Format('有%d 条记录',[DataSet.RecordCount]);
End;
OnCalcFields事件主要用于给出“计算字段”的值。AutoCalcFields属性的值决定了什么时候会发生OnCalcFields事件。
如果AutoCalcFields属性设为True,下列情况下会发生OnCalcFields事件:
.数据集被打开时;
.在数据控件中,输入焦点从一条记录移到另一条记录;
.在数据控件中,输入焦点从一个字段移到另一个字段;
.当前记录被修改或从数据库中检索了一条记录。
不过,即使AutoCalcFields属性设为False,当数据集中的任意一个非计算字段的值发生变化时都会触发OnCalcFields事件。
由于OnCalcFields事件有可能是频繁发生的,因此,处理OnCalcFields 事件的代码要尽可能地简短。在AutoCalcFields属性设为True的情况下,在处理OnCalcFields事件的句柄中不能修改数据集的数据,因为一旦当前记录被修改,又要触发OnCalcFields事件,从而导致无限循环。例如,假设您在处理OnCalcFields事件的句柄中调用了Post,就会触发OnCalcFields事件,导致再次调用Post,再次触发OnCalcFields事件……
6.9 TBDEDataSet
TBDEDataSet是从TDataSet继承下来的,它提供了通过BDE(BorlandDatabase Engine)访问数据的能力。这一节主要介绍TBDEDataSet,读者应当对前面介绍的TDataSet已经有了比较深刻的认识。
与TDataSet一样,TBDEDataSet也是虚拟的和抽象的,除非您想建立自定义的数据集,否则,一般不需要直接用到TBDEDataSet。
TBDEDataSet重载了TDataSet中涉及记录导航、索引和书签的方法,增加了一些处理BLOB字段、缓存更新的属性、方法和事件。
6.9.1 CacheBlobs属性
TBDEDataSet的CacheBlobs属性用于控制BDE是否把BLOB字段的内容放到缓存中。如果这个属性设为True,当应用程序读取BLOB字段的值时,BDE将把BLOB字段的内容放在缓存中,这样,当应用程序下次要读取这个字段的值时,就不必再从数据库服务器那儿去检索,只要直接从内存中取过来就行了,这样可以提高应用程序的性能。
不过,如果应用程序需要频繁地更新BLOB字段的值,这时候反而应当把CacheBlobs属性设为False,这样能保证检索到的BLOB字段的值总是最新的。
6.9.2 缓存更新
TBDEDataSet提供了缓存更新的技术。所谓缓存更新就是,应用程序从数据库中检索数据,在本地缓存中建立一个副本,用户对数据进行修改后,也只是反映在缓存中,以后可以调用ApplyUpdates一次性地把所有的修改反映到数据集中。
可以看出,缓存更新技术可以明显地提高应用程序的性能,而且可以方便地取消修改,只要还没有调用ApplyUpdates。下面列出了TBDEDataSet中有关缓存更新的属性、方法和事件:
.CachedUpdates如果这个属性设为True,缓存更新有效;
.UpdateObject用于指定一个TUpdateSQL构件来更新基于查询的数据集;
.UpdatePending如果缓存中有未决的记录,这个属性就返回True;
.UpdateRecordTypes指定数据集中哪些记录是可见的;
.UpdateStatus返回当前的更新状态;
.OnUpdateError如果更新过程中出错将触发这个事件;
.OnUpdateRecord每更新一条记录就会触发一次这个事件;
.ApplyUpdates把缓存中的数据写到数据集中;
.CancelUpdates把缓存中未决的修改取消;
.CommitUpdates把缓存清掉;l FetchAll从数据库检索所有记录到缓存中;
.RevertRecord撤消对当前记录的修改。
6.10 TDBDataSet
TDBDataSet是从TBDEDataSet继承下来的,它提供了数据库和会话期管理的能力。
TDBDataSet中增加了若干个属性和方法用于管理数据库和BDE会话期,包括:
.CheckOpen检查数据库是否已打开;
.Database返回一个TDatabase构件;
.DBHandle返回一个BDE句柄,调用BDE的API时要用到这个句柄;
.DBLocale返回当前的国际语言驱动程序;
.DBSession返回一个BDE会话期对象;
.DatabaseName用于指定要访问的数据库;
.SessionName用于指定一个BDE会话期对象。
这里详细解释一下DatabaseName属性和SessionName属性。如果应用程序要访问远程数据库服务器如Sybase、Oracle或InterBase,应当用TDatabase构件来连接数据库,此时,应当设置DatabaseName属性指定要连接的数据库,可以设为TDatabase构件的名称。如果没有显式地使用TDatabase构件,DatabaseName属性应当设为BDE 别名。对于Paradox和dBASE表来说,可以设为表的路径。
SessionName属性用于指定一个BDE会话期对象。如果应用程序没有显式地使用TSession构件,不必设置这个属性。如果应用程序显式地使用了多个TSession构件,应当设置SessionName属性指定其中一个。
一般来说,应用程序用不到DBHandle、DBLocale和DBSession等属性,除非要直接调用BDE的API。这三个属性都是只读的。
TDBDataSet中还有一个只读的Provider属性,它能够返回一个IProvider接口。在多层的Client/Server应用程序中,客户程序需要通过IProvider接口与应用服务器通讯。