众所周知,Delphi最主要的特性之一就是可以用来开发数据库程序。但是在Delphi 5以前数据库的开发主要是基于BDE(Borland数据库引擎)的,而BDE存在很多问题,包括过于庞大、不易分发、不稳定等。因此,Borland决定今后将放弃BDE的进一步开发,当前BDE已经处于维护阶段,只是修正Bug而已。
但是BDE的停止更新并不意味着Borland停止技术革新的脚步了,Delphi 5中包括了一个很重要的新特性,那就是可以用ADO Express架构来开发数据库程序。ADO Express控件提供了对微软的ActiveX Data Objects (ADO)的封装(ADO属于微软通用数据存取架构的一部分),它给我们提供了BDE以外的数据库编程的强大功能,随着微软对ADO功能的改进,它正在变得越来越强大,除此之外微软还提供了ADO的扩展来进一步拓展它的应用。
接下来我们就来研究一下ADO的扩展,本文的第一部分主要是关于DDL(数据定义语言)、安全机制( ADOX)和Jet及Replication对象库 (JRO);第二部分我们则要来研究一下多维ADO (ADO MD)。
ADOX的介绍
ADOX 可以用来执行一系列用ADO无法单独实现的功能。例如,使用ADO我们要想对已有的数据库的结构进行修改是非常麻烦的,但使用ADOX就可以很容易地做到,同时ADOX还提供了提取数据库的用户信息或创建新的用户账号等方面的扩展。ADOX扩展了ADO的对象模型,提供了10个新的对象,它们都可以和ADO配合使用。比如我们可以用ADO Connection对象连接到一个数据源,并提取出元数据(注意:元数据就是对数据库结构的描述,包括表、字段、索引、关键字段、存储过程的定义等,而不是指数据库包含的数据内容),大多数数据库使用SQL来定义元数据。在ADOX出现以前,提取元数据的唯一的方法是使用ADO Connection对象的OpenSchema方法,而且要想创建新的数据库对象,我们还只能使用基于SQL的DDL对象和ADO Command对象。
ADOX提供了一种不需要懂SQL就可以操纵元数据的方式。但要注意的是 ADOX并不支持所有的数据库,它只局限于微软的Access, SQL Server, 以及其他几种数据库。要想详细了解这方面的信息,请参看http://www.microsoft.com/data。
ADOX的对象模型如图1.81所示,图1.82显示了表的对象模型。ADOX对象表的内容如表1.5所示,ADOX的最上层对象是Catalog。它的下级对象包括表、视图、过程、用户以及账号。Catalog对象可以用来打开数据库 (通过ADO Connection 对象),或创建一个新的数据库。到ADO 2.1版本为止,我们还只能创建Jet 4.0数据库,在未来版本中这一功能有可能扩展到其他数据库。
图1.81 图1.82
表1.5 ADOX对象的简单描述
对 象 |
说 明 |
Catalog |
包含描述数据源模式目录的集合,提供对表、视图、用户、过程和账号的操作 |
Column |
表示表、索引或关键字的列 |
Group |
表示在安全数据库内具有访问权限的账号 |
Index |
表示数据库表的索引 |
Key |
表示数据库表中的主关键字、外部关键字或唯一关键字字段 |
Procedure |
表示存储过程或查询 |
Table |
表示包括列、索引和关键字的数据库表 |
User |
表示在安全数据库中具有访问权限的用户账号 |
View |
表示记录或虚拟表的过滤集 |
我们可以使用Catalog对象对表、过程以及视图进行操作。比如,通过枚举表集合,可以知道当前数据库有哪些表。更进一步,可以获得一个表的字段、索引、关键字段等元数据信息。通过用户和账号对象集合,可以获得数据库的安全信息(注意:这个功能只对安全数据库有效,对于Access数据库,我们必须在数据源连接字符串中包括System.mdw )。
ADOX另一个强大的功能是可以用Catalog 对象来创建新的数据库,并通过表、字段、索引对象的Add方法向数据库添加表、字段、索引和关键字段。
创建一个简单的ADOX察看器
接下来就演示一下如何在Delphi中使用ADOX。我们将创建一个应用程序,主要功能是:
(1)显示数据库的元数据。
(2)显示数据库对象的属性。
(3)显示视图和存储过程的源代码。
图1.83 |
首先创建一个新的项目,在主窗体上放置下列控件: MainMenu、TreeView、Memo和 StatusBar。完成的程序示意如图1.83所示。
接下来,我们要引入ADOX的类型库。先选择菜单项 Project | Import Type Library,然后从类型库列表中选择Microsoft ADO Ext. 2.1 for DDL and Security。为了避免同VCL已有控件名冲突,对ADOX类要重新命名(比如TTable可以改成TADOXTable),按Create Unit按钮来生成接口文件。Delphi会生成ADOX_TLB.pas文件,我们需要在项目的Uses部分引用它。
现在创建一个File | Open Catalog菜单项,并生成一个OnClick事件处理函数,代码如下:
procedure TForm1.OpenCatalog1Click(Sender: TObject);
begin
图1.84 |
// 通过微软提供的对话框来获得数据源
DS :=PromptDataSource(Application.Handle,'');
//如果用户选择了一个数据源.
if DS <> '' then
BrowseData(DS);
end;
这里使用了在ADODB单元中提供的PromptDataSource方法来显示标准的数据连接属性对话框(如图1.84)。
注意:这里应该选择Microsoft Jet 4.0 OLE DB Provider作为数据源,连接到Access的样例数据库Northwind.mdb,然后程序会调用BrowseData过程。这个过程会把从数据源中提取的元数据显示在TreeView中,其源码如下:
procedure TForm1.BrowseData(DataSource: string);
var
RootNode : TTreeNode;
OneNode : TTreeNode;
SubNode : TTreeNode;
I : Integer;
OldCursor : TCursor;
begin
// 改变光标为沙漏型
OldCursor := Screen.Cursor;
Screen.Cursor := crHourglass;
StatusBar1.Panels[0].Text :=
'提取源数据,请等待';
// 清空TreeView和Memo
ClearTree;
Memo1.Lines.Clear;
Application.ProcessMessages;
//连接数据源
Catalog._Set_ActiveConnection(DataSource);
RootNode := TreeView1.Items.Add(nil, 'Catalog');
//添加表信息
OneNode := TreeView1.Items.AddChild(RootNode, 'Tables');
for I := 0 to Catalog.Tables.Count-1 do begin
SubNode := TreeView1.Items.AddChild(
OneNode, Catalog.Tables[I].Name);
//处理字段、索引及关键字段
ProceedTables(Catalog.Tables[I], SubNode);
end;
//添加视图信息
if CheckViews(Catalog) then begin
OneNode := TreeView1.Items.AddChild(RootNode, 'Views');
for I := 0 to Catalog.Views.Count-1 do
SubNode := TreeView1.Items.AddChild(
OneNode, Catalog.Views[I].Name);
end;
//添加过程信息
OneNode := TreeView1.Items.AddChild(RootNode,
'Procedures');
for I := 0 to Catalog.Procedures.Count-1 do
SubNode := TreeView1.Items.AddChild(
OneNode, Catalog.Procedures[I].Name);
RootNode.Expand(False);
//恢复缺省光标清空状态条
Screen.Cursor := OldCursor;
StatusBar1.Panels[0].Text := '';
end;
通过三个循环我们遍历了所有的表、视图对象。每个对象都被放到了TreeView中的适当分支上。此外,每个表还都需要处理对应的字段、索引和关键字段列表,这里是通过ProceedTables过程来实现的,代码如下:
procedure TForm1.ProceedTables(T: Table; N: TTreeNode);
var
I : Integer;
SubNode : TTreeNode;
begin
//添加字段信息
if T.Columns.Count > 0 then
SubNode := TreeView1.Items.AddChild(N, 'Columns');
for I := 0 to T.Columns.Count-1 do
TreeView1.Items.AddChild(SubNode,
T.Columns.Item[I].Name);
//添加索引信息
if T.Indexes.Count > 0 then
SubNode := TreeView1.Items.AddChild(N, 'Indexes');
for I := 0 to T.Indexes.Count-1 do
TreeView1.Items.AddChild(SubNode,
T.Indexes.Item[I].Name);
//添加关键字段信息
if T.Keys.Count > 0 then
SubNode := TreeView1.Items.AddChild(N, 'Keys');
for I := 0 to T.Keys.Count-1 do
TreeView1.Items.AddChild(SubNode, T.Keys.Item[I].Name);
end;
现在回到BrowseData过程,会看到在循环视图对象前,执行了如下校验过程:
if CheckViews(Catalog) then ...
这是为了避免不支持视图的数据源可能带来的错误,CheckView函数代码如下:
function CheckViews(C: _Catalog): Boolean;
var
I : Integer;
begin
try
I := C.Views.Count;
CheckViews := True;
except
CheckViews := False;
end;
end;
为了获得更详细的对象信息,可通过实现TreeView控件的OnChange事件来显示元数据,代码如下:
procedure TForm1.TreeView1Change(Sender: TObject;
Node: TTreeNode);
begin
if Node.Parent.Parent <> nil then
case Node.Parent.Text[1] of
'C' : ViewColumns(Node.Parent.Parent.Text,Node.Text);
'I' : ViewIndexes(Node.Parent.Parent.Text,Node.Text);
'K' : ViewKeys(Node.Parent.Parent.Text,Node.Text);
'T' : ViewTables(Node.Text);
'V' : ViewProps(Node.Text);
'P' : ProcProps(Node.Text);
end;
end;
过程通过调用ViewColumns、ViewIndexs、ViewKeys来显示被选对象的属性,表1.6~表1.8是相应对象的详细信息。
表1.6 字段对象属性
属 性 |
说 明 |
Attributes |
描述列特性 |
DefinedSize |
指示列的规定最大尺寸 |
NumericScale |
指示列中数值的范围 |
ParentCatalog |
指定表或列的父目录以便访问特定提供者的属性 |
Precision |
指示列中数值的最高精度 |
RelatedColumn |
指示相关表中相关列的名称(仅关键字列) |
SortOrder |
指示列的排序顺序(仅索引列) |
Type |
字段类型 |
表1.7 索引对象属性
属 性 |
说 明 |
Clustered |
指示索引是否被分簇 |
IndexNulls |
指示在索引字段中具有 Null 值的记录是否有索引项 |
PrimaryKey |
指示索引是否代表表的主关键字 |
Unique |
指示索引关键字是否必须是唯一的 |
表1.8 关键字段属性
属 性 |
说 明 |
DeleteRule |
指示主关键字被删除时将执行的操作 |
RelatedTable |
指示相关表的名称 |
Type |
指示列的数据类型 |
UpdateRule |
指示主关键字被更新时会执行的操作 |
ViewProps和ProcProps过程则是用来显示视图和存储过程的。其中ProcProps代码如下,ViewProps 过程与此类似:
procedure TForm1.ProcProps(Name: string);
var
S : string;
Disp : IDispatch;
Command : _Command;
begin
S := 'PROCEDURE : ' + Catalog.Procedures.Item[Name].Name;
S := S + ^M^J + 'Created : ' +
VarToStr(Catalog.Procedures.Item[Name].DateCreated);
S := S + ^M^J + 'Modified : ' +
VarToStr(Catalog.Procedures.Item[Name].DateModified);
if CmdSupported(Catalog.Procedures.Item[Name]) then begin
Disp := Catalog.Procedures.Item[Name].Get_Command;
Command := Disp AS Command;
S := S + ^M^J^M^J + Command.Get_CommandText;
end;
Memo1.Text := S;
end;
其中存储过程可以通过ADO Command 对象获得,我们先使用Get_Command方法来获得Command对象的IDispatch接口,然后调用接口的Get_CommandText方法来获得存储过程的源代码。
在了解了如何使用ADOX来获得数据源的元数据后,接下来要研究的就是如何使用ADOX来不使用复杂的SQL DDL语句就可以创建数据库。
创建数据库
首先需要创建Catalog对象的一个实例。通过CataLog对象不仅可以指定要创建的数据库的类型,还可以指定数据库文件的位置。代码如下:
const
BaseName = 'c:\data\demo.mdb';
DS = 'Provider=Microsoft.Jet.OLEDB.4.0;Data Source=' +
BaseName;
var
Catalog : TADOXCatalog;
...
//创建Catalog对象的一个实例
Catalog := CoCatalog.Create;
//如果数据库已经存在,就删除它
if FileExists(BaseName) then
DeleteFile(BaseName);
//创建新的.mdb文件
Catalog.Create(DS);
//指定活动连接
Catalog._Set_ActiveConnection(DS);
...
创建好数据库后,我们要向库中添加表和字段,这包括下面几个步骤:
(1)创建表对象的一个实例。
(2)创建字段对象的一个实例。
(3)设定新字段的属性。
(4)继续添加新的字段
(5)重复第(3)、(4)步。
(6)添加新的表。
代码如下
//第1步创建表对象的一个实例
Table := CoTable.Create;
//指定表名
Table.Name := 'Customers';
// ...设定表所属的Catalog
Table.ParentCatalog := Catalog;
//第2步创建字段对象的一个实例
Column := CoColumn.Create;
with Column do begin
ParentCatalog := Catalog;
//第3步设定新字段的属性
Name := 'CustID';
Type_ := adInteger;
Properties['Autoincrement'].Value := True;
Properties['Description'].Value := 'Customer ID';
end;
//第4步添加新的字段
Table.Columns.Append(Column, 0, 0);
Column := nil;
//第5步重复第3、4步
with Table.Columns do begin
Append('FirstName', adVarWChar, 64);
Append('LastName', adVarWChar, 64);
Append('Phone', adVarWChar, 64);
Append('Notes', adLongVarWChar, 128);
end;
//第6步添加新的表
Catalog.Tables.Append(Table);
Catalog := nil;
接下来就可以添加索引和关键字段了。下面代码演示了如何给LastName字段创建索引:
Index := CoIndex.Create;
with Index do begin
Name := 'LastNameIndex';
IndexNulls := adIndexNullsDisallow;
Columns.Append('LastName', adVarWChar, 64);
Columns['LastName'].SortOrder := adSortAscending;
end;
Table.Indexes.Append(Index, EmptyParam);
方法的原理很简单,先创建一个Index对象,然后设定它的Name属性,指定如何处理NULL索引,将其同字段相关联,最后添加到表中,关键字段实现原理与此类似。
到目前为止,我们还没有提到如何使用用户和账号对象,这是由于当前的项目是基于Access数据库的,而这两个对象是Access不支持的。
使用Jet 和Replication对象
另一个ADO的扩展是Jet和Replication对象 (JRO)。ADOX能对不同的数据源操作,而JRO只对Jet数据库起作用。也就是只能操作Access数据库。
JRO提供了一组对象,可以创建、修改和同步复制数据库。其核心对象是Replica,它可以用来创建新的Replica,获得Replica的属性,根据其他Replica的改变来同步更新。
图1.85 |
JRO框架还包括有JetEngine对象,JetEngine对象提供了一些Jet引擎的特性,特别是能够压缩数据库、设定数据库密码、对数据库加密和从内存缓冲中刷新数据。对象模型如图1.85所示。
这里只简单介绍一下用JRO来复制数据库。
第一步,要创建一个design master(设计原型),表明数据库将用来作为一个复制的数据源,可以用来复制。首先需要调用Replica对象的MakeReplicable方法,然后可以用GetObjectReplicability和SetObjectReplicability方法来改变数据库的replicability状态。根据情况可以创建部分或者完全的设计原版的复本。
第二步,可以利用Filter对象来定义更新规则。最后,可以在两个复本之间保持同步,可以在Internet上进行直接或间接的同步。如果是间接同步,需要使用微软公司的Office Developer带的复制管理器。
注意:要想在Delphi中使用JRO库,需要引入Microsoft Jet and Replication Objects 2.1 Library (2.1版本)的类型库。
使用JetEngine对象
下面代码显示了如何使用JetEngine对象来压缩Northwind.mdb数据库,并创建一个新的压缩后的Northwind.mdb数据库的复本NewNorth.mdb:
const
Provider = 'Provider=Microsoft.Jet.OLEDB.4.0;';
//替换下面的路径为微软Access例子库的真实路径
SrcMDB = 'c:\data\northwind.mdb';
DstMDB = 'd:\data\newnorth.mdb';
procedure TForm1.Button1Click(Sender: TObject);
var
JetEng : JetEngine;
Src : WideString;
Dest : WideString;
begin
,//创建JetEngine对象
JetEng := CoJetEngine.Create;
//设定数据源
Src := Provider + 'Data Source=' + SrcMDB;
//设定目的源
Dest := Provider + 'Data Source=' + DstMDB;
//如果目标数据库存在,就删除它
if FileExists(DstMDB) then
DeleteFile(DstMDB);
//压缩数据库
JetEng.CompactDatabase(Src, Dest);
//释放JetEngine对象
JetEng := nil;
end;
让我们稍微深入地解释一下压缩数据库的过程。表的页面首先被重组。压缩后,原来碎片的数据页面被放在相邻的数据库页面中,这可以极大地改进数据库的性能。通过删除原来只是被标记为删除的记录来回收未被使用的空间。然后自增加字段被重置,以便下一个分配的值可以保持连续。最后更新用来优化查询的表的统计信息,这是因为统计信息被改变了,所有的查询都会被标记,下一次运行查询时,查询又会被重新编译。
结论
本文介绍了两个ADO扩展:DDL和Security (ADOX)以及Jet和Replication对象 (JRO)。我们还简单研究了一下如何使用ADOX对象来获得数据源的元数据。如何创建新数据库和如何使用JRO来压缩Jet数据库,并简单介绍了数据库复制过程。
第二部分我们将要介绍ADO Multi-Dimensional (ADO MD),它可以用来操作多维数据存储。
自己在搞老钟的软件时候很有想法,把数据库也要区域化,所以试着做所谓的“翻译”,目前整了ACCESS的,还不知道好用不。
实际上就是同时有两套资源,一套用于捕捉识别→定位,一套用于更新,当然,两套内容被放到了顺序一一对应的两个数组里。
主要的工作是,更改ACCESS里的表名、表中的列名,最后更改文件名。
要查到现有数据库的表名才能和第一套资源进行比对,所以
开始的时候想,能不能在TSQL里写查询,网上也给出的可行性,就是像SQL SERVER一样查询系统表。可是,ACCESS的系统表有权限限制,没弄明白怎么在代码里改权限(估计即便弄明白了,也要引用ACCESS的某个模块,搞一堆复杂的方法),所以就放弃了。
后来看到可以用ADOX直接修改,高兴了。
可是不知道怎么用CATALOG连接数据库,试着用连接字符串、OLEDBCONNECTION和DBCONNECTION向CATALOG.ACTIVECONNECTION赋值,都报错,不高兴了。
最后看到说,ACTIVECONNECTION只能接受已经打开的ADODB.CONNECTION,虽然觉得不好,但也只能这样。
真正的问题:
在CATALOG对表名修改后,并没有即时的在硬盘上生效,而CATALOG也没有提供更新的功能。这使得每次操作后,若需要上一次操作结果,就必须彻底释放上一个CATALOG或重新实例化新的CATALOG。
在一个试验用的小程序里,不给出ADODB.CONNECTION,而使用CATALOG.CREATE(connectionstring),我发现即便让CATALOG=NULL也不能解除数据库的锁定状态,也就是说,连接仍然开着;而提供了CONNECTION的情形能够正常的解锁(connection.CLOSE())。
这似乎造成了一个硌硬人的局面:在创建.mdb文件后,连接要保持打开到程序结束!
其实回头想一想,就会知道,ACTIVECONNECTION属性就是连接所在的位置,只要将这个连接关闭就可以了,当然,要关闭这个连接,就不能只引用ADOX,还必须引用ADO,至于是强制转换还是什么的,那就好说了。
这个试验使我明白了:ADO和ADOX是天造地设的一对,任何试图拆散他们的想法终会遇到重重困难~