客戶端實現SQL查詢
由於在客戶端不存在TQuery控件,似乎客戶服務器模式是無法做SQL查詢的。 但是,Delphi很好的解決了這個問題。事實上,只要客戶端連接上服務端應用程 序,客戶端的TClientDataSet就包含了一個名字為Provider的屬性,對應到服務器端DataSetProvider的所有默認屬性和方法,其中DataSetProvider有一個 Options屬性,只要讓其中的poAllowCommandText=true,那麼, DataSetProvider的poAllowCommandText就可以接受前台來的SQL 命令,並傳送 給TQuery。可以看出,真正傳遞數據的是DataSetProvider的接口,所以,用這 個接口搭建傳遞SQL 的橋梁是必需的。
客戶端進行SQL 查詢的方法是:
ClientDataSet.Close;
ClientDataSet.CommandText := 'SQL語句';
ClientDataSet.Open;
下面通過一個實例來說明方法:
一、SQL 服務器端程序:
首先用上面相同的方法建立一個服務器端COM 工程。工程名取為:PcSQL。
放入一個Query和一個DataSetProvider。
DataSetProvider的屬性Options下的poAllowCommandText=true
這實際上已經建立了一個基於SQL 查詢的服務器端程序。
客戶端查詢服務器端的別名集
在SQL 查詢以前,用戶往往需要指定查詢哪個數據庫,所以需要把服務器上 BDE 數據庫別名(Alias )設置數據抓到前台程序來具體做法我們通過一個實例 來解決:
還是先回到服務器端:
在上述服務器端再建立一個TSession和一個TdataBase(在BDE 頁下)
Session1屬性:
AutoSessionName=true
SessionName=Session1_4 (運行中是可以自動調整的)
KeepConnection=true
Active=true
DataBase1屬性:
AliasName: 連接數據庫別名(比如Mymdb)
DataBaseName: 自定義的名(這裡比如為a)
SessionName: Session1_4 (SessionName值)
如果不希望輸入密碼:
LoginPrompt : false
這樣Query1的連接屬性要變一下,因為它要通過Database來聯接數據庫(以 後在前台可以直接通過改變DataBase的屬性來改變聯接)
DataBaseName= DataBase1的DataBaseName:自定義的名(這裡比如為a)。
SQL屬性輸入一個基本的SQL語言:select * from 獎金 Active=true 表示這 樣也可以連接上了。
TQurey 增加個屬性
SessionName: Session1_4
這樣就可以用Session對象來管理數據源的信息了。
再加入四個Label,顯示當前在線人數,以及進行SQL 查詢的人數。
為了傳給客戶端值,並能夠由客戶端來對服務器端進行某些控制,需要在COM 中加入兩個方法:
View -> Type Library 顯示接口窗口。
右鍵 IPcSQL(具體根據設置名不同而不同) -> New ->建立兩個方法 (Method):
GetDatabaseNames ,用以傳下別名數據,其接口信息為:
Return type :HRESULT
Parameters
name type modifier
Param1 VARIANT* [out,retval]
SetDatabaseName , 用以接受客戶機來的信息,其接口信息要求傳入
三個信息(注意:圖上的SetDatabaseName誤為SetDatabaseNames,實際請設 為SetDatabaseName,否則客戶調用名稱可能會出錯)。
Return type :HRESULT
Parameters
name type modifier
DBName BSTR [in] ----傳上來的別名
(用Add加入)
UserName BSTR [in]
Password BSTR [in]
刷新(Refresh Implementation)以後,就產生兩個函數
function TPcSQL.GetDatabasenames: OleVariant;
begin
end;
procedure TPcSQL.SetDatabasename(const DBname, Username,
Password: WideString);
begin
end;
function TPcSQL.GetDatabaseNames: OleVariant;
var
I: Integer;
DBNames: TStrings;
begin
// 建立一個字符串數組存放BDE所有的數據庫別名數據。
DBNames := TStringList.Create;
try
// 利用Session控件取得當前BDE所有的數據庫別名數據。
Session1.GetDatabaseNames(DBNames);
/// 建立一個變量數組給函數返回變量Result。
Result := VarArrayCreate([0, DBNames.Count - 1], varOleStr);
// 最後再把數據庫別名數據指定給該變量數組。
for I := 0 to DBNames.Count - 1 do
Result[I] := DBNames[I];
Finally
//如果錯誤就把構造的DBNames釋放掉。
DBNames.Free;
end;
end;
上面的函數的關鍵除了取得BDE 數據庫別名以外,還聲明了一個變量數組來 存放數據庫別名數據,所以用VarArrayCreate函數建立一個變量數組,其中參數 1: 指定數組范圍;參數2:數組數據類型,由於別名要通過DCOM傳給前台,數據 類型必須設成varOleStr 才有效。 同時還要說明VarArrayCreate函數是在 Variants單元中建立的,所以,在implementation下還需要聲明:
uses Variants;
否則會編譯出錯,,必要時還要加上Unit1。
第二個過程是客戶機提供上來的聯機數據。
procedure TPcSQL.SetDatabaseName(const DBName, UserName,
Password: WideString);
begin
try
// 把前台傳來的數據庫別名、用戶上線名稱、用戶上線密碼
// 等三項數據指定給TDatabase控件,並且執行聯機的操作。
Database1.Close;
Database1.AliasName := DBName;
if (UserName<>'') and (Password<>'') then begin
Database1.Params.Values['PASSWORD'] := Password;
Database1.Params.Values['USER NAME'] := UserName;
end;
Database1.Open;
except
// 如果聯機時發生錯誤,則產生一個exception給前台程序
// 前台程序將會利用到這個exception來判斷是否要把輸入上
// 線數據的窗口激活。
on E: EDBEngineError do
raise Exception.Create('Password Required') ;
end;
end;
服務器端進行客戶計數
除了上面的功能外,這個程序還加上了一個在線用戶以及查詢用戶統計的功 能。由於這個應用程序執行模式是 Multiple Instance執行模式,所以當某個前 台第一次連上線後,應用程序服務器會激活RemoteDataModule的事件程序,而斷 線後又會執行OnDestroy事件程序,因此就可以用這兩個事件計算連上服務器的 用戶個數。至於Query個數的計算,則由TQuery的OnAfterOpen事件函數判斷。
Form1部分,主要用於顯示
首先在form上安放五個Tlabel控件,其中四個放在一個容器Panel1裡兩個計 數的label分別取名為ClientCount和QueryCount。屬性Capyion=0。注意,下面 的程序在Form1所在的單元Unit1中編寫。
在private下聲明兩個變量:
FQueryCount: Integer;
FClientCount: Integer;
在public下聲明兩個過程
procedure UpdateClientCount(Incr: Integer);
procedure IncQueryCount;
在實現區寫入過程:
procedure Tform1.UpdateClientCount(Incr: Integer);
begin
FClientCount := FClientCount + Incr;
ClientCount.Caption := IntToStr(FClientCount);
end;
procedure Tform1.IncQueryCount;
begin
Inc(FQueryCount);
QueryCount.Caption := IntToStr(FQueryCount);
end;
COM部分
在PcSQL窗口的兩個事件OnCreate和OnDesticy,建立事件驅動程序:
procedure TPcSQL.RemoteDataModuleCreate(Sender: TObject);
begin
end;
procedure TPcSQL.RemoteDataModuleDestroy(Sender: TObject);
begin
end;
procedure TPcSQ.RemoteDataModuleCreate(Sender: TObject);
begin
// 增加一位前台上線者(調用Form1上的程序)
Form1.UpdateClientCount(1);
end;
procedure TPcSQ.RemoteDataModuleDestroy(Sender: TObject);
begin
// 減少一位前台上線者(調用Form1上的程序)
Form1.UpdateClientCount(-1);
end;
在Tqurey的事件AfterOpen建立事件驅動程序:
procedure TPcSQL.Query1AfterOpen(DataSet: TDataSet);
begin
end;
procedure TPcSQ.AdHocQueryAfterOpen(DataSet: TDataSet);
begin
// 新打開一個TQuery查詢(調用Form1上的程序)
Form1.IncQueryCount;
end;
保存,編譯,注冊
二、SQL 客戶端程序
1)建立一個普通的工程。
2)放置一個TDCOMConnrction控件(在Datasnap頁),屬性:
在本機注冊時,可直接設置以下屬性:
ServerName:應用程序服務器注冊名(pro1.pc121)
Connected=true 激活
這時你可以看到服務器端的COM 程序被激活了。
如果在網絡上調試,需要給出服務器名:
ComputerName:服務器名(自動給出網上鄰居)
注意:
ServerGUID的GUID值是自動給出的。
3)放置一個TClientDataSet控件(在Data Access頁)
屬性:
RemoteServer= DCOMConnrction1
ProviderName:=DataSetProvider1
Active=true (激活後將能正常連接)
4)放置TDataSource,屬性:
Dataset:指向ClientDataSet1
安放合適的數據庫綁定控件。放置Demo控件作為SQL 數據輸入用。
procedure TForm1.Button1Click(Sender: TObject);
begin
// 把Memo內用戶輸入的SQL命令,通過IProvide的
// DataRequest方法傳給應用程序服務器。
ClientDataSet1.Close;
ClientDataSet1.CommandText := Memo1.Lines.Text;
ClientDataSet1.Open;
end;
希望用戶直接調用服務器端的別名,可以加入一個ComboBox控件:
form1的Create 事件可以寫成:
procedure TForm1.FormCreate(Sender: TObject);
var
I: Integer;
DBNames: OleVariant;
begin
// 連上應用程序服務器
DCOMConnection1.Connected := True;
// 取得BDE所有設置的數據庫別名數據
DBNames := DCOMConnection1.AppServer.GetDatabaseNames;
// 假如有數據庫別名數據,則將其一個一個地加到ComboBox控件內
if VarIsArray(DBNames) then
for I := 0 to VarArrayHighBound(DBNames, 1) do
ComboBox1.Items.Add(DBNames[I]);
//把默認的數據庫顯示在第一個
ComboBox1.Text:='mymdb';
end;
運行以後可以看到服務器端的所有的別名:
為了用這個別名列表調用其它的數據庫,考慮到有的數據庫需要輸入密碼和 用戶名,需要加入一個新的Form。
File->New->other->new-form 這就加入了一個
Form2
加入相應的輸入密碼控件。
其中:CheckBox1的屬性 Name=Check ;而兩個Edit控件的Text屬性為空字符 串。
Form2的兩個按鈕事件可以寫成:
procedure TForm2.Button1Click(Sender: TObject);
begin
Check.Checked:=True;
close;
end;
procedure TForm2.Button2Click(Sender: TObject);
begin
Check.Checked:=False;
close;
end;
然後回到form1。
首先在private 區聲明一個函數InputDialog,用於啟動讓用戶輸入上線數據 的Form2。
function InputDialog(var UserName, Password:String):Boolean;
然後在實現區寫入該函數的程序:
// 啟動讓用戶輸入上線數據的Form
function TForm1.InputDialog(var UserName, Password:String):Boolean;
var ret:Boolean;
begin
ret:=False;
try
Form2:=TForm2.Create(Application);
Form2.ShowModal;
UserName:=Form2.Edit1.Text;
Password:=Form2.Edit2.Text;
if Form2.Check.Checked then ret:=True
else ret:=False;
finally
Form2.Free;
Result:=ret;
end;
end;
現在,可以給ComboBox1加入一個雙擊事件:
procedure TForm1.ComboBox1DblClick(Sender: TObject);
// 先給應用程序服務器傳一次空的UserName及Password
var
UserName, Password: string;
begin
// 當您在ComboBox中選取的數據不是空字符串時
if ComboBox1.Text <> '' then
begin
ClientDataSet1.Close;
try
// 如果後台數據庫是Paradox或dBASE可能就沒問題,可是如果是SQL base的 關系型數據庫則會收到exception。
//SetDatabaseNames是服務端的COM 方法
DCOMConnection1.AppServer.SetDatabaseName (ComboBox1.Text,'','');
except
// 由於後台數據庫是SQL base的關系型數據庫,所以您必須通過Form2輸入 UserName及Password,並且把這些數據通過應用程序服務器提供的 SetDatabaseName傳給後台數據庫驗證。
on E: Exception do
if E.Message = 'Password Required' then
begin
if InputDialog(UserName, Password) then
DCOMConnection1.AppServer.SetDatabaseName(ComboBox1.Text,
UserName, Password);
end else raise;
end;
end;
end;
以後就可以利用服務器端的別名列表選擇數據庫了。
雙擊該列表框實際上已經實現了聯接,但是,窗口除關閉掉數據庫顯示以外 ,並沒有其它的反應,這就是說,這個程序使用上還有若干不方便的地方,最重 要的就是當連結上數據庫以後,無法知道表的名字。這樣,也無法方便的構造查 詢數據集的SQL語言。
下面解決這個問題。
現在,在服務器上再做一個方法: GetTableNames ,准備向客戶端送表名。
注意,要用Add加入一個傳出參數。
刷新後產生一個新的方法:
function GetTableNames: OleVariant; safecall;
現在可以寫方法程序了:
function TPcSQL.GetTableNames: OleVariant;
var
I: Integer;
DBTables: TStrings;
begin
// 建立一個字符串數組存放所有表名。
DBTables := TStringList.Create;
try
// 利用Database控件取得當前所有表名數據。
Database1.GetTableNames(DBTables , False);
/// 建立一個變量數組給函數返回變量Result。
Result := VarArrayCreate([0, DBTables.Count - 1], varOleStr);
// 最後再把表名數據指定給該變量數組。
for I := 0 to DBTables.Count - 1 do
Result[I] := DBTables[I];
finally
DBTables.Free;
end;
end;
然後重新保存,注冊,運行一下。
再看客戶端:
現在給它再加一個ComboBox,以顯示當前打開的數據庫的表名。
; 在.FormCreate事件過程中,再聲明一個變量:
DBTables: OleVariant;
在.FormCreate程序的最後加上:
//顯示表名
ComboBox2.Clear;
DBTables := DCOMConnection1.AppServer.GetTableNames;
if VarIsArray(DBTables) then
for I := 0 to VarArrayHighBound(DBTables, 1) do
ComboBox2.Items.Add(DBtables[I]);
為了在改變數據庫後也能正確顯示相應的表名,ComboBox1的雙擊事件也要加 上相應的一段。
procedure TForm1.ComboBox1DblClick(Sender: TObject);
var
UserName, Password: string;
//下面兩的變量聲明是新加的
DBTables: OleVariant;
i:integer;
begin
// 當您在ComboBox中選取的數據不是空字符串時
if ComboBox1.Text <> '' then
begin
ClientDataSet1.Close;
try
// 先給應用程序服務器傳一次空的UserName及Password
// 如果後台數據庫是Paradox或dBASE可能就沒問題,
// 可是如果是SQL base的關系型數據庫則會收到exception。
//SetDatabaseNames是服務端的COM 方法
DCOMConnection1.AppServer.SetDatabaseName(ComboBox1.Text,'','');
except
// 由於後台數據庫是SQL base的關系型數據庫,所以您必須
// 通過Form2輸入UserName及Password,並且把這些數據
// 通過應用程序服務器提供的SetDatabaseName傳給後台數
// 據庫驗證。
on E: Exception do
if E.Message = 'Password Required' then
begin
if InputDialog(UserName, Password) then
DCOMConnection1.AppServer.SetDatabaseName(ComboBox1.Text,
UserName, Password);
end else raise;
end;
//顯示表名,這段程序是新加的
ComboBox2.Clear;
DBTables := DCOMConnection1.AppServer.GetTableNames;
if VarIsArray(DBTables) then
for I := 0 to VarArrayHighBound(DBTables, 1) do
ComboBox2.Items.Add(DBtables[I]);
ComboBox2.Text:=ComboBox2.items[0];
end;
end;
給ComboBpx2加一個雙擊事件,可以把表的名字加入SQL 語言:
procedure TForm1.ComboBox2DblClick(Sender: TObject);
begin
Memo1.text:=Memo1.text+combobox2.text;
end;
效果就完全不同了,你可以方便的選擇數據庫和表:
然後組合適當的SQL語言,最終打開一個合適的表。
進一步考慮,組合新的SQL語言的時候最好要有字段名的數據,這不需要從服 務器得到,因為在多層情況下,ClientDataSet實際上起著Ttable或者Tquery相 似的作用,對數據庫的控制上,有幾乎相同的語言,例如記錄指針的移動,字段 數據的取得和寫入等等,這樣一來,你也可以直接使用這些方法來操縱數據庫。
再一次提醒大家,ClientDataSet是個功能強大非常重要的控件,在Delphi的 很多高級場合,都要使用到它。
當然,利用ClientDataSet,被打開數據庫的字段名表也很容易得到。
首先再加一個Combbox控件Combbox3。
把SQL 查詢的Button事件作如下修改:
procedure TForm1.Button1Click(Sender: TObject);
//首先要定義兩個變量
var i,N:integer;
begin
ClientDataSet1.Close;
ClientDataSet1.CommandText := Memo1.Lines.Text;
ClientDataSet1.Open;
//這是後加入的顯示子段名的程序
ComboBox3.Clear;
N:=ClientDataSet1.FieldCount;
for I := 0 to N - 1 do
ComboBox3.Items.Add(ClientDataSet1.Fields[i].FieldName);
ComboBox3.Text:=ComboBox3.items[0];
end;
事實上,FormCreate事件程序也要加入相應的程序,使一打開程序就有子段 名表顯示。
procedure TForm1.FormCreate(Sender: TObject);
var
//增加兩個定義
I,N: Integer;
DBNames: OleVariant;
DBTables: OleVariant;
begin
// 連上應用程序服務器
DCOMConnection1.Connected := True;
// 取得BDE所有設置的數據庫別名數據
DBNames := DCOMConnection1.AppServer.GetDatabaseNames;
// 假如有數據庫別名數據,則將其一個一個地加到ComboBox控件內
if VarIsArray(DBNames) then
for I := 0 to VarArrayHighBound(DBNames, 1) do
ComboBox1.Items.Add(DBNames[I]);
//把默認的數據庫顯示在第一個
ComboBox1.Text:='abc';
//顯示表名
DBTables := DCOMConnection1.AppServer.GetTableNames;
if VarIsArray(DBTables) then
for I := 0 to VarArrayHighBound(DBTables, 1) do
ComboBox2.Items.Add(DBtables[I]);
ComboBox2.Text:=ComboBox2.items[0];
//顯示字段名表,增加的程序
ComboBox3.Clear;
N:=ClientDataSet1.FieldCount;
for I := 0 to N - 1 do
ComboBox3.Items.Add(ClientDataSet1.Fields[i].FieldName);
ComboBox3.Text:=ComboBox3.items[0];
end;
需要時,也可以寫入ComboBox3的雙擊事件:
好了,現在可以看看效果:
這些數據取得以後,你就可以把這個客戶程序做得更加方便通用,那就要看 你的想象力和程序設計的水平了,這裡的例子,主要是想提供客戶端使用DCOM提 供的方法的思想和規則,通過這樣的研究,您是不是對DCOM 和分布式數據庫的 問題有了更深的理解了呢?
第七節一對多表的服務器端程序
一、Application Server
事先要有一個標准的
放入 ADOTable1
ADOTable2
Datasouce1
DataSetProvider1(在
Data Access)
DataSetProvider2
屬性:
ADOTable1(主表)
ADOTable2(從表)
用ConnectionString和TableName連上數據庫
為了構建主從表,注意下面的圖:
DataSouce1屬性:DataSet=ADOTab1
ADOTable2屬性:MasterSource=DataSource1
MasterField=連結字段
TdataSetProvider1屬性:Dataset=ADOTable1
TdataSetProvider2屬性:Dataset=ADOTable2
這就完成了主從表的服務器端構建。
保存,注冊,運行一下。
二、客戶端編程
首先建立一個普通的工程:
加入一個TDCOMConnection,兩個TClientDataSet,和兩個TdataSource。
DCOMConnection1
屬性:ComputerName=計算機名(本地可以不寫)
ServerName=服務器端程序名(COM)
Connecter=true (表示連上)
ClientDataSet1
屬性:RemoteServer=DCOMConnection1
ProviderName=DataSetProvider1
屬性:RemoteServer=DCOMConnection2
Active-=true
ClientDataSet2
ProviderName=DataSetProvider2
Active-=true
兩個TDataSource分別連上
ClientDataSet1和
ClientDataSet2
加入兩個TDBGrid,分別連上相應的DataSource。
主從表似乎應該建立起來了,但運行一下就可以發現,主從關系並沒有建立 起來。
解決辦法:
雙擊ClientDataSet1,出現一個Form1.ClientDataSet1窗口:
在窗口空白處右鍵,快捷菜單上選Add Field 這時,我們可以看到最後一行 出現:ADOTable2 。這是很重要的,正是靠這個建立了主從聯接關系
OK 讓它進入Form1.ClientDataSet1窗口(也可以調整主表所顯示的字段,但 ADOTable2 不能少,它實際上是把從表作為主表的一個字段使用了)。
ClientDataSet2屬性:
DataSetField=ClientDataSet1ADOTable2
Active=true
好啦,主從關系建立起來了。
通過這個研究可以發現,實際上在服務端程序中,DataSetProvider2是多余 的,以後在設置的時候,可以不要。只需要一個DataSetProvider1就可以和客戶 端聯系了。
Form
file -> New -> Multitier -> Remote Data Module
CoClass Name : Master2
Instacting: Multiple Instacting
Threading Model: Apartment
procedure TForm1.ComboBox3DblClick(Sender: TObject);
begin
Memo1.text:=Memo1.text+combobox3.text;
end;