1. DATASNAP 历史
作为MIDAS起始于Delphi3,Delphi4是MIDAS II,Delphi5中是MIDASIII,而后基于COM远程数据模块方式使
用TCP/IP,HTTP,(D)COM构建出强大的通讯能力.从Delphi6开始改名为DataSnap,直到D2007这个框架一直在使
用.D2009重新架构了DataSnap—移除COM依赖,使用TCP/IP以更轻量级的方式生成远程服务对象和客户端连接
能力.同时提供了与Delphi Prism2009开发的.NET程序通讯的功能.
Delphi2010中构建于D2009架构之上,并对此架构做了进一步的扩展,包括使用两个向导来创建新的
部署目标(VCL窗体,Window服务,控制台及面向Web的ISAPI,CGI或Web App Debugger).HTTP(S)传输协
议,HTTP验证,客户端回调函数,REST和JSON的支持,及使用过滤器来支持压缩和解压缩.
1.1 DATASNAP范例数据位置
本白页中我建议您使用Demo和范例来学习.虽然Delphi支持很多数据库系统,使用DBX4,ADO dbGo,或
其他数据存取技术,为了演示方便我这里使用DBX4来操作BlackfishSQL的employee.jds数据库.见
[ C:\Documents and Settings\All Users\Documents\RAD
Studio\7.0\Demos\database\databases\BlackfishSQL\employee.jds].在截图中可以看到我使用的
是Windows Vista或Win7操作系统,使用Windows Server 2008 Web编辑器来部署DataSnap ISAPI服务.
2. DATASNAP目标:如何获取数据
DataSnap2010支持三种不同的Windows方式:VCL窗体,Windows服务和控制台应用程序.本节中我们
将讨论他们的好处,不同和每种方式最适合在什么情况下使用.
下面会创建一个DataSnap服务端和客户端,我们将讲解
TDSServer,TDSServerClass,TDSTCPServerTransport,TDSHTTPService,TDSHTTPWebDispatcher和
TDSHTTPServiceAuthenticationManager组件,以及自定义的服务方法和TDSServerModule类.
将讨论不同的传输协议(TCP,HTTP)的好处及传输效率.并讨论DataSnap服务对象的不同生命期选项
(Server,Session,Invocation),及他们的效率和使用的建议.最后,讨论部署.
2.1. DATASNAP 服务端范例
在Object Repository中有两个不同的DataSnap服务向导:一个是生成基于Windows的Datasnap服务
项目,一个是生成基于WebBroker的DataSnap服务项目(需要部署到IIS或Apache).我们将会演示.
启动了Delphi2010,点击File.New.Other,你会在Object Repository中看到DataSnap服务向导中
显示的三个图标:DataSnap Server,DataSnap WebBroker Server,和Server Module.
双击第一个(后面的两个在下面的小结中讲解),弹出如下对话框:
界面中第一部分是控制项目类型的.默认可以生成可视化的带有主窗体的VCL窗体应用程序.第二个
选项是创建控制台应用程序,生成一个控制台窗口—可以用来输出请求应答信息(用Writeln语句输出
服务应用程序正在做什么).这两种方式都是为了做范例或最初部署,很少用于最终部署.由于
DataSnap架构不再基于COM,客户端将不能使服务端启动.因此为了响应客户端的请求,DataSnap服务
端应该一直在运行.如果你希望应用7X24小时全天候运行,DataSnap服务端必须同时也在运行中.对应
VCL窗体或控制台应用程序,需要一个账户登录到Windows中后才能启动DataSnap服务,背离了这种要
求.第三者选择在这时最适合:一个Windows服务应用程序,安装后配置成为自动启动,当计算机启动后
将自动运行(不需要账户登录).服务应用程序不会弹出界面,很难调试Bug.然而,为了整合这三种的优
势,我将用几分钟创建一个项目组,包括VCL窗体应用程序的DataSnap服务,控制台DataSnap服务,及
Windows服务Datasnap服务,都共享同一个自定义的服务方法,这样就可以开发一个Datasnap服务应用
程序,在需要的时候编译出三个不同类型的部署方式.
第二部分是选择使用的Datasnap服务的通讯协议.和DanaSnap2009相比,我们可以看到多了一个
HTTP通讯,及HTTP验证.为了更加灵活,这里建议选择全部选项,我们可以同时使用TCP/IP,HTTP,及使
用HTTP引入的HTTP验证.
第三部分已经为我们配置好了,如果我们要提供一个服务方法类,我们可以选择它的基
类:TPersistent,TDataModule或TDSServerModule.推荐使用最后的一个选项,可使用RTTI来启动执行
函数 (也可能你觉得使用TDataModule更合适—不操作数据库,或不使用其他非可视控件,这时使用
TPersitent也够用了).
现在是从DSServer.pas中贴出来的一小段代码,来说明TDSServerModule和
TProviderDataModule(也是继承于TDataModule)之间的关系.
TDSServerModuleBase = class(TProviderDataModule)
public
procedure BeforeDestruction; override;
destructor Destroy; override;
end;
{$MethodInfo ON}
TDSServerModule = class(TDSServerModuleBase)
end;
{$MethodInfo OFF}
当无法确定时就使用TDSServerModule选项作为基类.
2.1.1. 创建多目标项目组-- VCL 窗体项目
如上面所说,这里创建多目标的Datasnap服务项目组.首先创建一个VCL窗体应用程序作为Datasnap
服务,选择所有的通讯协议.
默认创建了一个叫做Project1.dproj的项目,并带有三个单元文
件,ServerContainerUnit1.pas,ServerMethodUnit1.pas和Unit1.pas.首先File.Save Project As保
存项目,并输入有实际意义的文件名称.将Unit1.pas保存为MainForm.pas,ServerMethodsUnit1.pas
保存为ServerMethodsUnitDemo.pas文件,保存Project1.dproj为DataSnapServer.dproj.
稍后我们将向项目组添加控制台应用程序和Window服务应用程序.首先我们来检查一下项目,并编
译工程.如果你编译DataSnapServer项目,将会出现一个错误信息(由于我们将
ServerMethodsUnit1.pas改名所致).错误原因是由于ServerContainerUnitDemo.pas单元中的
Implementation部分引用了ServerMethodsUnit1.pas单元.为了修复这个冲突,修改引用单元的文件
名称,从新编译.这是发现在第37行出现错误,使用了ServerMethodsUnit1中的TServerMethods1类型.
修改ServerMethodsUnit1为ServerMethodsUnitDemo.这时可以正确的编译项目了.
ServerContainerUnitDemo的引用部分应该向下面代码所示:
implementation
uses
Windows, ServerMethodsUnitDemo;
{$R *.dfm}
procedure TServerContainer1.DSServerClass1GetClass(
DSServerClass: TDSServerClass; var PersistentClass: TPersistentClass);
begin
PersistentClass := ServerMethodsUnitDemo.TServerMethods1;
end;
end.
2.1.1.1. SERVERCONTAINERUNITDEMO
打开ServerContainerUnitDemo单元,将会看到不少于五个组件:一个TDSServer,一个
TDSServerClass,一个TDSTCPServerTransport(用于TCP/IP通讯),一个TDSHTTPService(用于HTTP
通讯),一个TDSHTTPServiceAuthenticationManager组件(用于HTTP验证).
前面两个一直会存在,其他的三个则是根据选择的通讯协议生成的.
2.1.1.1.1. TDSSERVER
TDSServer组件只有四个属性,AutoStart,HideDSAdmin,Name和Tag.AutoStart属性默认设置为
True,意味着在窗体创建后自动启动DataSnap服务.如果将AutoStart设置为False,需要手动调用
Start方法启动服务,并调用Stop方法停止服务.可以调用Started方法验证DataSnap服务是否已经
启动.
HideAdmin属性默认设置为False.如果设置为True,连接到DataSnap服务的客户端将无法调用
Datasnap服务中的TDSAdmin类的内置方法.TDSAdmin不是一个真正的类,我们可以调用的TDSAdmin
方法定义在DSNames单元:
TDSAdminMethods = class
public
const CreateServerClasses = 'DSAdmin.CreateServerClasses';
const CreateServerMethods = 'DSAdmin.CreateServerMethods';
const FindClasses = 'DSAdmin.FindClasses';
const FindMethods = 'DSAdmin.FindMethods';
const FindPackages = 'DSAdmin.FindPackages';
const GetPlatformName = 'DSAdmin.GetPlatformName';
const GetServerClasses = 'DSAdmin.GetServerClasses';
const GetServerMethods = 'DSAdmin.GetServerMethods';
const GetServerMethodParameters = 'DSAdmin.GetServerMethodParameters';
const DropServerClasses = 'DSAdmin.DropServerClasses';
const DropServerMethods = 'DSAdmin.DropServerMethods';
const GetDatabaseConnectionProperties = 'DSAdmin.GetDatabaseConnectionProperties';
end;
TDSServer组件有五个事件:OnConnect,OnDisconnect,OnError,OnPrepare和OnTrace.我们可以
实现这五个事件来响应不同的情况,例如向日志文件中写入日志.
OnConnect,OnDisconnect,OnError和OnPrepare事件有一个继承于TDSEventObject的参数,包含
了DxContext,传输,服务和DbxConnection组件的属性,在OnConnect和OnDisconnect事件中
TDSConnectEventObject类型还包含了ConnectionProperties和ChannelInfo属性.
TDSConnectEventObject也包括了由错误引起的异常, TDSConnectEventObject还包括了我们要使
用的MethodAlias和ServerClass属性.
OnTrace事件有一个TDBXTraceInfo类型的参数.注意由于这个OnTrace事件处理程序也会包含一
些代码错误,如TDBXTraceInfo和CBRType是编译器未知的.为了解决这个问题,我们需要引用
DBXCommon单元(为识别TDBXTraceInfo类型)和DBComonTypes单元(为识别CBRType类型).
在OnConnect事件处理中,我们可以通过ChannelInfo来查看连接信息,例如(使用自定义的函数
LogInfo向日志文件中写入信息):
procedure TServerContainer1.DSServer1Connect(
DSConnectEventObject: TDSConnectEventObject);
begin
LogInfo('Connect ' + DSConnectEventObject.ChannelInfo.Info);
end;
在OnTrace事件处理程序中我们可以使用TraceInfo.Message中的信息记录服务端正在做什么.
function TServerContainer1.DSServer1Trace(TraceInfo: TDBXTraceInfo): CBRType;
begin
LogInfo('Trace ' + TraceInfo.CustomCategory);
LogInfo(' ' + TraceInfo.Message);
Result := cbrUSEDEF; // take default action
end;
注意,在客户端也可以使用连接到TSQLConnection组件的TSQLMonitor组件来跟踪DataSnap服务
端和客户端之间的通讯(在创建这个DataSnap服务的客户端时讲解).
一个跟踪日志输出如下所示:
17:05:55.492 Trace
17:05:55.496 read 136 bytes:{"method":"reader_close","params":[1,0]}
{"method":"prepare","params":[-1,false,"DataSnap.ServerMethod",
"TServerMethods1.AS_GetRecords"]}
17:05:55.499 Prepare
如你所见,TraceInfo.Message中包括了传输信息的字节数和被调用的方法名称等信息.
2.1.1.1.2. TDSSERVERCLASS
TDSServerClass组件将服务端特定的类发布给远程客户端(使用动态方法调用).
TDSServerClass组件有一个Server属性指向TDSServer组件.其他除了Name和Tag外的重要属
性是LifeCycle.默认是Session,但是也可设置为Server或Invocation.从长到短,Server,Session
和Invocation的意思是一个类的实例在服务端的生命周期为整个服务,一个DataSnap会话或一次
方法调用.Session表示每个连接将获取其自己的服务类实例.如果将其改为Invocation,将会得到
一个无状态的服务类—可用于部署CGI Web服务应用程序(其也是无状态的,每个请求都进行加载
卸载).将LifeCycle改为Server,则所有的连接请求使用一个服务类实例.这可以用于计算请求数
量,但是必须自己保证线程安全.
TDSServerClass有四个事件:OnCreateInstance,OnDestroyInstance(当实例创建和注销时触
发).OnGetClass和OnPrepare.OnPrepare事件可用于准备服务方法.使用D2009或使用D2010手动向
容器中添加TDSServerClass时,OnGetClass事件必须由我们自己实现,以便于指定一个可远程调用
的类.在D2010的向导中,已经自动为我们实现了OnGetClass事件,如下:
procedure TServerContainer1.DSServerClass1GetClass(
DSServerClass: TDSServerClass; var PersistentClass: TPersistentClass);
begin
PersistentClass := ServerMethodsUnitDemo.TServerMethods1;
end;
注意:当我们重命名了自动生成的代码单元ServerMethodsUnit1为
ServerContainerUnitDemo.pas后必须修改这里.
2.1.1.1.3. TDSTCPSERVERTRANSPORT
TDSTCPServerTransport组件负责在DataSnap服务端和客户端进行通讯,使用TCP/IP协议.
TDSTCPServerTransport组件有五个重要的属性:BufferKBSize,Filters(D2010新特
性),MaxThreads,PoolSize,Port和Server.
BufferKBSize属性指定通讯缓冲区大小,默认设置为32KB.Filters属性可以包含一个传输过滤
器集合,将在第四节讲解.MaxThreads属性定义最大线程数(默认为0不限制).PoolSize可用于连接
池(如果修改了这里,也需要相应的修改DataSnap客户端).
Server属性指向TDSServer组件.TDSTCPServerTransport组件没有事件.
2.1.1.1.4. TDSHTTPSERVICE
TDSHTTPService组件负责使用HTTP协议组织DataSnap服务端和客户端通讯.
TDSHTTPService组件有十个属性(除了Name和
Tag):Active,AuthenticationManager,DSHostName,DSPort,Filters,HttpPort,Name,RESTContext,Server,和只读的ServerSoftware属性.
Active属性指定DSHTTPService开始侦听请求.可以在设计时设置,但是这会影响DataSnap服务
在运行时启动(由于DSHTTPService组件侦听了同一个端口—在设计时启动侦听,在运行时就不能
再启动一个侦听了).最好的方式在在TServerContainer的OnCreate事件中激活TDSHTTPService:
procedure TServerContainer1.DataModuleCreate(Sender: TObject);
begin
DSHTTPService1.Active := True;
end;
AuthenticationManager属性用于定义处理HTTP验证的管理组件,这里指向了
TDSHTTPServiceAuthenticationManager组件.这个组件在下节详述.
DSHostName和DSPort属性用于定义DataSnap服务端连接,但只有在没有指定Server属性时生效.
通常都是使用Server属性.
Filters属性可以包含一系列传输过滤器,在第四节详述.
HttpPort属性定义DSHTTPService组件侦听的特定端口以响应连接.注意这个属性默认是80端口,
通常在发布时必须做修改(IIS等Web服务已占用了80端口).
RESTContext属性指定REST上下文URL,这样就可以以REST服务的方式调用DataSnap服务.默
认,RESTContext属性设置为rest,我们可以用http://localhost/datasnap/rest/...来调用服务.
在第六节详述REST,JSON和客户端回调函数.
最后,Server属性指向同一个容器中的TDSServer组件.如果没有指定Server属性,也可以使用
DSHostName和DSPort属性连接到使用TCP的DataSnap服务.当设置了Server属性,DSHostName和
DSport属性失效.
TDSHTTPService组件有五个事件:四个是REST相关的,一个是跟踪事件,REST相关事件将在第六
节详述.
OnTrace事件可用于跟踪对DSHTTPService组件的调用,例如:
procedure TServerContainer1.DSHTTPService1Trace(Sender: TObject;
AContext: TDSHTTPContext; ARequest: TDSHTTPRequest;
AResponse: TDSHTTPResponse);
begin
LogInfo('HTTP Trace ' + AContext.ToString);
LogInfo(' ' + ARequest.Document);
LogInfo(' ' + AResponse.ResponseText);
end;
注意HTTP跟踪信息只有当客户端使用HTTP连接到服务端是才会触发(默认使用TCP/IP协议).
跟踪输出如下所示:
17:05:55.398 HTTP Trace TDSHTTPContextIndy
17:05:55.400 /datasnap/tunnel
17:05:55.403 OK
从中可见,AContext设置为TDSHTTPContextIndy,ARequest设置为/DataSnap/tunnel,AResponse
设置为OK.
2.1.1.1.5. TDSHTTPSERVICEAUTHENTICATIONMANAGER
当选中HTTP通讯协议的验证复选框后,TDSHTTPServiceAuthenticationManager组件将自动出现
在服务容器中.也可手动添加到服务容器中,当然TDSHTTPService组件的AuthenticationManager
属性必须指向TDSHTTPServiceAuthenticationManager组件.
TDSHTTPServiceAuthenticationManager组件有一个事件:OnHTTPAuthenticate事件,可以验证
Datasnap客户端到服务端连接的HTTP信息.
procedure TServerContainer1.DSHTTPServiceAuthenticationManager1HTTPAuthenticate(
Sender: TObject; const Protocol, Context, User, Password: string;
var valid: Boolean);
begin
if (User = 'Bob') and (Password = 'Swart') then
valid := True
else
valid := False
end;
当然,你可以使用数据库技术来扩展验证方式.客户端最好使用HTTPS方式将用户名和密码等
HTTP验证信息发送到服务端,所以我希望易博龙可以在现有HTTP和TCP/IP基础上在添加一个HTTPS
协议.
HTTPS可以确保连接安全和数据包加密, 数据包被窃取也不会泄露用户和密码信息.可与你所在
域的ISP或Web管理员协商是否有可能使用HTTPS—--强烈推荐(如我使用的
是https://www.bobswart.hl/).
DataSnap服务应用程序另一个优势是可将HTTP验证信息(所用协议和上下文信息)记录下来.以
便于查找谁登陆过,谁试图登录(如做欺骗登录操作试图获取DataSnap服务的权限).
2.1.1.2. SERVERMETHODSUNITDEMO
注意我们已经检查了ServerContainerUnitDemo.pas单元,现在看一下DataSnap服务的另外重要
的单元:ServerMethodsUnitDemo.pas单元.在新建DataSnap服务对话框中我们指定了
TDSServerModule类作为基类,因此TServerMethods1类型继承于TDSServerModule(其继承于
TDSServerModuleBase,又继承于TProvideDataModule,添加一个析构函数和BeforeDestruction过
程. TProvideDataModule继承于正常的TDataModule,增加了服务提供者的能力 更多信息见后
面).
由于TServerMethods1继承于TDataModule,设计时可以看到一个数据模块的可视区域.我们可以
向其中添加一下不可视组件,如数据存取控件等.在第三节将操作数据库,现在保持设计区为空,仅
向TServerMethods1类添加方法.
如果不想向项目组添加其他类型的项目—DataSnap控制台应用程序及DataSnap Windows服务应
用程序,可跳到2.1.4节查看实现服务方法.
2.2. DATASNAP 客户端
第一个DataSnap服务端范例启动并侦听请求,现在创建一个客户端.本节中,我们将讲解如何在
客户端连接服务端,如何导入方法生产服务类.
确信DataSnap服务已启动,我们创建一个DataSnap客户端应用程序项目.为了在设计时方便切换
DataSnap服务项目和DataSnap客户端项目,将客户端项目添加到同一个项目组.任何类型的项目都
可作为DataSnap客户端项目,这里我们选择VCL窗口应用程序,保存为DataSnapClient.dpr,主窗体
为ClientForm.pas.控件栏中DataSnap服务目录中含有六个DataSnap(服务)组件,DataSnap客户端
目录中不含我们现在需要的组件.
DataSnap客户端目录中的组件都是一些老的DataSnap组件,可以使用,但不推荐.但可使用新
的TDSProviderConnect组件,可以在新的DataSnap客户端上连接老的DataSnap服务(3.2中详述).
除了DataSnap客户端目录,我们还要看看dbEpress目录,可以找到一个新的组件叫做
TSQLServerMethod(注意:在下一个截图中这个新组件很容易发现,其用TSql前缀替代了TSQL前
缀).
TSqlServerMethod组件可用于调用DataSnap服务的远程方法,但首先需要连接到DataSnap服务.
可以使用TSQLConnect组件建立连接—--不在使用原来的TXXXConnection组件.为了灵活
性,TSQLConnection组件的Drive属性下拉框中有个新选项:DataSnap.在ClientForm上放置一个
TSQLConnection组件,设置其Driver属性为DataSnap.Driver属性将变成可展开的对象,展开后如
下图:
注意:默认CommunicationProtocol属性为空,TSQLConnection将使用TCP/IP作为通信协议(端口
211).BufferKBSize为32(KB),端口设置为211(与服务端设置相同).实际应用中一般将端口号设置
为其他端口(同时修改服务端客户端),因为211端口是DataSnap默认端口不安全.
HostName,UserName和Password用于连接到DataSnap服务.在本地测试时,将HostName设置为
localhost,通常可以设置为服务器名称,DNS或IP地址.
不要忘记将TSQLConnection的LoginPropt属性设置为False,否则将会在连接的时候弹出登录窗
口.
一旦TSQLConnection的Driver属性设置好,就可以设置Connected属性为True去连接DataSnap服
务端.注意这时DataSnap服务必须运行才能连接成功.
2.2.1. DATASNAP客户端类
在确定连接正常后,右击TSQLConnection组件,选择Generate DataSnap Class选项,将生成一个
新单元,默认叫做Unit1,含有一个叫做TServerMethods1Client的类(在DataSnap服务方法类名称
后加了一个Client).保存单元为ServerMethodsClient.pas.
这个TServerMethods1Client类如下所示
type
TServerMethods1Client = class
private
FDBXConnection: TDBXConnection;
FInstanceOwner: Boolean;
FEchoStringCommand: TDBXCommand;
FServerTimeCommand: TDBXCommand;
public
constructor Create(ADBXConnection: TDBXConnection); overload;
constructor Create(ADBXConnection: TDBXConnection;
AInstanceOwner: Boolean); overload;
destructor Destroy; override;
function EchoString(Value: string): string;
function ServerTime: TDateTime;
end;
如你所见,TServerMethods1Client类有两个构造方法,一个析构方法和两个我们在服务端定义
的服务方法.
为使用这些方法,将ServerMethodsClient单元引用到ClientForm,在客户端窗体上放置一个按
钮,在按钮的OnClick事件中写如下代码:
procedure TForm2.Button1Click(Sender: TObject);
var
Server: TServerMethods1Client;
begin
Server := TServerMethods1Client.Create(SQLConnection1.DBXConnection);
try
ShowMessage(DateTimeToStr(Server.ServerTime))
finally
Server.Free
end;
end;
这个代码将创建一个TServerMethods1Client类实例,然后调用ServerTime服务类,最后释放这
个DataSnap服务的代理对象.
点击按钮将弹出对话框显示服务端的时间.
同样方法测试EchoString.
2.2.1.1. HTTP COMMUNICATION PROTOCOL
注意我已经提到TSQLConnection组件以TCP/IP作为默认通讯协议.这样我们就看不到任何HTTP
跟踪信息(在2.1.1.1.4小结中定义).然而,很容易修改通讯协议,仅需在TSQLConnection的Driver
属性中的CommunicationProtocal子属性中输入HTTP即可.注意这是我们还要修改Port属性,因为
211被TCP/IP占用,同时要修改服务端的TDSHTTPService组件的端口号.
修改后运行DataSnap客户端,会出现如下错误:
解决方法是在DataSnap客户端向ClientForm中添加DSHTTPLayer单元引用.
2.2.1.2. HTTP 验证
使用HTTP通讯协议的好处之一是可以使用其包含的HTTP验证.由DataSnap服务端的
TDSHTTPServiceAuthenticationManager组件所支持(详见2.1.1.1.5).
如果实现了OnHTTPAuthenticate事件处理,将会核对HTTP验证.我们必须保证输入正确的信息才
能确保TDSHTTPServiceAuthenticationManager验证通过.否则将会得到一个HTTP/1.1 401未验证
错误.
HTTP检验从客户端传递到服务端的用户名和密码,及其他TDSHTTPServiceAuthentication特定
信息,我们需要在DataSnap客户端的TSQLConnection控件中填写DSAuthUser和DSAuthPassword属
性.
注意我们也需要指定HostName的值,除非是在同一台电脑上测试.
2.3. DATASNAP服务部署
范例的服务端和客户端在同一台电脑上运行良好,但是实际环境中,DataSnap服务将运行在服务
器上,一或多台客户端通过网络连接服务端.服务端程序通常部署在没有安装Delphi的电脑上.这
种情况下就需要考虑不用运行时包来编译DataSnap,而仅生成一个大的可执行文件.由于我们还没
有使用任何数据操作组件,也不需要任何的其他数据库驱动或DLL文件.
2.3.1. DATASNAP 客户端部署
假设客户端与服务端运行在不同的电脑上,我们必须保证客户端可以连接到服务端.为了保证这
点,客户端的TSQLConnection组件不能仅仅指定Driver属性中的CommunicationProtocol和端口,
还需要指定HostName属性.设置IP地址或DNS.例如连接我的DataSnap服务的HostName属性
为http://www.bobswart.nl/.(注意不需要指定http://前缀,因为在CommunicationProtocol属性中指定了
通讯协议).
3. DATASNAP和数据库
除了使用Delphi2010 DataSnap框架创建简单的服务方法,我们还可以在服务端添加数据库操作,
实现多层数据库应用—DataSnap服务连接到数据库,但DataSnap客户端是瘦客户端,不含任何数据
库驱动.
我们同样可以快速创建数据库操作的DataSnap范例,只使用SQLConnection组件和已创建的客户
端类.也可使用另外两个DataSnap组件TsqlServerMethod和TDSProviderConnection.
首先,我们要确保服务端公布了一个数据集,打开ServerMethodsUnitDemo并在数据模块中添加
一个TSQLConnection组件.连接TSQLConnection组件到数据库和表(本例我连接到BlackFishSQL的
Employees表).将TSQLConnection组件添加到数据模块的设计区域.设置Driver属性为
BlachFishSql.然后设置Database属性为employee.jds文件的路径: C:\Documents and Settings\All
Users\Documents\RAD Studio\7.0\Demos\database\databases\BlackfishSQL on Windows
XP, or C:\Users\Public\Documents\ RAD Studio\7.0\Demos\database\databases\BlackfishSQL.
将TSQLConnection的LoginPrompt属性设置为False.设置Connected属性为True验证是否能正确连
接.
下一步,添加一个TSQLDataSet组件,将其SQLConnection设置为TSQLConnection组件.
设置其CommandType为ctQuery,双击CommandText属性输入SQL语句.
SELECT EMP_NO, FIRST_NAME, LAST_NAME, HIRE_DATE, JOB_COUNTRY FROM EMPLOYEE
确保在设计时设置了TSQLConnection组件的LoginPrompt和Connected属性为False,及
TSQLDataSet的Active属性为False.
现在在ServerMethodsUnitDemo单元的TServerMethods1类中添加一个public方法返回一个
TSQLDataSet组件.
type
TServerMethods1 = class(TDSServerModule)
SQLConnection1: TSQLConnection;
SQLDataSet1: TSQLDataSet;
private
{ Private declarations }
public
{ Public declarations }
function EchoString(Value: string): string;
function ServerTime: TDateTime;
function GetEmployees: TDataSet;
end;
如你所见,TServerMethods1类中定义了一个叫做GetEmployees的方法,实现如下:
function TServerMethods1.GetEmployees: TDataSet;
begin
SQLDataSet1.Open; // make sure data can be retrieved
Result := SQLDataSet1
end;
重新编译服务并运行.注意如你关闭了服务但无法重新编译,可能是DataSnap服务还在运行.
3.1. TSQLSERVERMETHOD
回到DataSnap客户端,TSQLConnection组件应该已经断开到DataSnap服务端的连接(如果仍然连
接,说明你没有重新编译服务端).重新将Connected设置为True,从服务端更新信息,并重新从服务
端生成客户端类(使用右键菜单).
为了重写原来的ServerMethodsClient单元,推荐从项目中移除原来的ServerMethodsClient后
在选择’Generate DataSnap Client Classes’方法.保存了新的单元文件后不需要修改客户端代
码.生成了新文件后,可以从中看到GetEmplorees方法.
type
TServerMethods1Client = class
private
FDBXConnection: TDBXConnection;
FInstanceOwner: Boolean;
FEchoStringCommand: TDBXCommand;
FServerTimeCommand: TDBXCommand;
FGetEmployeesCommand: TDBXCommand;
public
constructor Create(ADBXConnection: TDBXConnection); overload;
constructor Create(ADBXConnection: TDBXConnection;
AInstanceOwner: Boolean); overload;
destructor Destroy; override;
function EchoString(Value: string): string;
function ServerTime: TDateTime;
function GetEmployees: TDataSet;
end;
为使用GetEmployees方法获取TDataSet,我们可以使用TsqlServerMethod组件.将
TsqlServerMethod组件添加到客户端窗体,设置其SQLConnection属性为SQLConnection1,然后打
开ServerMethodName下拉框显示可用的方法:一些DSAdmin方法(可通过设置TDSServer的
HideDSAdmin属性为True禁止),接下来是三个DSMetaData方法,七个TServerMethods.As_XXX方法
(由原来的IAppServer提供),和最后我们自己的TServerMethods1的方法:EchoString,ServerTime,
和GetEmployees方法.
本例我们选择TServerMethods1.GetEmployees作为ServerMethodName属性的值.根据
ServerMethodName属性值,SqlServerMethod控件获取查询结果记录集.
我们使用TDataSetProvider,TClientDataSet和TDataSource来使数据显示在TDBGrid中.
在客户端添加一个TDataSetProvider控件,将其DataSet设置为SqlServerMethod控件.下一步添
加一个TClientDataSet控件,将其ProviderName设置为DataSetProvider控件.设置其
RemoteServer属性为空—--这是用于原有DataSnap的方法,在新的DataSnap架构中不再使用.
最后,放一个TDataSource控件,设置其DataSet为TClientDataSet控件,然后就可以放TGBGrid和
TDBNavigator控件了.设置其DataSource为TDataScource组件,将可以在客户端窗体中看到数据.
为了在设计时验证连接,我们设置ClientDataSet的Active属性为True.将触发SqlServerMethod
的Active为True(开始检索数据,然后自动将Active设置为False),同时会将
TSQLConnection.Connected设置为True连接到服务端.
这种方式连接一直保持,TCliendtDataSet和TSQLConnection的被激活,数据显示在TDBGrid中.
这种方式很容易以只读方式查看数据.注意这里说只读,因为TSQLServerMethod不允许
TDataSetProvide-TClientDataSet组合将客户端的数据变更传回服务端.
TSQLServerMethod是轻量级方式连接获取只读数据.这样,如果你需要获取DataSnap服务器上的
数据但不需要做修改,当前这种发布一个返回TSQLDataSet结果集方法的架构就很好.
然而,有时我们需要更新数据,这时就必须使用不同的方法获取数据.
3.2. TDSPROVIDERCONNECTION
如果我们想提交更新,我们就需要TDSProviderConnection组件与服务端的TDataSetProvider关
联,因此我们不但可以读取数据也可以修改数据.
首先,我们需要修改服务端数据模块,现有的只是返回一个TDataSet,我们必须添加一个实际的
TDataSetProvider,确保将其从DataSnap服务端发布到客户端.所以,回到ServerMethodsUnitDemo
单元放一个TDataSetProvider组件,设置其DataSet属性为TSQLDataSet.应该将TDataSetProvider
重命名,如dspEmplyees;
现在,重新编译DataSnap服务,运行.然后修改客户端.
3.2.1. TDSPROVIDERCONNECTION 客户端
为检索到发布的TDataSetProvider组件需要修改一下DataSnap客户端.将TSQLServerMethod和
TDataSetProvider组件从客户端窗体删除,添加一个TDSProviderConnection组件.设置其
SQLConnection属性为TSQLConnection组件,连接到DataSnap服务.我们也需要设置一个
ServerClassname属性值(很不幸不能选择只能输入).现在只能手动输入TDSServerModule的名字,
这里是TServerMethods1.
在前一个例子中,TClientDataSet只设置了一个ProviderName属性.然而,使用
TDSProviderConnection组件,我们必须首先设置其RemoteServer属性为TDSproviderConnection
组件,然后设置ProviderName属性(这个属性还为原来设置的DataSetProvider1,现在设置为
dsEmployeer----在服务端发布的TDataSetProvider组件的名称).
在ProvideName属性的下拉框中显示dspEmployees选项(在服务端的ServerDataModule单元中发
布的名称).
现在我们可以设置TClientDataSet.Active为True,在设计时查看数据.
设置TClientDataSet.Active为True,同时将TSQLConnection.Connected置为True.注意在设计
时最好不要将这两个属性设置为True.首先,如果你在IDE中打开DataSnap客户端项目,将会试图连
接服务端,如果服务端没开启将失败.其次,如果在运行时启动应用程序,并且连接不可用,应用程
序将会抛出异常.从而应用程序不能使用本地数据,导致远程连接无效将不能使用应用程序.
最好的方式是使用菜单选项或按钮明确的设置TSQLConnection组件连接,TClientDataSet组件
进行获取数据.并提交包括username/password的信息,将在后面说明.现在确保
TClientDataSet.Active属性设置为False,同时TSQLConnection.Connected属性为False.在客户
端窗体中放一个按钮,在OnClick事件中明确的打开TClientDataSet.
procedure TForm2.Button2Click(Sender: TObject);
begin
ClientDataSet1.Open;
end;
现在添加代码提交数据变更,保存回服务器.
3.2.2 数据库更新
有两种方法将数据修改保存会服务端:自动和手动.都是调用一下方法,但是会自动调用或手动
调用,各有优缺点.
对于自动方式,我们可以使用TClientDataSet的数据修改时触发的OnAfterInsert,OnAfterPost
和OnAfterDelete事件.在事件处理程序中,实现很简单,调用TClientDataSet的ApplyUpdates方法,
发送变更,将Delta包发送到服务端保存回数据库.
procedure TForm2.ClientDataSet1AfterPost(DataSet: TDataSet);
begin
ClientDataSet1.ApplyUpdates(0);
end;
如果发生了更新错误,将会触发TClientDataSet的OnReconcileError事件,更多信息见3.2.3
手动方式发生更新也是使用TClientDataSet的ApplyUpdates方法.但是这时方法不在
OnAfterInsert,OnAfterPost和OnAfterDelete事件中执行.而是我们添加一个按钮让用户显示的提交更
新.
procedure TForm2.btnUpdateClick(Sender: TObject);
begin
ClientDataSet1.ApplyUpdates(0);
end;
自动提交的好处当然用户不会忘记将变更保存回服务端.然而,缺点是无法提供Undo能力.一旦
提交数据就更新回了服务器.另外,如果使用手动提交,所有变更保存在客户端---TClientDataSet
组件的内存中.这样就允许用户Undo部分变更:使特定记录或全部记录放弃更新.点击更新按钮显
示调用ApplyUpdate方法.可能会导致用户忘记提交修改数据.我们应该在窗口关闭时添加代码检
查TClientDataSet中是否还有未提交数据(检查TClientDataSet.ChangeCount属性).
3.2.3. RECONCILE ERRORS
TClientDataSet.ApplyUpdates方法有一个参数:应用更新时允许发生的最大错误数量.如果有
两个客户端连接到了DataSnap服务端,获取Employees数据并同时修改了第一行数据.依据目前为
止我们的实现,两个客户端都会使用TClientDataSet的ApplyUpdates将数据变更到DataSnap服务
端.如果都将ApplyUpdates的参数MaxErrors设置为0,则第二个客户端的提交将会停止.第二个客
户端应该使用一个大于0的参数指定允许的错误/冲突数.然而,即使第二个客户端将MaxErrors设
置为-1(不管有多少错误发生都继续提交后面的更新记录),都不会提交被第一个用户更新过的记
录.换句话说,你需要执行一系列冲突处理来解决这些更新已经被更新的记录或列的冲突问题.
幸运的是,Delphi提供了一个很强大的对话框来处理这个问题.当在DataSnap客户端需要做一些
冲突处理时,都可以使用这个对话框(或自己实现,但最终都是处理冲突问题).
使用Delphi提供的功能,File.New.Other,在Delphi文件子目录中选择Reconcile Error对话框
图标.
选中这个图标点击OK,保存为RecError.pas,加入到DataSnapClient项目.这个单元包括了定义
和实现更新错误对话框.来解决数据库更新错误.
ReconcileErrorForm窗体实例将按需要动态创建.那么如何使用这个特殊的ReconcileErrorForm窗体呢?
好,其实很简单.对于每个没有成功更新的记录,都会触发TClientDataSet的OnReconcileError事件.定义如
下:
procedure TForm2.ClientDataSet1ReconcileError(DataSet: TClientDataSet;
E: EReconcileError; UpdateKind: TUpdateKind;
var Action: TReconcileAction);
这个事件处理程序与四个参数,第一个是抛出错误的TClientDataSet,第二个参数是引发错误冲
突的原因,第三个参数是更新类型UpdateKind(insert,delete,modify),第四个参数是你要如何处
理冲突.可以返回如下枚举类型值:
- raSkip:不更新这条记录,但在变更日志中保留未提交的变更,下次提交在试.
-raAbort:取消记录冲突处理..
-raMerge:将更新记录与远程数据库记录合并,仅在客户端变更修改过的远程字段
-raCorrect:使用正确的值替换更新记录,这需要用户介入.
-raCancel:对本记录的修改全部放弃.回到初始值状态.
-raRefresh:对本记录修改全部放弃,但重新加载当前数据库的记录值.
关于ReconcileErrorForm不需要考虑全部执行选项.只需要做两件事件.一,在DataSnap客户端主窗体中
引用错误处理对话框单元.二,在OnReconcileError事件中写一行代码调用ReconcileErrorForm单元中的
HandleReconcileError全局函数. HandleReconcileError函数也有四个同样的参数,只需要按顺序传递即可.
如下所示:
procedure TFrmClient.ClientDataSet1ReconcileError(DataSet: TClientDataSet;
E: EReconcileError; UpdateKind: TUpdateKind;
var Action: TReconcileAction);
begin
Action := HandleReconcileError(DataSet, UpdateKind, E)
end;
3.2.4. 示范冲突错误
现在最大的问题是:实际工作中如何使用的冲突处理?为了测试,需要两个或更多DataSnap客户
端同时运行.为使用当前的客户端和服务端进行测试,需要执行如下步骤:
-启动服务端应用程序
-启动第一个客户端应用程序,点击链接按钮,获取数据
-启动第二个客户端应用程序,点击链接按钮,获取数据
-使用第一个客户端应用程序,修改第一行数据的FirstName列
-使用第二个客户端应用程序,修改第一行数据的FirstName列
-在第一个客户端应用程序中点击更新按钮
-在第二个客户端应用程序中点击更新按钮,这时将会发生一个或多个错误.因为第一个应用程
序已经修改了同一行的同一个列.引起冲突. OnReconcileError被触发.
-进入更新错误对话框,现在可以处理冲突(忽略(Abort),取消(Abort),合并(Merge),更正(Correct),取消
(Cancel),更新(Refresh)).测试一下Skip和Cancel的不同,及Correct,Refresh和Merge的不同.
Skip移动到下一行记录,忽略更新请求.但其更新将保留在更新日志中.Cancel也忽略更新请求
同时清除本记录所有以前的更新记录.
Refresh清除本记录所有的变更记录,并将数据库中的值作为当前记录的值.Merge试图将数据库
记录和更新记录合并.将变更提交到数据库.更新和合并的记录都不会再进行处理,记录已经与数
据库同步.
Correct是一个强大的选项,在事件处理中给你一个指定更新记录值的机会.需要写代码或弹出
对话框指定新值.
3.3. DATASNAP 数据库部署
部署一个使用数据库的DataSnap服务需要比部署一个简单DataSnap服务的步骤要多些.客户端,
没什么变化,还是一个瘦客户端,如将MidasLib加入到了项目引用就仅需部署一个单一的可执行文
件.
服务端,必须部署数据库驱动.及所选数据库依赖的驱动和文件.使用DBX4,确保发布
TSQLConnection组件和dbxconnections.ini及dbxgrivers.ini文件(可在C:\Documents and
Settings\All Users\ Documents\RAD
Studio\dbExpress\7.0 directory on Windows XP or in the C:\Users\Public\Documents\RAD
Studio\dbExpress\7.0中找到). dbxdrivers.ini文件指定所用驱动, DriverPackageLoader及
MetaDataPackageLoader(通常指向同一个包).对于BlackFishSQL,使用的DBXClientDriver140.bpl文件,需要
部署到服务端.更多关于部署BlashFishSQL信息见RAD Studio\7.0 目录下的deploy_en.htm文件.
3.4. 重用已有的远程数据模块
如果你有一个远程数据模块类,也可以将其组合到新的DataSnap项目中来.但是必须要牺牲一些特性,尤其
是引入了COM.
首先,如果有一个你要迁移的DataSnap服务应用程序,而不仅仅是一个远程数据模块,你需要使用命令行
/unregister注销DataSnap服务.不做这步将无法注销远程数据模块.在远程数据模块单元,移除initialization区
域.如果还希望这个单元在D2007及一下版本重用,可以使用编译开关:
{$IF CompilerVersion >= 20}
initialization
TComponentFactory.Create(ComServer, TRemoteDataModule2010,
Class_RemoteDataModule2010, ciMultiInstance, tmApartment);
{$IFEND}
end.
从项目中移除UpdateRegistry函数或用编译开关修饰:
{$IF CompilerVersion >= 20}
class procedure UpdateRegistry(Register: Boolean;
const ClassID, ProgID: string); override;
{$IFEND}
最重要的变更—将项目转换为无COM依赖的DataSnap服务.-及移除类型库(.ridl文件)和类型库导入单元.这
无法通过编译器开关修改,因此需要为D2007及一下版本和D2009及以上版本分别生成一个项目文件.在
TRemoteDataModule类中放一个TDSServerClass组件.最后,将所有的自定义方法都转到
TRemoteDataModule单元的public节中.
4. DATASNAP 过滤器[FILTER]用法
本节将说明过滤器工作原理,及如何使用已存在的过滤器(如压缩)或创建新的DataSnap过滤器.DataSnap
过滤器是一个特殊的DLL,拦截通讯流,在整个过滤器链中操作通讯流.所以本例中我们可以使用压缩和解压缩
过滤器,或记录压缩等.
必须在客户端和服务端指定过滤器.在服务端,必须指定TDSTCPServerTransport组件的过滤器属性列表.
在客户端,必须在客户端项目中引用过滤器单元文件.对于客户端这就足够了,因为每个DataSnap过滤器都会
自动注册.
当处理OnConnect事件时,可以检查用于连接的已注册的过滤器,例如使用自定义的日志函数输入日志信息,
procedure TServerContainer1.DSServer1Connect(
DSConnectEventObject: TDSConnectEventObject);
var
i: Integer;
begin
LogInfo('Connect ' + DSConnectEventObject.ChannelInfo.Info);
for i:=0 to DSConnectEventObject.Transport.Filters.Count-1 do
LogInfo(' Filter: ' +
DSConnectEventObject.Transport.Filters.GetFilter(i).Id);
end;
4.1. ZLIBCOMPRESSION FILTER
作为范例,我们使用已随D2010提供的DataSnap过滤器.可用于在客户端和服务端压缩数据流.这里说的
ZlibCompression过滤器可以在DbxCompressionFilter单元找到.
TDSTCPServer和TDSHTTPService组件都有一个TTransportFiltersCollection类型的Filters属性.点击Filters
属性后面的按钮,编辑过滤器列表.在这个对话框中,我们可以加一个新的TTransportFilterItem,然后在Object
Inspector中设置FilterID和一些属性.在下拉框中可以找到Delphi2010提供的ZLibCompression过滤器.
注意除了设置服务端TDSTCPServerTransport组件Filters属性外,也需要在客户端指定一个相应的过滤器
(压缩请求解压缩应答).这时,我们进需要将DbxCompressionFilter单元引用到ClientForm.其将自动注册一个
TTransportCompressionFilter并与服务端通讯.
如果没有在客户端添加DbxCompressionFilter单元引用,运行客户端后将会抛出异常信息: