緣起
古典的Delphi程式人員,透過Delphi的Midas技術,提供了許許多多令人讚賞的服務跟應用程式,從Delphi 5的那年代就已經是非常厲害的多層式架構了。但從Delphi 5的1998年開始到Delphi XE 10.3的2019年,仍然有許多程式人員停留在Delphi 5到Delphi 7的Midas,在這21年當中,Delphi的資料庫元件從1990年代的BDE進化到2000年代的ADOExpress,再進化到DBExpress、FireDAC,資料庫的存取效能已非BDE的年代可比。
而在1994年才開始嶄露頭角的WWW,也已經在2000-2010年之間,經過了兩三次的蛻變,從原本單純的Server端靜態網頁轉變成電子商務主流,再轉變成Web 2.0由使用者提供內容為主的多元內容來源,再轉變成社群網路的世代。
更在2008年開始,由Apple跟Google把行動裝置帶到了所有人的身邊,也把網路跟人們生活的所有層面緊密的結合了起來。
但Delphi 5的Midas系統,很多仍然停留在1990年代那當口,並不是Delphi的架構跟技術沒有進步,只是開發人員在各自的領域裡忙著,沒能騰出時間來進化原來的系統。也是Delphi在時代的更迭中,不斷摸索,到建立現在這條路徑的過程中,沒能提供足夠的資源讓開發人員儘快跟上來所致。
在Delphi的開發路途上,我算是比較另類的,少碰資料庫跟報表,多碰多媒體、網路通訊、軟硬體整合的應用。後來有各種不同的應用專案,就多多少少都接一些,正好這次有機會碰上DataSnap的專案,要把原本在2015年製作的二進位DataSnap專案,升級成RESTful相容的Server,並能穿透防火牆對HTTP協定的管理。
因緣際會,正好趁這個機會,把DataSnap Server跟DataSnap over WebServer做些實驗,接下來,我們用三篇文章來實作DataSnap Client Server的三種排列組合,讓大家有機會能夠看著文章跟著做,做完一篇文章的實例,隨即掌握該篇文章當中對DataSnap的重點,三篇做完,把原本在古典Delphi製作的Midas系統,轉生成為DataSnap的系統,進而讓Delphi的優良技術延續下去,也讓各種可能的系統能夠在更有效率的技術上製作出來,不讓使用者在不適當的技術上面受折磨。
用FireDAC搭配DataSnap二進位格式製作Client Server程式
我已經不記得FireDAC是從哪個版本的Delphi開始成為內建的資料庫元件,雖現在FireDAC跟DBExpress都是內建的資料庫元件,但我還是以FireDAC為主。這第一篇文章,就先以FireDAC為基礎,製作以二進位資料傳輸的DataSnap Server與Client程式。
建立空殼DataSnap Server專案
首先我們建立一個DataSnap專案,以下用圖片來展示:
因為在我的電腦中,Port 80已經被IIS使用了,所以這裡的HTTP Port,我也用預設的8080,如果您不確定自己的電腦中哪些Port可以使用,建議先點選Test Port按鈕,確定一下那個Port可不可以使用。
Server Method是我們要實作各種DataSnap開放給Client使用的API所宣告的地方,因為我們要在這裡連接到SQL Server,所以我選擇TDSServerModule。但選擇TDataModule或者TDSServerModule差別不大,都是要在裡面放置與資料庫Server連線用的TFDConnection等連線元件,所以我直接就選了TDSServerModule:
到這個步驟,點選Finish以後,專案就會建立完成,在Delphi環境裡面,會有三個檔案,分別是專案主畫面、ServerMethodsUnit1.pas跟ServerContainerUnit1.pas,我把專案儲存為TestDSServer,所以右上角的專案內容,看起來就會變成下圖所示這樣:
ServerContainerUnit1.pas當中,Design畫面則是自動被放了一些元件上去:
主畫面Unit2.pas也是空空的畫面:
建立DataSnap Client程式
首先建立一個空的VCL Application專案,我把它命名為TestDSClient,接著在Client主畫面上放置一個TSQLConnection元件,把它的Driver屬性設定為DataSnap,Params設定就依照預設的設定即可:
接著,先把剛剛製作好的TestDSServer從檔案總管裡面執行起來,再到畫面上用滑鼠右鍵點擊SQLConnection1元件,點選其中的Generate DataSnap client class項目:
點選這個項目後,就會建立出一個新的單元檔,當中會宣告與Server連線的新Class,這是開發環境直接跟已經執行起來的Server程式索取資料,即時產生的Class,我們就可以用這個Class直接呼叫Server端的Method了。
在Client端的主畫面上,我放了一個TEdit元件,讓使用者輸入文字,再加一個按鈕(TButton),點擊按鈕的時候,就呼叫Server端的ReverseString方法,把輸入的文字反轉,程式碼如下:
procedure TForm3.btnReverseClick(Sender: TObject);varserver : TServerMethods1Client;retStr : String;beginserver := TServerMethods1Client.Create(SQLConnection1.DBXConnection);tryretStr := server.ReverseString(self.Edit1.Text);ShowMessage('Reture String: ' + retStr);finallyServer.Free;end;end;
我們直接在點選按鈕的Event Handler裡面新增上面的程式碼,先建立server元件(透過SQLConnection1.DBXConnection),接著就可以用這個server元件來呼叫server端的方法了,此時,使用的連線是透過211 Port以二進位進行溝通的。
接著,我們在Server端加入資料庫連線,提供兩個方法作為範例,一個是讓Client端傳SQL指令進行查詢,然後Server端把查詢到的資料筆數轉為字串回傳。另一個則是讓Client端傳select SQL指令,讓Server把查詢到的資料以Table的形式回傳到Client端,讓Client端可以透過MemoryTable讀取資料內容。
擴增Server端功能- getRecordCount
我先用MySQL做個簡單的DemoDB,作為範例之用,我把MySQL資料庫放在10.10.10.1的IP上頭,設定好一個名為DemoDB的資料庫,並且設定好一個使用者帳號,名為DemoDB,讓它可以接受10.10.10.1連線,並且在上頭新增了四筆資料:
接著,把FDQuery1的Connection屬性設定為FDConnection1,這樣一來,FDQuery1的SQL指令就可以透過FDConnection1來執行,送到MySQL伺服器去,而查詢的結果,也會經由FDConnection1回傳到FDQuery1裡面來。
設定好了,我們就可以新增getRecordCount這個新方法了,這步驟就純粹是程式碼編輯囉,先在TServerMethods1類別裡面新增這行宣告:
function getRecordCount(SQLCmd: String): string;
然後按下Ctrl+Shift+C,讓Delphi開發環境自動幫我們建立對應的程式碼,也就是一個空殼的方法實作:
function TServerMethods1.getRecordCount(SQLCmd: String): string;
begin
end;
做好空殼方法之後,我們先重新編譯TestDSServer,並且從檔案總管執行它,再重複前面提到過的『再到畫面上用滑鼠右鍵點擊SQLConnection1元件,點選其中的Generate DataSnap client class項目』,執行完成後,剛剛Server新增的這個函式就會被新增到Client端的Unit1.Pas裡面了:
function TServerMethods1Client.getRecordCount(SQLCmd: string): string;beginif FgetRecordCountCommand = nil thenbeginFgetRecordCountCommand := FDBXConnection.CreateCommand;FgetRecordCountCommand.CommandType :=
TDBXCommandTypes.DSServerMethod;FgetRecordCountCommand.Text := 'TServerMethods1.getRecordCount';FgetRecordCountCommand.Prepare;end;FgetRecordCountCommand.Parameters[0].Value.SetWideString(SQLCmd);FgetRecordCountCommand.ExecuteUpdate;Result := FgetRecordCountCommand.Parameters[1].Value.GetWideString;end;
這些自動產生的程式碼我們不用處理,接著回頭關掉Server程式、來實作程式碼吧。
在Server端,我們的getRecordCount有個參數,就是要執行的SQL指令,所以完整的實作如下:
function TServerMethods1.getRecordCount(SQLCmd: String): string;beginself.FDQuery1.Active := False;tryself.FDQuery1.SQL.Text := SQLCmd;self.FDQuery1.Active := True;Result := IntToStr(self.FDQuery1.RecordCount);finallyself.FDQuery1.Active := False;end;end;
直接執行傳來的SQL指令,執行後,把FDQuery1.RecordCount轉為字串回傳,執行的畫面如下:
procedure TForm3.btnGetRecordCountClick(Sender: TObject);varserver : TServerMethods1Client;retStr : String;beginif not self.SQLConnection1.Connected thenself.SQLConnection1.Connected := True;server := TServerMethods1Client.Create(SQLConnection1.DBXConnection);tryretStr := server.getRecordCount(self.Ed_SQLCmd.Text);ShowMessage('Reture count: ' + retStr);finallyServer.Free;end;end;
所以執行的畫面,就如上面第二張的截圖,出現了Return Count: 4, 因為我只在資料庫裡面建立了四筆資料,所以select * from testRec 查詢執行後,取得了四筆資料,回傳4這個字串。
擴增Server端功能- StreamGet
接著,我們要把查詢到的資料回傳給Client,由於回傳的資料我們要用Serialization(序列化)處理後,直接回傳到Client,所以就直接回傳TStream,Client端接到了資料,再把Serialization資料轉回為放在記憶體裡面的TTable(為了處理這樣的需求,FireDAC元件當中新增了TFDMemTable這個元件用來處理這樣的資料)。
首先仍然要先做出Server端的空殼方法:
function StreamGetSQL(SqlCmd: String): TStream;
然後重新編譯Server程式、從檔案總管執行它、再重複Client端的步驟『再到畫面上用滑鼠右鍵點擊SQLConnection1元件,點選其中的Generate DataSnap client class項目』,執行完成後,剛剛Server新增的這個函式就會被新增到Client端的Unit1.Pas裡面了:
function TServerMethods1Client.StreamGetSQL(SqlCmd: string): TStream;beginif FStreamGetSQLCommand = nil thenbeginFStreamGetSQLCommand := FDBXConnection.CreateCommand;FStreamGetSQLCommand.CommandType := TDBXCommandTypes.DSServerMethod;FStreamGetSQLCommand.Text := 'TServerMethods1.StreamGetSQL';FStreamGetSQLCommand.Prepare;end;FStreamGetSQLCommand.Parameters[0].Value.SetWideString(SqlCmd);FStreamGetSQLCommand.ExecuteUpdate;Result := FStreamGetSQLCommand.Parameters[1].Value.GetStream(FInstanceOwner);end;
Client端需要新增的元件,從前面只建了一個ReversString跟只要接RecordCount的需求,到現在需要接回Server傳回的資料,Client端需要把Stream的資料轉回物件、也得把Table結構、等待SQL連線執行等等元件都新增上來,下面第一張圖是還沒新增這些元件之前的樣子:
接下來,我把TFDSchemaAdapter, TFDMemTable, TFDGUIxWaitCursor, TFDStanStorageBinLink, TFDTableAdapter 這些元件都加入到表單畫面中,就成了下面這個截圖的樣子:
這些元件個有其特殊的用途,我們來看一下:
FDSchemaAdapter1: TFDSchemaAdapter; => 把Stream資料轉為物件
FDMemTable1: TFDMemTable; => 儲存轉換完成的Table資料
FDGUIxWaitCursor1: TFDGUIxWaitCursor; =>等待Server 端連線完成的等待作業
FDStanStorageBinLink1: TFDStanStorageBinLink; => FDSchemaAdapter轉換二進位資料時,用來處理資料格式的物件。
FDTableAdapter1: TFDTableAdapter; => 連結FDSchemaAdapter1 跟FDMemTable的中介角色,它裡面的DatSTableName必須設定為回傳的TableName,回傳的Table才能正確的被儲存到FDMemTable1裡面去。
這些基本設定完成後,就可以把Client端按鈕的Event Handler寫上去了:
procedure TForm3.btnGetTableClick(Sender: TObject);varServer: TObject;LMemStream: TMemoryStream;beginserver := TServerMethods1Client.Create(SQLConnection1.DBXConnection);LMemStream := CopyStream(TServerMethods1Client(Server).StreamGetSQL('select * from testRec'));tryif LMemStream <> nil then beginLMemStream.Position := 0;self.FDSchemaAdapter1.LoadFromStream(LMemStream,TFDStorageFormat.sfBinary);self.FDMemTable1.First;ShowMessage(self.FDMemTable1.FieldByName('name').AsString);end;finallyLMemStream.Free;Server.Free;end;end;
上面這段程式,是我抓了第一筆資料裡面的name欄位來顯示的,想不起來資料長怎樣嗎?我們回頭看一下MySQL裡面建好的資料:
取回的FDMemTable1資料是完整的,所以呼叫Next,就可以依序取得其他各筆的資料。要記得:FDTableAdapter1.DatSTableName得要回傳跟SQL指令裡面的Table名字一致,才不會在執行過程中發生錯誤喔。
總結本篇
DataSnap Client Server是還不錯的架構,在過去一二十年裡面也廣泛的被用在各種行業的系統中,雖然因為Web架構已經成為市場主流,Embarcadero也因此從善如流,提供了DataSnap over REST (可以選擇IIS或者Apache, 或者用Delphi自行編譯exe提供HTTP Server),也提供了EMS Server (從Delphi XE 10.2開始,改名為RAD Server),讓我們開發人員可以使用更輕量化的資料傳輸架構,但怎麼選擇還是程式人員的習慣、開發時程能最快Time To Market等考量為主。
透過本篇的介紹,如果您以前就會Midas,看完以後您一定能立刻做出DataSnap Server/Client程式,因為我在寫這篇文章之前,Midas我沒寫過,DataSnap Server我也只寫過一次,要我直接寫PHP的RESTful server端還比較快,連我都可以這麼快摸清楚,您以前就會Midas了,一定可以看著我摸索的過程、我發現的一些眉角,立刻做出簡單的DataSnap程式的。
當然,一如過去的幾篇文章,我也把範例程式碼完整的放在這裡,您可以下載回去看看我實作的簡單範例。
下一篇,您可能會以為是DataSnap直接做RESTful server,答案是YES and NO,是的,我們會介紹DataSnap透過RESTful 資料進行傳輸,不過,我會多跳一步,我們直接看『DataSnap WebBroker Application』,用Delphi WebBroker應用程式,先用VCL應用程式,再改為ISAPI格式,這樣一來,各種排列組合的效能,就可以攤開來做比較了,您覺得呢?