delphi(14)
时间:2006-06-10 来源:许我一个信仰
第15章 MIDAS编程
多层分布式应用服务(Multitiered Distributed Application Services,即MIDAS)套件是一套组件,使用这些组件可以很容易地建立多层的客户-服务器数据库应用程序。这也是公司愿意为Delphi企业版支付数千美元的原因之一,而它确实物有所值。如果您购买了个人版本的Delphi,那么必须再购买企业版才能得到MIDAS组件和相应的动态链接库(DLL)。如果您使用的是标准版或专业版,那么本节中的例子将无法使用。但您仍然可以阅读一下本节,来看一看Delphi企业版是否适合您。
对另一些人来说,本节将通过例子来演示如何使用MIDAS的一些核心功能。通过示范如何利用一些核心控件来建立客户和服务器程序,对这些功能进行了演示;共有三个例子示范了这些控件:一个动态查询程序,它使用DCOM连接到同一台计算机和远程机器上的服务器,另一个程序示范了出错情况下的恢复,还有一个公文包程序的例子。
请记住:客户程序通常有图形用户界面,并且与用户进行交互。而服务器是向客户程序提供服务的应用程序。客户-服务器这个术语隐含着图形用户界面与数据库服务器。n层、多层或三层这些术语,大体上也是同样的意思。第一层是客户程序,中间层或第二层包含了商务规则的编码,通常是应用服务器,而最后一层是数据库服务器自身(参见图15.1)。本章提供了一些例子程序进行示范,其中客户端是用Delphi实现的标准Windows可执行文件,而中间层则是MIDAS和用Delphi实现的进程外COM服务器——自动化服务器,此外还需要适当的数据库。为避免创建难于理解的例子,本章中只使用了DBDEMO表和本地Interbase数据库。请记住,任何数据库服务器,如SQL Server、Oracle或Sybase等,在客户程序和中间层的代码不进行改变或改动很少的情况即可使用。
图15.1 基本的三层客户-服务器应用程序配置,分别使用了三台物理上独立的计算
机示范了每一层的不同作用。所有的三层可以都位于同一台物理计算机上
15.1 MIDAS组件概述
本节中讨论了通常可能用到的组件。用于实现三层应用程序的MIDAS组件分为客户程序的组件和服务器程序的组件。另外,可能还需要一些通常用于建立客户程序的其他组件。
注意:这里并未提供对MIDAS组件的详尽描述。MIDAS套件是非常广泛的,现在看来还没有专门讲述MIDAS的Delphi书籍。
MIDAS为开发者提供了客户程序与服务器程序之间的桥梁。一旦创建了包含TRemoteDataModule对象的服务器程序,然后即可建立客户程序,就像是两层应用程序一样。即,可以根据个人的喜好选择是否使用数据感知控件,而无论怎样都可以在客户程序中得到并使用相关的数据,就像是已经了解了有关数据库的知识一样。由于中间层的服务器程序是新出现的部分,我们从用于建立应用服务器的组件开始。
15.1.1 定义服务器应用程序
在两层的客户-服务器应用程序中,包括数据库服务器以及数据感知客户程序。客户程序由程序员编写,而服务器则是数据库应用程序。在三层系统中,客户与数据库层之间添加了应用服务器层。本节示范了用于建立服务器程序的一些基本的组件。
注意:请记住,在n层、三层和多层结构之间并无实际的区别。对于我们的目的来说,它们是同样的;至于是否存在区别,则是一个有待确定的问题。
TRemoteDataModule
TRemoteDataModule是TDataModule的后代,其用法也大致相同。在服务器程序中,可将远程数据模块作为所有非可视组件的容器使用。TRemoteDataModule实现了IAppServer接口,只需向标准的应用程序中添加该类的对象,即可实现需要向客户程序提供的功能。
要创建MIDAS服务器,首先在Delphi中启动一个标准的应用程序工程。从New Items对话框的Multitier属性页中,向工程添加一个远程数据模块对象。创建远程数据模块的向导过程如下所示(见图15.2),其中需要选定CoClass的名字、实例化方法以及线程模型。提供了这些信息后,Delphi将创建类型库和新的远程数据模块子类,该子类由TRemoteDataModule子类化而来,并继承了所定义的CoClass接口。
MIDAS应用服务器是一个自动化服务器。可以向接口添加一些功能,并在远程数据模块中进行实现(参见15.2节“对MIDAS服务器进行查询”,其中的例子实现了一个接口,返回服务器可以访问的所有表名)。远程数据模块将实现UpdateRegistry方法,该方法负责在第一次运行程序时向Windows NT注册服务器。
无须向远程数据模块添加额外的功能,但需要添加一些组件,至少包括一个TProvider和一个TDataSet组件。添加TDatabase和TSession组件也很有用。在Delphi专业版中引入了数据集、数据库和会话组件,它们与用于建立两层数据库应用程序的组件是相同的。TProvider与MIDAS一同发布,包括TDataSetProvider和TXMLTransformProvider两种组件。
图15.2 远程数据模块的向导过程将子类化TRemoteDataModule
并生成类型库,用作COM服务器的接口
扼要地重新叙述一下,一个MIDAS服务器程序包括一个TDataSet组件(如TTable或TQuery),一个TProvider组件(如TDataSetProvider组件),一个TDatabase组件和TSession组件。下面我们简要地对这些组件重新回顾一下。
TDataSetProvider
TDataSetProvider由TBaseProvider子类化而来。数据集提供者处于客户与服务器程序之间,它是客户数据集的中介。提供者维护了一个对数据源数据集的引用以及Options特性,该特性描述了如何使用一个特定的数据集提供者。
要使用数据集提供者,需要将DataSet特性赋值为TNestedTable、TQuery、TTable或TStoredProcedure等类型的对象。要使TClientDataSet类型的对象能够与数据集提供者通信,需要将Exported特性设置为True。Options特性有14个可用的值。例如,要使数据集提供者能够接收动态SQL语句,需要向Options集合添加poAllowCommandText值。这项工作可以在设计时利用Object Inspector完成。对于TProviderOptions的完整的解释,可以看一下Delphi的帮助文档。本章稍后建立例子程序时,我们将针对一些特定的设置进行讨论。
TDatabase
向远程数据模块添加一个TDatabase。数据库组件引用了BDE(Borland数据库引擎)别名或物理上的数据库。如果存在已定义的别名,可以将其赋值给TDatabase.AliasName特性。Connected特性打开或关闭数据库。DatabaseName特性指定与数据库关联的名字。如果DatabaseName特性是已存在的BDE别名,则无须将该值赋予数据库组件的AliasName或DriverName特性。要定义新的数据库别名,只需添加DatabaseName和DriverName特性值,将AliasName特性置为空即可。
TDatabase.Params特性类型为TStrings,用于定义一些形如name = value的参数对,以便传递给要连接的数据库。TDatabase.SessionName特性是一个TSession组件的名字。
TSession
TSession组件用于管理数据库连接。多线程的数据库应用程序是其主要用途之一。将AutoSessionName设置为True,可以保证服务器的每个实例都具有惟一的会话名;在多个客户与多个TRemoteDataModule对象实例进行连接时,这是必需的(更多的信息请参见TClassInstance和TComponentFactory类)。
TDataSet
TDataSet是TTable、TQuery、TNestedTable和TStoredProcedure的祖先类。对于TDataSetProvider类的DataSet特性来说,上面提到的每个数据集组件都是可用的数据源(关于数据访问组件的完整细节,请参见第13章)。
TDataSetProvider
TDataSetProvider是MIDAS客户与MIDAS服务器之间的桥梁。客户由TDataSetProvider对象得到数据,而该对象则由TDataSet对象得到数据。而客户使用TDataSetProvider对象对数据库进行更新。
要使TClientDataSet对象从MIDAS服务器得到数据,必需设置DataSet、Exported和ProviderName特性。上面提到过,DataSet特性向TDataSetProvider对象提供数据库中的数据。而只有在Exported特性的值是True的情况下,客户才能连接到数据集提供者,TClientDataSet.ProviderName特性的值需要设置为提供者的名字。下面列出的特性值示范了在远程数据模块中四个关键组件的基本设置(请注意,在列表中使用了TTable,实际上也可使用其余的三种数据集组件)。
TDatabase.DatabaseName = DatabaseName
Tdatabase.SessionName = SessionName
Session.AutoSessionName = True
Tsession.SessionName = SessionName
TTable.DatabaseName = DatabaseName
TTable.SessionName = SessionName
TTable.TableName = TableName
TDataSetProvider.Table = TDataSet
TDataSetProvider.Exported = True
TDataSetProvider.Name = ProviderName
当TClientDataSet.Active特性设置为True时,数据库、数据集和会话将连接到所引用的数据集。
部署服务器
当服务器的实现和测试完成后,即可进行部署工作。类似于InstallShield Express的安装程序用于自动化应用部署,它可用来部署MIDAS服务器。在部署MIDAS服务器时,需要安装服务器程序、MIDAS.DLL和STDVCL40.DLL。安装过程实际就是将这些文件复制到目标计算机,并根据文件类型进行注册。
注意:您可能会推测STDVCL40.DLL将变成STDVCL60.DLL,以反映Delphi版本的变化。在本书写作时(使用Delphi 6 Beta 2),该文件的名字仍然是STDVCL40.DLL。当您部署MIDAS应用程序时,应当意识到文件名可能会发生变化。
提示:在Windows NT和Windows 2000中,Run对话框是通过单击Start | Run激活的。
要注册创建的MIDAS应用服务器,可以运行应用程序或使用/REGSERVER开关运行程序。例如,给定服务器server.exe,在Run对话框中或DOS命令提示符下输入:
server.exe /regserver
接着是使用Run对话框来注册MIDAS.DLL和STDVCL40.DLL。可以使用Windows自带的regsvr32.exe程序来注册这些支持MIDAS程序的DLL。例如,假定这些DLL被复制或安装到c:\winnt\system32目录。然后使用:
regsvr32 c:\winnt\system32\MIDAS.Dll
即可为这些DLL在Windows注册表中添加相应的条目。
15.1.2 定义客户程序
在三层的MIDAS系统和与两层的客户-服务器系统中,客户程序是非常相似的。大部分基本的工作部件都是相同的,只有微小的变化。首先,不再需要使用Data Access属性页上的数据集组件来从数据库得到数据,而要使用TClientDataSet。TClientDataSet是TDataSet的子类,通常与一些数据感知控件进行协作,例如TTable、TQuery以及其他数据集组件。例如,可以像其他数据集组件一样调用Fields编辑器,并添加静态TField对象;你在第13章中学到的许多特性都可以在TClientDataSet中使用。
除了TClientDataSet之外,还可能需要使用TCustomConnection。该组件提供了与MIDAS服务器之间的连接。例如,可以使用TDCOMConnection连接到远程机器上的服务器。表15.1列出了TCustomConnection组件,以及所支持的不同连接协议。
表15.1 TCustomConnection组件支持多种连接协议
连接类型 |
需求及描述 |
TDComConnection |
支持到远程机器的Microsoft的DCOM连接,远程机器必须安装DCOM |
TSockConnection |
到远程应用服务器的TCP/IP连接,远程机器必须运行scktsrvr.exe |
TWebConnection |
使用HTTP协议连接到远程应用服务器;客户机必须安装Wininet.dll。服务器必须安装IIS 4或更高版本,或Netscape企业版3.6或更高版本。TWebConnection所连接的Web服务器必须安装Httpsrvr.dll(Httpsrvr.dll与Delphi一同发布) |
TCorbaConnection |
使用CORBA连接到应用服务器 |
至于客户程序的其余部分,我们在第13章中已经见到过。举例来说,如果要使用数据感知控件,则需要TDataSource组件。TDataSource组件的DataSet特性指向一个TClientDataSet类型的对象,而不是TTable或TQuery。而你显然需要使用数据感知控件。实际上客户程序彼此非常相似,以至于在15.3节“错误处理”中可以利用Delphi中的Database Form向导来创建示例程序。要将窗体转换为使用MIDAS服务器,只需利用向导把TTable替换为TClientDataSet,并向标准的数据感知窗体添加一个TDCOMConnection组件即可。
与服务器程序进行连接
客户程序需要用TCustomConnection组件连接到服务器程序。表15.1列出了可用的连接组件,可以根据系统的部署情况进行选择。如果系统部署在企业内部网或Internet上,可以使用TSocketConnection或TWebConnection。如果系统部署在同一物理网络上,可以选择使用TDCOMConnection或TCorbaConnection。
注意:要使服务器运行在远程计算机上并使用DCOM,需要在远程计算机上安装并注册服务器、MIDAS.DLL和STDVCL40.DLL。
每个TCustomConnection都实现了AppServer接口,这使得连接组件支持一致的接口而无须考虑所使用的连接协议。例如,如果使用TDCOMConnection组件,需要提供ServerGUID或ServerName特性。在定位服务器时,ServerGUID更为可靠。如果TDCOMConnection.ComputerName特性是空的,则假定服务器与客户位于同一台计算机上。添加远程机器名,则客户将在该计算机上运行服务器程序。要在设计时或运行时连接到服务器,可以将Connected特性设置为True。
注意:GUID,发音为goo-id,是一个全局惟一的标识符。GUID可确保是字符与数字的惟一序列,它保证了COM对象在世界范围内是惟一标识的。
每种连接都有一些额外的特性,它们对于该协议是必须的。TSocketConnection需要IP地址和主机名。而TWebConnection则需要用户名和密码、URL(统一资源定位符)以及代理服务器名。TCorbaConnection需要库ID、主机名和对象名,对象名即应用服务器名。Corba不是Microsoft协议,因此并不使用ServerGUID。TCorbaConnection.RepositoryID、TCorbaConnection. ObjectName和TCorbaConnection.HostName三个特性与TDComConnection.ServerGUID、TDComConnection.ServerName和TDComConnection. ComputerName三个特性的作用是相似的。
配置TClientDataSet对象
TClientDataSet对象代表了内存中的数据集。而您则需要提供RemoteServer特性和ProviderName。RemoteServer特性是一个TCustomConnection组件,类似于TDComConnection,而ProviderName是服务器程序中TDataSetProvider对象的名字。一旦连接到服务器和提供者之后,TClientDataSet就可以像Data Access属性页上的两层数据集组件一样使用了。
因为TClientDataSet组件的作用类似于静态或动态数据集的入口,所以,如果服务器将表组件与数据集提供者关联起来,那么客户端数据集支持与表相似的行为;如果服务器将查询组件与提供者关联,则客户端数据集支持查询行为。将SQL语句赋值给TClientDataSet.CommandText特性,即可向服务器传递SQL语句。如果服务器端要支持动态SQL,则TDataSetProvider必须设置poAllowCommandText选项,在15.2节“对MIDAS服务器进行查询”中可以看到。
添加数据源
如果在客户程序中使用与数据进行绑定的控件,那么除了连接和客户数据集组件以外,还需要TDataSource类型的对象。可将客户数据集的值赋予TDataSource.DataSet特性。我们在第13章中提到过,需要将数据源赋值给数据感知DataSource特性,以便与数据库建立连接。如果不使用数据感知控件,则无需数据源。
创建用户界面
MIDAS套件的功能并不影响如何开发客户程序。由于TClientDataSet和TConnection组件负责管理与服务器的关系,而TClientDataSet是由TDataSet子类而来,因此可以像使用TTable、TQuery或其他数据集一样使用TClientDataSet。关于两层与三层的MIDAS客户之间的相似性,我们将在15.3节“错误处理”中提供了这方面的一个例子。
部署MIDAS客户
MIDAS客户程序需要在所有运行该程序的计算机上安装并注册MIDAS.DLL。类似于服务器的部署,可以使用Windows自带的regsvr32.exe程序注册MIDAS.DLL。假定系统目录为c:\winnt\system32,将该DLL复制到system32目录,使用下列命令即可进行注册:regsvr32 c:\winnt\system32\midas.dll。
15.2 对MIDAS服务器进行查询
上一节涵盖了MIDAS套件的客户程序与服务器程序的一般性的例子。服务器程序使用TRemoteDataModule、TDatabase、TSession、TDataSet和TProvider。MIDAS套件自带了两种数据集提供者,分别是TDataSetProvider和TXMLTransformProvider。在客户程序中,需要使用MIDAS的TConnection和TClientDataSet组件。而关于MIDAS客户-服务器开发的其他方面,则与使用其他工具和技术的客户-服务器开发非常相似。
本节通过建立一个MIDAS动态SQL服务器程序示范了开发过程的各个方面。客户连接到服务器,并得到可用表的列表。客户向服务器传递关于可用表的有效的SQL语句,服务器执行查询,并向客户返回可用的结果集。查询的例子使用了DCOM,这是个很好的机会,可以创建服务器并在远程计算机上进行测试。该程序中使用了BDE别名DBDEMOS,如果希望使用其他数据库,只需替换服务器端的TDatabase.DatabaseName组件即可。
15.2.1 服务器程序的实现
本例中的服务器程序可以列出数据库中的所有表名,并根据客户程序的请求返回这些名字。而其他的所有事情都是通过组件完成的,看一下下面列出的代码就知道了。
unit UServerModule;
interface
uses
Windows, Messages, SysUtils, Classes, ComServ, ComObj, VCLCom,
DataBkr,
DBClient, Server_TLB, StdVcl, DBTables, DB, Provider, MConnect,
Variants;
type
TServerModule = class(TRemoteDataModule, IServerModule)
Provider: TDataSetProvider;
Database1: TDatabase;
Query1: TQuery;
Session1: TSession;
private
{ Private declarations }
protected
class procedure UpdateRegistry(Register: Boolean;
const ClassID, ProgID: string); override;
function GetTableNames: OleVariant; safecall;
public
{ Public declarations }
end;
var
ServerModule : TServerModule;
implementation
{$R *.DFM}
class procedure TServerModule.UpdateRegistry(Register: Boolean;
const ClassID, ProgID: string);
begin
if Register then
begin
inherited UpdateRegistry(Register, ClassID, ProgID);
EnableSocketTransport(ClassID);
EnableWebTransport(ClassID);
end else
begin
DisableSocketTransport(ClassID);
DisableWebTransport(ClassID);
inherited UpdateRegistry(Register, ClassID, ProgID);
end;
end;
function TServerModule.GetTableNames: OleVariant;
var
I : Integer;
TableNames : TStrings;
begin
TableNames := TStringList.Create;
try
Session1.GetTableNames( Database1.DatabaseName, '*.*', True,
False, TableNames );
result := VarArrayCreate( [0, TableNames.Count - 1],
varOleStr);
for I := 0 to TableNames.Count - 1 do
result [I] := TableNames[I];
finally
TableNames.Free;
end;
end;
initialization
TComponentFactory.Create(ComServer, TServerModule,
Class_ServerModule, ciMultiInstance, tmApartment);
end.
惟一需要自己编写的代码就是远程数据模块中的GetTableNames方法。我们首先把程序的各个部分组装起来,然后再看如何向服务器的接口添加GetTableNames方法,并对该方法进行简要的讨论。
创建服务器工程
可以像创建其他程序一样创建服务器程序。启动Delphi并使用缺省的新工程,或在Delphi运行时,选择File | New | Application来启动新的工程。请记住,我们将在同一工程组中建立客户程序与服务器程序。在保存该工程组时,要使用有意义的名字和位置。无需改动缺省的窗体,从New Items对话框的Multitier属性页中选择RemoteDataModule即可(请记住,只有在企业版的Delphi中RemoteDataModule才是可用的)。然后将显示Remote Data Module向导(见图15.2)。该向导过程中,您需要输入CoClass(即COM接口类)的名字、实例化模式和线程模型。
COCLASS名 当在Remote Data Module向导中输入CoClass名字时,实际是在定义自动化接口的类名。该值将成为远程数据模块的name特性,加上前缀T后,就是数据模块的类名。新的远程数据模块继承了TRemoteDataModule类并实现了CoClass接口。例如,如果在向导过程的CoClass域键入server,那么远程数据模块的类名就是TServer,而CoClass名字则是CoServer,接口是IServer。使用给出的例子,数据模块中的类定义如下:
TServer = class(TRemoteDataModule, IServer)
该模块的var语句如下:
var
Server : TServer;
继续上面的例子,将创建包含Microsoft IDL(Interface Definition Language,接口定义语言)的类型库server.tlb,以及包含接口定义的Object Pascal语法的Pascal文件server_TLB.pas。
自动化服务器要支持的任何特性和方法都必须使用Type Library编辑器来定义(见图15.3)。如果使用类型库编辑器,则Delphi将负责维护类型库的Pascal代码和Microsoft IDL文件。稍后我们将继续讨论Type Library编辑器。
图15.3 用于定义接口和管理Microsoft IDL的Type Library编辑器
实例化模型 实例化模型表示如何启动应用程序。有三种实例化模型,分别是Internal Instance、Single Instance和Multiple Instance。对于进程内自动化服务器,可使用Internal Instance模型,即DLL服务器。如果每个客户程序都运行服务器程序的一个实例,则使用Single Instance模型。如果客户程序共享服务器程序,则使用Multiple Instance模型;但每个客户程序都有自己的服务器实例——即Remote Data Module,这些实例在同一进程空间中运行。
线程模型 可用的线程模型包括Single、Apartment、Free、Both和Neutral。Single线程模型的服务器将序列化对COM对象的调用。而远程数据模块每次只处理一个请求,从而避免了多线程的问题。Apartment线程模型对远程数据模块的单一实例,每次只允许发出一个请求;但可以有多个远程数据模块的实例存在,每个实例分别处理不同的请求。Apartment模型同样需要保护全局数据,以避免线程冲突。
提示:如果使用的数据库具有BDE功能,则需要在服务器中使用TSession组件,并将AutoSessionName设置为True。
当使用ADO数据集时,推荐使用Free模型。在使用Free线程模型时,必须保护实例数据和全局数据,以避免线程冲突。Both模型与Free模型相同,但该模型将序列化对客户接口的回调。Neutral模型只在COM+中可用,否则它与Apartment模型是等同的。
定义远程数据模块
Query服务器会返回数据库中可用表的列表。为了定义GetTableNames方法,需要将GetTableNames添加到接口中,并在远程数据模块中实现该接口。通过使用Remote Data Module向导和下列步骤,即可定义示例程序的代码列表开始处所示的远程数据模块。
1.在一个新的工程中,单击File,New,Other,并选定New Items对话框的Multitier属性页中的Remote Data Module图标。
2.单击OK,然后将显示Remote Data Module向导(见图15.2)。
3.将CoClass命名为Server。使用缺省的实例化和缺省的线程模型(缺省值分别是Multiple Instance和Apartment)。
4.单击OK以创建类型库。
5.步骤1~4将生成远程数据模块单元、类型库的Object Pascal文件,以及包含IDL的文件。除了GetTableNames方法之外,其他的都已经在远程数据模块中定义了。
6.在Object Inspector中,将TRemoteDataModule.Name特性改为ServerModule(避免与工程名server冲突)。从View菜单中,单击Type library以显示Type Library编辑器。
7.单击IServerModule接口。在TypeLibrary编程器中单击New Method按钮并填好该方法的Parameters属性页(图15.4显示了该按钮和接口定义)。
图15.4 在类型库编辑器中创建GetTableNames接口(如图所示)
8.将该方法命名为GetTableNames,并选择Variant *类型和[out, retval]修饰符。
9.单击工具栏按钮Refresh Implementation,类型库编辑器将更新远程数据模块,以包括GetTableNames方法的声明和定义(为空)。
通过上述步骤,类型库编辑器将定义如下的方法:
function GetTableNames:OleVariant;safecall;
对自动化接口方法,必须使用safecall调用约定。为完成ServerModule,我们需要添加必要的组件,来把各个部分连接起来,并添加代码以实现GetTableNames方法。
添加会话对象 由于选择了Apartment线程模型和BDE,我们需要将TSession组件的AutoSessionName特性设置为True。从组件面板的BDE属性页向远程数据模块添加一个TSession对象,并设置AutoSessionName特性设置为True。这就自动地按格式Session#_#更新SessionName,确保会话的名字是惟一的。
添加数据库 在本例中,我们使用DBDEMOS数据库。DBDEMOS分别在单独的文件中引用了Paradox和DBase的实例表。由于DBDEMOS别名是存在的,因此我们只需将DBDEMOS作为TDatabase.DatabaseName特性的值输入即可。
从组件面板的BDE属性页向ServerModule添加一个TDatabase组件,并把DatabaseName特性的值修改为DBDEMOS。而SessionName特性将自动设置为TSession组件的SessionName特性值。如果不使用存在的别名,那么就需要对数据库组件输入TDatabase.AliasName和TDatabase.DriverName特性值,并对TDatabase组件的Params特性添加一些必要的连接参数。
添加查询 服务器程序的查询能力是通过TQuery组件提供的。从组件面板的BDE属性页添加一个TQuery组件,并对TQuery.DatabaseName特性输入与TDatabase.DatabaseName特性相同的值。对我们的例子来说,只需输入DBDEMOS。SessionName特性将自动添加。
为确保所有一切都配置正确,把下列SQL语句添加到TQuery.Strings特性并将TQuery.Active特性设置为True:
select * from biolife
如果对DatabaseName特性选定了BDE别名DBDEMOS,那么Active特性应一直设置为True。如果使用其他的DatabaseName值,请对SQL语句进行相应的改动。
添加TDataSetProvider 在Delphi的专业版和企业版中,都提供了前三个组件。而TDatabaseSetProvider位于组件面板的Data Access属性页上,只有Delphi企业版才提供该组件。
从Data Access属性页添加一个TDataSetProvider组件,并修改其DataSet、Name和Options特性。DataSet特性应设置为上面添加的TQuery组件的名字。在本例中,该组件命名为Provider。最后,在Object Inspector中向TDataSetProvider.Options特性添加poAllowCommandText选项。poAllowCommandText使得客户可以向数据集提供者传递动态SQL语句。
现在所有的组件都已经到位,剩下的工作就是定义GetTableNames方法。由于该方法依赖于远程数据模块中组件的属性,因此我们必须首先添加这些组件。
编写GetTableNames方法的代码 TServerModule.GetTableNames方法实现了一个COM接口,因此我们只能使用COM所提供的数据类型。其中的一些相当巧妙。为从自动化服务器向客户传递表名的列表,我们必须把这些名字填充到一个可变数组中(如果是TStrings对象就好了)。
function TServerModule.GetTableNames: OleVariant;
var
I : Integer;
TableNames : TStrings;
begin
TableNames := TStringList.Create;
try
Session1.GetTableNames( Database1.DatabaseName, '*.*', True,
False, TableNames );
result := VarArrayCreate( [0, TableNames.Count - 1],
varOleStr);
for I := 0 to TableNames.Count - 1 do
result [I] := TableNames[I];
finally
TableNames.Free;
end;
end;
该函数创建了一个TStrings对象,并调用TSession.GetTableNames方法,来把某个特定数据库中的表名复制到字符串列表中。也可以使用TDatabase.GetTableNames方法,但会话组件中的方法可以更好地控制所返回的表名的格式;该方法的第二个参数为文件掩码,用于指定所返回的表;第三个参数为布尔值,用于指定是否返回文件扩展名。对于DBDEMOS的表,我们将得到.DB和.DBF表文件。代码的下一行创建了一个可变数组,元素为varOleStr(可变OLE字符串),下标从0到表个数减1;然后将TStringList中的表名复制到可变数组中,并返回结果。请注意,字符串列表TableNames是通过try-finally块创建和释放的。代码看起来有些冗长,但要记住OLE/COM是一个Microsoft标准,代码只是进行了一些必要的迂回,以便与COM进行协作而已。
这就是服务器端的工作。编译并运行服务器程序以便向开发程序的PC的注册表添加一个条目,这是个必要的步骤,使得客户程序可以连接到服务器。下一步是创建客户程序。
15.2.2 实现客户程序
客户程序是模仿Delphi企业版中的SQL Explorer建立的(或者是Delphi专业版中的Database Explorer)。该程序在主窗体的左侧使用列表框列出可用的表,并使用了PageControl控件显示了两个属性页。第一个属性页使用TDBGrid显示输出,而第二个属性页使用了一个TMemo控件,使得用户可以输入SQL语句。在图15.5中,一个SQL语句select * from animals已经发送到服务器,此时客户程序的当前焦点位于显示数据的属性页。程序的图形用户界面非常直接,因此我们可以把注意力集中到用于与服务器程序进行连接的组件。
添加TDCOMConnection组件
TDCOMConnection组件位于DataSnap属性页,可以用作与服务器之间的连接组件。如果与客户程序在同一台计算机上运行服务器程序,那么需要添加ServerGUID特性。当输入服务器程序的GUID时,Delphi将更新与该GUID相关联的COM对象的ServerName特性。
图15.5 MIDAS查询演示应用程序的客户端程序
提示:也可使用ServerName特性连接到服务器程序,但GUID更为准确可靠。
服务器GUID是类型库中定义的TGUID值,亦即Server_TLB.pas中所定义的CLASS_servername常数。在我的计算机上,定义该GUID的语句是CLASS_ServerModule: TGUID = '{5A873C25-A15A-11D4-9E2B-000000000000}';。从类型库中复制并粘贴服务器GUID,或输入ServerName,则TDCOMConnection组件将自动填入正确的GUID。
提示:当添加GUID时,包括起始和结束的括弧{}和GUID值。
如果要在远程计算机上运行服务器,除了在远程机器上安装并注册服务器之外,还需要安装DCOM、MIDAS.DLL和STDVCL40.DLL。最后,还要在TDCOMConnection. ComputerName特性中填入远程计算机的名字。本例中将其设置为空即可。
添加TClientDataSet组件
客户端数据集类似于两层应用程序中通常的数据集。TClientDataSet组件知道如何通过连接组件与服务器中的数据集提供者进行通信。添加TClientDataSet组件,并在TClientDataSet.RemoteServer特性给出所用的连接控件(在示例程序中,应是DCOMConnection1)。接下来选择提供者组件的名字。提供者组件指的是服务器程序中的TDataSetProvider组件,该组件的名字是Provider,因此TClientDataSet.ProviderName特性的值就是Provider。
注意:在客户数据集组件中选择ProviderName将运行服务器程序。在选择了提供者组件的名字后,可以将TDCOMConnection.Connected特性重置为False,这样就可以关掉服务器程序。
客户程序从TMemo控件得到SQL语句后,将把该语句赋值给TClientDataSet.CommandText特性。我们只需添加一个TDataSource组件,将数据源连接到TClientDataSet和TDBGrid组件,再编写少许几行代码即可完成该应用程序。
将用户界面连接到TClientDataSet组件
TDataSource.DataSet特性被赋值为客户窗体上的TClientDataSet组件。这进一步展示了面向对象编程的威力。如果没有继承,就无法子类化TDataSet得到TClientDataSet,结果会形成两个数据源类:一个特定于TClientDataSet,而另一个则对应于通常的TDataSet。由于TDataSource组件已经从Data Access属性页添加并连接到TClientDataSet组件,它现在必须赋值给TDBGrid.DataSource特性。
客户程序需要执行三个步骤,这些都必须由代码实现。第一步是在运行时连接到服务器,第二步是从服务器应用程序返回的OLEVariant数组中读出和拆开表名。第三步是将SQL语句从TMemo控件传递到TClientDataSet组件。前两步可以由窗体的构造函数事件处理程序来实现,而第三步可以通过按钮单击事件执行。这两个事件处理程序示范了所有必要的代码。
procedure TFormClientMain.FormCreate(Sender: TObject);
var
I : Integer;
TableNames : OleVariant;
begin
DCOMConnection1.Connected := True;
TableNames := DCOMConnection1.AppServer.GetTableNames;
if VarIsArray(TableNames) then
for I := 0 to VarArrayHighBound(TableNames, 1) do
ListBox1.Items.Add( TableNames[I] );
end;
procedure TFormClientMain.SpeedButton1Click(Sender: TObject);
begin
ClientDataSet1.Close;
ClientDataSet1.CommandText := Memo1.Lines.Text;
ClientDataSet1.Open;
end;
事件处理程序FormCreate负责连接到服务器。服务器程序中的远程数据模块实现了IAppServer接口,并包括了对IAppServer接口的可能的增强。我们向该接口添加了GetTableNames方法;如果像上面那样编写,Delphi将使用新的绑定技术将GetTableNames方法绑定到服务器所包含的实现。事件处理程序的其余部分执行与GetTablesNames相反的操作,从可变数组中读出varOleStr值。
注意:关于编程风格的注记:在示例程序中使用事件处理程序是为了让您把注意力集中于所发生的事情。这种风格对原型和例子程序最为合适。对于软件产品,最好用一些命名良好的函数来实现这些行为,并在事件处理程序中调用这些行为。
执行SQL语句的事件处理程序只是简单地关闭TClientDataSet组件,用TMemo控件的内容更新组件的CommandText特性,再打开TClientDataSet组件。客户和服务器的源代码都包含在随书配套的CD-ROM中。请记住,当部署客户程序时,需要在客户计算机上安装客户程序,以及安装并注册MIDAS.DLL。
15.3 错 误 处 理
当有多个用户同时使用多个客户程序时,存在这样的可能性:两个用户在读取数据后企图更新同一数据。用户1和用户2读取了一条记录,用户1对该记录进行更新,而用户2的记录数据就过期了。当稍后用户2试图更新记录时,就会出现问题:究竟哪个值是正确的。这种矛盾必须得到解决。
Delphi企业版在Object Repository中包含了一个Reconcile Error对话框。从New Items对话框的Dialogs属性页中,可以将该窗体添加到您的MIDAS工程中,这样至少可以在该窗体中对错误进行处理。本节中我们将仔细查看Reconcile Error对话框的各种特征,以及如何将其连接到MIDAS客户程序。为做到这一点,从第13章中的基于RenegadesDemo工程和数据库我们建立了客户程序和服务器程序,这些程序都在本书的CD-ROM上。在转到开始错误处理的新材料之前,让我们快速回顾一下如何建立应用程序。
15.3.1 建立客户与服务器示例程序
大多数情况下,都可以使用上一节中的服务器程序,并使用Renegades示例数据库。如图15.6中前台所示(后台是客户程序),服务器增加了跟踪连接数目的特征。后台的客户程序用基本的Database Form向导,使用TClientDataSet对象和TDCOMConnection对象代替了生成的TTable对象(如果需要复习,可以阅读第13章中有关Database Form向导的章节)。要记住更新TDataSource.DataSet特性以指向客户数据集。
如果要对客户程序再多加一些修饰的话,可以增加TMainMenu和TStatusBar组件。由于我们要更新数据库,因此在File菜单中除了Connect和Exit菜单项之外,还增加了Apply Updates菜单项。
对服务器定义的修改
在服务器程序的Remote Data Module向导中选择了tmSingle Threading模型。由于这是构造函数TComponentFactory.Create的参数ThreadingModel的缺省值,因此在远程数据模块初始化部分的TComponentFactor的构造函数调用中并未看到该参数。
initialization
TComponentFactory.Create(ComServer, TRenegadesModule,
Class_Renegades,
ciMultiInstance);
TComponentFactory类负责创建支持接口的Delphi组件。从上一节我们知道,Single Instance线程模型将序列化COM调用,这样就减少了在代码中提供线程支持的需要。
对线程模型的改变使得我们可以更新服务器窗体的标题。远程数据模块的每个实例——分别对应于每个连接客户,可分别在服务器窗体上调用UpdateUserCount方法。当远程数据模块创建时计数加1,在远程数据模块释放时计数减1。UpdateUserCount驻留在服务器窗体的单一实例中,该方法只是简单地更新整数计数器以及显示该计数器的标签(如图15.6所示)。
图15.6 错误处理演示的客户和服务器
客户程序的增强
客户程序是通过Database Form向导迅速组装起来的。由于客户程序需要将改变更新到数据库文件,我们向客户程序添加了Apply Updates菜单项。ApplyUpdates的实现被添加到该菜单项的Click事件处理程序中。
procedure TFormMain.ApplyUpdates1Click(Sender: TObject);
begin
if( ClientDataSet1.ChangeCount > 0 ) then
ClientDataSet1.ApplyUpdates(-1)
end;
代码中测试了TClientDataSet.ChangeCount特性。如果确实发生了改变,将用参数-1调用TClientDataSet.ApplyUpdates方法,参数的意义是所允许的最多错误个数。使用-1意味着当错误数目已达到最大值时,并不停止更新。
警告:数据集提供者并不检测由于TMemo对象中的字段所引起的冲突。
要将修改保存到数据库中,用户必须调用Apply Updates行为。TClientDataSet.ApplyUpdates生成对事件处理程序BeforeApplyUpdates的调用,调用由TClientDataSet.ProviderName所表示的数据集提供者,并生成对AfterApplyUpdates事件处理程序的调用(如果存在的话),然后调用ReconcileError事件处理程序以处理由数据集提供者返回到客户数据集的错误。通过添加事件处理程序给TClientDataSet.OnReconcileError,客户程序可以提供错误处理功能。
15.3.2 使用错误处理窗体
为提供错误处理的标准化格式,需要将RecErr.pas单元和相应的窗体添加到您的应用程序中。这可以通过使用New Items对话框的Copy功能完成。在New Items对话框的Dialogs属性页上单击Copy单选按钮,然后添加Reconcile Error Dialog(本例中文件被保存为RecErr.pas和RecErr.dfm)。
注意:为透彻了解错误处理窗体的功能,您可以在一台或多台PC上运行Renegades客户程序的多个实例,只要连接到同一服务器程序实例即可。
客户程序与错误处理窗体之间的连接是通过RecErr.pas单元中的一个全局函数进行的。
function HandleReconcileError(DataSet: TDataSet; UpdateKind:
TUpdateKind;
ReconcileError: EReconcileError): TReconcileAction;
把RecErr.pas单元添加到客户窗体的uses子句,并在客户窗体中调用HandleReconcileError。从TClientDataSet.OnReconcileError事件处理程序中,将DataSet、UpdateKind和ReconcileError异常传递到错误处理窗体。这些都是事件方法的参数,因此客户代码显得非常直接。
procedure TFormMain.ClientDataSet1ReconcileError(DataSet:
TCustomClientDataSet; E: EReconcileError; UpdateKind:
TUpdateKind; var Action: TReconcileAction);
begin
Action := HandleReconcileError( DataSet, UpdateKind, E );
end;
当进行更新后,数据集提供者将返回所有出错的行。然后调用上面的事件处理程序,该方法又进而调用全局函数HandleReconcileError(参见图15.7)。
在图15.7中,两个客户程序试图更新Renegade Trevor MacDonald。错误处理对话框显示了修改后的值、发生冲突的值以及原来的值。在窗体的底部,可以选定复选框以只显示发生窗体的字段,或取消复选框以显示所有的字段。在对话框的上部,可以选择对错误进行调整的方案。调整方案是HandleReconcileError函数的返回值,类型为TReconcileAction,它将决定客户程序的行为。对于调整方案的完整列表,请参见表15.2。
图15.7 从Object Repository错误处理窗体中,可在MIDAS
客户-服务器系统中提供标准化的错误处理方法
提示:如果在错误处理对话框中选定调整方案为Correct,那么标准的错误处理窗体允许在该对话框中对修改错误进行更新。
表15.2 TReconcileAction值。可以使用来自Object Repository的标准的错误处理对话框,
也可以创建一个自定义版本,它们都返回TReconcileAction值,客户数据集和数据
集提供者使用这些值来解决记录之间的冲突
方案 |
TReconcileAction值 |
描述 |
Skip |
raSkip |
跳过错误,不更新冲突记录 |
Abort |
raAbort |
停止错误处理 |
Merge |
raMerge |
将更新记录与服务器上的版本合并 |
Correct |
raCorrect |
使用新值替代更新记录 |
Cancel |
raCancel |
清除所有的改变,返回到原来的值 |
Refresh |
raRefresh |
清除所有的改变,使用服务器上的当前值替代该记录 |
使用标准的窗体,可以节约一些额外的工作。如果创建自定义错误处理窗体,需要编写代码显示修改的、冲突的和原来的值,并允许用户选定解决方案。可以将错误处理对话框当做向导来使用。
15.4 公文包客户和服务器程序
有时候用户可能无法访问服务器。他们可能无法通过拨号连接或物理网络连接到服务器,或无法访问HTTP服务器,或者是在路上,等等。当需要建立用户在不连接到服务器也可使用的程序时,可以利用TClientDataSet的继承能力,向用户提供公文包支持。
提示:$(DELPHI)是一条编译器指令,它指向Delphi的安装路径。当相对于Delphi的安装路径定义库和源代码的路径时,可用$(DELPHI)作为Delphi的虚拟路径。
添加支持的步骤是很简明的。为演示在客户程序中支持公文包模型所需的必要改动,我们借用了$(DELPHI)\Demos\Midas\BrfCase\briefcase.bpg工程。
惟一需要的改动是设置TClientDataSet.FileName特性。如果设置了FileName特性,TClientDataSet对象将从该文件中读写数据,当客户连接到服务器时再对服务器进行更新。如果无法连接到服务器,则对服务器的改变将保存在文件中,直至服务器可用时再进行更新。
15.5 小 结
本章示范了几种问题和相应的解决方案,您在开发三层客户-服务器程序时,可能会遇到这些问题。本章的第一部分讨论了通过DCOM连接到服务器程序的基本技术。第二部分示范了对错误的调整。当可能有多个客户程序在同一数据上进行工作时,更新冲突是不可避免的。第三部分演示了对移动应用的支持,其中所需的改动极少。
由于没有冗长的代码列表,您可以确信MIDAS在支持分布式应用程序开发方面具有显著的优势。MIDAS支持Web、Socket、DCOM和Corba等连接模型,在保障用户连接到数据方面提供了强有力的手段。在下一章中,您将学会如何利用Delphi中的这些强大设施来支持Web应用程序开发。