在實際的MIS系統中,遠程數據庫訪問大多通過Modem連接,出於通信費用及速度方面的考慮,往往采用先將數據保存在本地,然後集中傳送到遠端的辦法。遠程數據傳送可以有多種方案,最常見的是先將要傳送的數據打包成文件,在利用文件傳輸形式傳送到目的地,在目的地對數據恢復後添加到本地數據庫中。這種方法普遍地應用於證券交易系統,其優點是速度快,並且可事先對數據壓縮,更大限度地節約傳送時間及費用。但這種方案也有其不足之處:由於利用文件傳輸機制,無法利用數據庫本身的特性如完整性約束、數據一致性、回滾機制等,因此在比較復雜的數據庫系統中較少采用。另一種方法是直接將兩端處理成"客戶/服務器"模式,將數據傳送看成是向Server提交數據。由於這種方案充分利用了數據庫服務器的特性,並且實際操作基本與局域網方式一致,因此本文將詳細介紹這種方案。另外本文的部分內容是基於Delphi/CBuilder的。
由於傳輸速度的原因,當傳送大量數據時絕對不贊成逐條記錄地向服務器提交數據,而應批量地向Server提交,Delphi/CBuilder中提供了一個TBatchMove控件專門用於批量傳送數據,利用它可極大減少網絡負擔,提高傳送速度。遺憾的是,TBatchMove控件只提供了簡單的錯誤控制功能,沒有提供顯示傳送進度、用戶終止傳送等重要功能。然而TBatchMove所依賴的BDE卻提供了一種"回調機制"可以完成上述兩個功能。所謂"回調"過程是這樣的:當BDE執行某種操作時,比如從一張表向另一張表拷貝大量數據的過程中,每過一段時間(如需要顯示拷貝進度時),BDE會調用一段你自己寫的函數(回調函數),以幫助你更完全地控制程序。這種做法有點想DLPHI中的Event(事件)及事件處理函數--某個具體的操作動作會讓VCL觸發某個事件,從而調用一段你寫好的事件處理函數,不同的事件會觸發不同的處理函數。
為了讓BDE能正確地與你的函數協同工作,你必須事先"注冊"你的函數,讓BDE知道某個事件發生時應調用(回調)你的某段代碼。BDE提供了一個DbiRegisterCallBack注冊函數,不幸的是,BDE的聯機幫助中的說明不能適合於Delphi/CBuilder,按照該說明編寫的程序根本不能通過編譯!筆者通過實踐找到了正確使用BDE回調函數的方法,下面將詳細介紹該機制的使用。BDE回調機制包含以下幾個步驟:
1)按BDE的預定格式編寫你的回調函數
2)調用DbiRegisterCallBack函數注冊你的回調函數,這樣當你執行相關數據庫操作時就自然地觸發你的回調函數。
3)執行相關數據庫操作,比如BatchMove1->Exectue();
4)注銷該回調函數
其中最關鍵的是正確注冊你的回調函數,因此先介紹第二步。(注冊與注銷都調用同一函數,只是最後一個參數略有不同)
首先你應知道在哪類"事件"發生時調用你的回調函數,其次你應明白與該事件相關的參數及數據結構--這一切都發生在調用DbiRegisterCallBack函數注冊時,所以下面先介紹DbiRegisterCallBack的正確用法及說明:
在原BDE幫助中該函數的原形(C)是這樣的
DBIResult DBIFN DbiRegisterCallBack (hCursor, ecbType, iClientData, iCbBufLen, pCbBuf, pfCb);
要使用該函數必須include頭文件,問題是Delphi/CBuilder中根本沒有提供該文件,取而代之的是"BDE.HPP",但是在包含進該文件後程序仍然不能編譯通過,因為該文件中沒有DBIFN等的說明。一個簡單的方法是在代碼中去掉DBIFN。函數中各參數解釋如下:hCursor是一個BDE中對象的句柄,如果這個參數為NULL,則表示注冊的回調函數適合於所有BDE任務;第二個參數ecbType是指回調函數的觸發條件的類別,有很多種類型可以選擇,其中cbGENPROGRESS表示當需要顯示一個長操作的進度時觸發這個回調函數;第三個參數iClientData是傳遞給回調函數的某個數據結構的指針,在我們的例子中為NULL;第四個參數iCbBufLen是指回調Buffer的大小,該大小隨第二個參數的不同而不同,比如sizeof(CBPROGRESSDesc);第五個參數pCbBuf是回調Buffer的指針,該指針類型隨第二個參數變化,比如cbGENPROGRESS的數據結構是CBPROGRESSDesc;最後一個參數是回調函數的地址指針,當該參數為NULL時表示注銷該類型的回調函數。關於回調函數將在稍後詳細介紹。下面是注冊執行長操作時顯示進度的回調函數的格式:
int rst= DbiRegisterCallBack (NULL,
//適合於任何進程
cbGENPROGRESS, //回調類型:顯示長操作的進度
NULL, //沒有數據
sizeof(CBPROGRESSDesc), //數據結構的大小
&aCBBuf, //數據的內存地址
ApiCallBackFun //回調函數的地址
);
接下來就應該完成第一步:編寫回調函數
在C中,回調函數應如下聲明:
CBRType__stdcallApiCallBackFun(
CBTyp eecbType,//回調類型
int iClientData,//回調數據(指針)
void *pCbInfo//回調數據結構指針
)
第一個參數是回調類型;第二個參數是回調數據,其解釋同DbiRegisterCallBack的第三個參數;第三個是回調數據的指針,該數據的結構隨回調類型的不同而不同。比如進度指示cbGENPROGRESS的數據結構是CBPROGRESSDesc,其定義如下:
struct CBPROGRESSDesc {
short iPercentDone; //進度的百分比
char szMsg[128]; //進度的文本信息
};
該結構的兩個域同時只有一個起作用,第一個表示操作的進度百分比,當其為-1時表示第二個域起作用。第二個域用字符串表示進度信息,其格式為<String><:><Value>,比如:RecordsCopied:125
本文主要在回調函數中完成兩個工作:
1)顯示數據拷貝(BatchMove)進度
2)提供讓用戶終止長時間拷貝的機制
顯示拷貝進度的代碼如下:
CBRType __stdcall ApiCallBackFun(
CBType ecbType, // Callback type
int iClientData, // Client callback data
void * pCbInfo // Call back info/Client)
{ AnsiString str;
if(ecbType==cbGENPROGRESS)
{
int j= StrToInt( ((CBPROGRESSDesc*)
pCbInfo)- >iPercentDone);
if(j< 0)
//如果iPercentDone為-1,則分析szMsg的信息
{
str=((CBPROGRESSDesc*)pCbInfo)- >szMsg;
int pos=str.AnsiPos(":")+1;
//提取出拷貝的記錄數
//下面的代碼用來在一個Form中顯示拷貝進度及拷貝數量
Form1- >Label2- >Caption= str.SubString(pos,100);
Form1- >Label2- >Update();
Form1- >ProgressBar1- >Position=
int((str.SubString(pos,100).
ToDouble()/Form1- >TransNum)*100);
Form1- >ProgressBar1- >Update();
}
else
{Form1- >ProgressBar1- >Position=j;
Form1- >ProgressBar1- >Update();
}
return cbrCONTINUE;
//必須返回cbrCONTINUE以便讓BatchMove繼續
//若返回cbrABORT則終止拷貝
}
一切完成以後,每當調用長時間BDE操作(比如BatchMove1->Exectue())時都會觸發該回調函數,注意在不需要時應"注銷"這個回調函數。
如果批量傳送數據時間很長,則必須為用戶提供終止該操作的機會,前面提到,若回調函數返回cbrABORT,則BatchMove過程立即終止。可以在Form上加上一個"停止"按鈕和一個全局布爾變量isContinue,當開始拷貝時設該變量為true,當按鈕按下後,設該變量為false,每次調用回調函數時檢查isContinue的值,若為true則回調函數返回cbrCONTINUE讓拷貝繼續,否則返回cbrABORT終止拷貝。但是問題在於一旦拷貝過程開始,該進程內所有消息將被阻塞,應用程序在拷貝結束之前沒有機會響應鍵盤、鼠標等一切消息,連屏幕刷新都不能完成,因此必須找到一種避免消息阻塞的方法。
大家知道,Windows是靠事件(消息)驅動的,在WIN32系統中有兩種消息隊列:系統隊列和應用程序隊列,當一個程序進行一個長時間操作時,系統分配給該程序的時間片將完全用於處理該操作,換句話說,應用程序沒有從它的應用程序隊列中取出消息並處理的機會,這樣該程序將停止一切對外部事件的響應直到該操作完成為止。具體到本文中就是程序必須等到BatchMove1->Execute()執行完畢後才能響應用戶操作,因此用戶將完全沒有機會終止拷貝過程。
解決的辦法是:在回調函數中取出消息隊列中的消息,並後台處理它們,這樣用戶將有機會按下終止按鈕。實現的代碼很簡單,在回調函數中最後加入以下代碼即可
CBRType __stdcall ApiCallBackFun(…)
{
……
MSG amsg;
while(PeekMessage(&amsg,NULL,0,0,PM_REMOVE))
//從隊列中取消息
{
TranslateMessage(&amsg); //翻譯消息
DispatchMessage(&amsg); //分發消息
}
if (isContinue)
return cbrCONTINUE;
else
return cbrABORT;
}
以上的代碼雖然都用CBuilder編寫,但是其原理同樣適用於DELPHI。