目的:
三层结构中客户端取数一般是采用
TClientDataSet =====> TDataSetProvider + TDataSet
有些人使用TDataSetProvider + TTable,然后在设计期间设置TTable的属性
缺点很明显:
1、不能使用SQL实现复杂的查询
2、TTable的属性在设计期间完全确定了,也就只能只能打开固定的一个表,
要使用多个表怎么办?只好在RemoteDataModule中放无数的TDataSetProvider和TTable这种做法不能满足我们的要求,我们需要的是:
1、在Client端使用SQL语句打开数据集
2、Provider应该能访问任意数据,
最好做到多个ClientDataSet同时使用一个Provider访问不同的数据Delphi的帮助里没有例子,不过我们还是很容易就找到了实现方法:
Server端:TDataSetProvider + TQuery
Client端:TClientDataSet
三层结构中客户端取数一般是采用
TClientDataSet =====> TDataSetProvider + TDataSet
有些人使用TDataSetProvider + TTable,然后在设计期间设置TTable的属性
缺点很明显:
1、不能使用SQL实现复杂的查询
2、TTable的属性在设计期间完全确定了,也就只能只能打开固定的一个表,
要使用多个表怎么办?只好在RemoteDataModule中放无数的TDataSetProvider和TTable这种做法不能满足我们的要求,我们需要的是:
1、在Client端使用SQL语句打开数据集
2、Provider应该能访问任意数据,
最好做到多个ClientDataSet同时使用一个Provider访问不同的数据Delphi的帮助里没有例子,不过我们还是很容易就找到了实现方法:
Server端:TDataSetProvider + TQuery
Client端:TClientDataSet
// for Delphi 5:
TDataSetProvider.Options属性中应该包括poAllowCommandText,
允许ClientDataSet使用SQL命令
Client端程序:
with ClientDataSet do
begin
Close;
CommandText := 'Select xxx ......';
Open;
end;
// for Delphi 4:
手头找不到D4了,因此不知道D4是否支持TClientDataSet.CommandText,
我们以前是这样做的:
Client端程序:
with ClientDataSet do
begin
Close;
DataRequest('Select xxx ......');
Open;
end;
Server端程序:
TDataSetProvider.OnDataRequest(Sender: TObject; Input: OleVariant): OleVariant;
begin
// 这里的Input就是客户端的ClientDataSet调用DataRequest方法时传递的参数
// 将它赋值给Query.SQL.Text就可以了
Query.SQL.Text := Input;
end;
好了,用上面的方法我们在Client端使用SQL了,也使得Provider与数据集无关,
Server端只需要公布一个Provider就可以供Client端的多个ClientDataSet使用了,看起来真不错,问题似乎得到了完美的解决 :))
---------------------------------------------------------------------
发现了BUG: :<<<
按上面的方法,Server端使用一个DataSetProvider
Client端使用两个TClientDataSet,都使用Server端的同一个Provider,分别打开两个表,如下:
with ClientDataSet1 do
begin
Close;
CommandText := 'Select * from Table1';
Open;
end;
with ClientDataSet2 do
begin
Close;
CommandText := 'Select * from Table2';
Open;
end;
上面的代码中ClientDataSet与数据库表的对应关系如下:
ClientDataSet1 =====> "Table1"
ClientDataSet2 =====> "Table2"
注意顺序为先打开表1,再打开表2
然后我们修改ClientDataSet1中的数据,再调用
ClientDataSet1.ApplyUpdates(-1)方法提交数据,却发现无论如何都成功不了,奇怪!
同样的数据直接在两层C/S结构的程序中可以成功地写入表中,可以肯定数据没问题
程序绝对正确,同样的程序早就调试通过了,现在却突然不能提交数据了,而且,奇怪的是,错误很稳定,100%提交失败,系统重起N次也没影响
---------------------------------------------------------------------
调试:
程序和数据都没问题,实在想不出错在哪里,只好用最低级的方式了,
打开M$ SQL Trace,睁大眼睛看提交到SQL Server上的所有SQL语句,然后,在三层结构下提交数据、在两层C/S结构下提交数据,比较SQL语句有什么不同,这下更奇怪了,程序中Insert一条记录,ApplyUpdates后看到的SQL语句是
insert into xxxx(xx,xx,...) values(xx,xx,...)
拿出来执行,返回的错误信息居然是'invalid column name xxxxxx',字段不存在????
Select * from xxxx 看到字段明明在那里嘛!
难道SQL有毛病?? 删掉表,重建,再来一遍错误一模一样,奇怪!
重复N遍之后,终于发现,Insert语句中操作的表居然是Table2!!!
但是程序中打开的是Table1,这么说来,MIDAS在提交数据时操作的目标表错了,跟本就不是Client端实际使用的表!
---------------------------------------------------------------------
分析:
打开Delphi 源代码 provider.pas ,搜索ApplyUpdates之类的东西,看究竟是如何实现的,最后看到了根源,如下:
// Delphi 源代码 provider.pas
procedure TDataSetResolver.DoUpdate(Tree: TUpdateTree);
begin
with Tree do
begin
...
Source.Edit;
...
end;
end;
procedure TDataSetResolver.DoDelete(Tree: TUpdateTree);
begin
with Tree do
begin
...
Source.Delete
...
end;
end;
procedure TDataSetResolver.DoInsert(Tree: TUpdateTree);
begin
Tree.Source.Append;
...
end;
其中TUpdateTree.Source定义为
TUpdateTree = class(TObject)
...
property Source: TDataSet read FSourceDS;
...
end;
水平有限,具体如何没看完,不过根据前面调试的结果,和上面的源代码结合起来,
估计这个Source: TDataSet最终就等于TDataSetProvider.DataSet
这样一来,如果我们用同一个Provider同时打开多个表,只有最后打开的ClientDataSet可以保证Server端的TDataSetProvider.DataSet和它对应着相同的数据集二前面打开的ClientDataSet就根本不能提交了, 这样一来,我们前面提出的方案就彻底崩溃了,根本原因就在于:
TDataSetProvider的根本就是为单个客户端设计的,完全没有考虑到同时访问多个数据集的问题,实际运行中永远只有最后打开的一个数据集能够保持存在,因而也就只有最后打开的一个ClientDatSet能提交数据成功
---------------------------------------------------------------------
结论:
要解决应该很简单,TDataSetProvider应做以下修改:
1、建立一个内部的DataSet队列
2、ClientDataSet打开数据集时,创建一个新的DataSet,加入到DataSet队列中
3、ClientDataSet关闭时,从DataSet队列中删除对应的DataSet
4、创建新DataSet时,可以根据TDataSetProvider.DataSet属性来确定使用的实际DataSet类
也就是说,如果DataSet指定为一个TQuery,那么就创建一个TQuery;
如果是一个TTable,那么就创建一个TQuery
这点很容易实现
5、创建新DataSet后,应该将TDataSetProvider.DataSet对应的TDataSet的全部属性赋值给新DataSet
6、定义一套规范,将ClientDataSet与DataSet队列中的TDataSet一一对应
7、取数、传递数据包、提交数据的代码都不需要变动,只需要根据ClientDataSet
在DataSet队列中找到对应的TDataSet,其余的工作完全一样
说起来简单,这种事情我们做不了,恐怕应该由Borland自己来干了!
写到最后,得出的结论基本上是悲剧性的:
1、最初提出的方案基本可行
2、但是绝对不能让多个ClientDataSet同时使用同一个Provider访问数据
否则将只有最后打开的ClientDataSet能提交数据成功
3、如果有多个ClientDataSet都需要修改数据集并提交的话,那么就不能只用一个Provider了,只好在RemoteServer中公布几个Provider
4、注意,错误只出在"同时"访问数据时,因此轮流使用还是可以的
唉!!真是悲哀啊!!!
TDataSetProvider.Options属性中应该包括poAllowCommandText,
允许ClientDataSet使用SQL命令
Client端程序:
with ClientDataSet do
begin
Close;
CommandText := 'Select xxx ......';
Open;
end;
// for Delphi 4:
手头找不到D4了,因此不知道D4是否支持TClientDataSet.CommandText,
我们以前是这样做的:
Client端程序:
with ClientDataSet do
begin
Close;
DataRequest('Select xxx ......');
Open;
end;
Server端程序:
TDataSetProvider.OnDataRequest(Sender: TObject; Input: OleVariant): OleVariant;
begin
// 这里的Input就是客户端的ClientDataSet调用DataRequest方法时传递的参数
// 将它赋值给Query.SQL.Text就可以了
Query.SQL.Text := Input;
end;
好了,用上面的方法我们在Client端使用SQL了,也使得Provider与数据集无关,
Server端只需要公布一个Provider就可以供Client端的多个ClientDataSet使用了,看起来真不错,问题似乎得到了完美的解决 :))
---------------------------------------------------------------------
发现了BUG: :<<<
按上面的方法,Server端使用一个DataSetProvider
Client端使用两个TClientDataSet,都使用Server端的同一个Provider,分别打开两个表,如下:
with ClientDataSet1 do
begin
Close;
CommandText := 'Select * from Table1';
Open;
end;
with ClientDataSet2 do
begin
Close;
CommandText := 'Select * from Table2';
Open;
end;
上面的代码中ClientDataSet与数据库表的对应关系如下:
ClientDataSet1 =====> "Table1"
ClientDataSet2 =====> "Table2"
注意顺序为先打开表1,再打开表2
然后我们修改ClientDataSet1中的数据,再调用
ClientDataSet1.ApplyUpdates(-1)方法提交数据,却发现无论如何都成功不了,奇怪!
同样的数据直接在两层C/S结构的程序中可以成功地写入表中,可以肯定数据没问题
程序绝对正确,同样的程序早就调试通过了,现在却突然不能提交数据了,而且,奇怪的是,错误很稳定,100%提交失败,系统重起N次也没影响
---------------------------------------------------------------------
调试:
程序和数据都没问题,实在想不出错在哪里,只好用最低级的方式了,
打开M$ SQL Trace,睁大眼睛看提交到SQL Server上的所有SQL语句,然后,在三层结构下提交数据、在两层C/S结构下提交数据,比较SQL语句有什么不同,这下更奇怪了,程序中Insert一条记录,ApplyUpdates后看到的SQL语句是
insert into xxxx(xx,xx,...) values(xx,xx,...)
拿出来执行,返回的错误信息居然是'invalid column name xxxxxx',字段不存在????
Select * from xxxx 看到字段明明在那里嘛!
难道SQL有毛病?? 删掉表,重建,再来一遍错误一模一样,奇怪!
重复N遍之后,终于发现,Insert语句中操作的表居然是Table2!!!
但是程序中打开的是Table1,这么说来,MIDAS在提交数据时操作的目标表错了,跟本就不是Client端实际使用的表!
---------------------------------------------------------------------
分析:
打开Delphi 源代码 provider.pas ,搜索ApplyUpdates之类的东西,看究竟是如何实现的,最后看到了根源,如下:
// Delphi 源代码 provider.pas
procedure TDataSetResolver.DoUpdate(Tree: TUpdateTree);
begin
with Tree do
begin
...
Source.Edit;
...
end;
end;
procedure TDataSetResolver.DoDelete(Tree: TUpdateTree);
begin
with Tree do
begin
...
Source.Delete
...
end;
end;
procedure TDataSetResolver.DoInsert(Tree: TUpdateTree);
begin
Tree.Source.Append;
...
end;
其中TUpdateTree.Source定义为
TUpdateTree = class(TObject)
...
property Source: TDataSet read FSourceDS;
...
end;
水平有限,具体如何没看完,不过根据前面调试的结果,和上面的源代码结合起来,
估计这个Source: TDataSet最终就等于TDataSetProvider.DataSet
这样一来,如果我们用同一个Provider同时打开多个表,只有最后打开的ClientDataSet可以保证Server端的TDataSetProvider.DataSet和它对应着相同的数据集二前面打开的ClientDataSet就根本不能提交了, 这样一来,我们前面提出的方案就彻底崩溃了,根本原因就在于:
TDataSetProvider的根本就是为单个客户端设计的,完全没有考虑到同时访问多个数据集的问题,实际运行中永远只有最后打开的一个数据集能够保持存在,因而也就只有最后打开的一个ClientDatSet能提交数据成功
---------------------------------------------------------------------
结论:
要解决应该很简单,TDataSetProvider应做以下修改:
1、建立一个内部的DataSet队列
2、ClientDataSet打开数据集时,创建一个新的DataSet,加入到DataSet队列中
3、ClientDataSet关闭时,从DataSet队列中删除对应的DataSet
4、创建新DataSet时,可以根据TDataSetProvider.DataSet属性来确定使用的实际DataSet类
也就是说,如果DataSet指定为一个TQuery,那么就创建一个TQuery;
如果是一个TTable,那么就创建一个TQuery
这点很容易实现
5、创建新DataSet后,应该将TDataSetProvider.DataSet对应的TDataSet的全部属性赋值给新DataSet
6、定义一套规范,将ClientDataSet与DataSet队列中的TDataSet一一对应
7、取数、传递数据包、提交数据的代码都不需要变动,只需要根据ClientDataSet
在DataSet队列中找到对应的TDataSet,其余的工作完全一样
说起来简单,这种事情我们做不了,恐怕应该由Borland自己来干了!
写到最后,得出的结论基本上是悲剧性的:
1、最初提出的方案基本可行
2、但是绝对不能让多个ClientDataSet同时使用同一个Provider访问数据
否则将只有最后打开的ClientDataSet能提交数据成功
3、如果有多个ClientDataSet都需要修改数据集并提交的话,那么就不能只用一个Provider了,只好在RemoteServer中公布几个Provider
4、注意,错误只出在"同时"访问数据时,因此轮流使用还是可以的
唉!!真是悲哀啊!!!