2019年8月31日 星期六

建置DataSnap Client Server (3之1)

緣起

古典的Delphi程式人員,透過DelphiMidas技術,提供了許許多多令人讚賞的服務跟應用程式,從Delphi 5的那年代就已經是非常厲害的多層式架構了。但從Delphi 51998年開始到Delphi XE 10.32019年,仍然有許多程式人員停留在Delphi 5Delphi 7Midas,在這21年當中,Delphi的資料庫元件從1990年代的BDE進化到2000年代的ADOExpress,再進化到DBExpressFireDAC,資料庫的存取效能已非BDE的年代可比。
而在1994年才開始嶄露頭角的WWW,也已經在2000-2010年之間,經過了兩三次的蛻變,從原本單純的Server端靜態網頁轉變成電子商務主流,再轉變成Web 2.0由使用者提供內容為主的多元內容來源,再轉變成社群網路的世代。
更在2008年開始,由AppleGoogle把行動裝置帶到了所有人的身邊,也把網路跟人們生活的所有層面緊密的結合了起來。
Delphi 5Midas系統,很多仍然停留在1990年代那當口,並不是Delphi的架構跟技術沒有進步,只是開發人員在各自的領域裡忙著,沒能騰出時間來進化原來的系統。也是Delphi在時代的更迭中,不斷摸索,到建立現在這條路徑的過程中,沒能提供足夠的資源讓開發人員儘快跟上來所致。
Delphi的開發路途上,我算是比較另類的,少碰資料庫跟報表,多碰多媒體、網路通訊、軟硬體整合的應用。後來有各種不同的應用專案,就多多少少都接一些,正好這次有機會碰上DataSnap的專案,要把原本在2015年製作的二進位DataSnap專案,升級成RESTful相容的Server,並能穿透防火牆對HTTP協定的管理。
因緣際會,正好趁這個機會,把DataSnap ServerDataSnap over WebServer做些實驗,接下來,我們用三篇文章來實作DataSnap Client Server的三種排列組合,讓大家有機會能夠看著文章跟著做,做完一篇文章的實例,隨即掌握該篇文章當中對DataSnap的重點,三篇做完,把原本在古典Delphi製作的Midas系統,轉生成為DataSnap的系統,進而讓Delphi的優良技術延續下去,也讓各種可能的系統能夠在更有效率的技術上製作出來,不讓使用者在不適當的技術上面受折磨。

FireDAC搭配DataSnap二進位格式製作Client Server程式

我已經不記得FireDAC是從哪個版本的Delphi開始成為內建的資料庫元件,雖現在FireDACDBExpress都是內建的資料庫元件,但我還是以FireDAC為主。這第一篇文章,就先以FireDAC為基礎,製作以二進位資料傳輸的DataSnap ServerClient程式。

建立空殼DataSnap Server專案

首先我們建立一個DataSnap專案,以下用圖片來展示:























第四步驟當中,預設並沒有勾選HTTP,如果只要建置用二進位傳輸資料的DataSnapHTTP選項不用勾選,但這個範例專案我要用兩次,所以先勾了:
















因為在我的電腦中,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.pasServerContainerUnit1.pas,我把專案儲存為TestDSServer,所以右上角的專案內容,看起來就會變成下圖所示這樣:

























這個時候如果點選ServerMethodUni1.pas,切到Design畫面,會看見視窗上什麼元件也沒有:

ServerContainerUnit1.pas當中,Design畫面則是自動被放了一些元件上去:

主畫面Unit2.pas也是空空的畫面:

在這個時候,Server雖然主畫面是空的,但執行起來,可以看到Port 211Port 8080是被佔用的狀態:


接著,我們就先來製作Client端,再進行功能擴增的作業吧。

建立DataSnap Client程式

首先建立一個空的VCL Application專案,我把它命名為TestDSClient,接著在Client主畫面上放置一個TSQLConnection元件,把它的Driver屬性設定為DataSnapParams設定就依照預設的設定即可:














再放一個TDSProviderConnection元件,把它的SQLConnection屬性設定為剛剛放好的這個SQLConnection1 
接著,先把剛剛製作好的TestDSServer從檔案總管裡面執行起來,再到畫面上用滑鼠右鍵點擊SQLConnection1元件,點選其中的Generate DataSnap client class項目:














點選這個項目後,就會建立出一個新的單元檔,當中會宣告與Server連線的新Class,這是開發環境直接跟已經執行起來的Server程式索取資料,即時產生的Class,我們就可以用這個Class直接呼叫Server端的Method了。
Client端的主畫面上,我放了一個TEdit元件,讓使用者輸入文字,再加一個按鈕(TButton),點擊按鈕的時候,就呼叫Server端的ReverseString方法,把輸入的文字反轉,程式碼如下:
procedure TForm3.btnReverseClick(Sender: TObject);
var
   server : TServerMethods1Client;
   retStr : String;
begin
   server := TServerMethods1Client.Create(SQLConnection1.DBXConnection);
   try
      retStr := server.ReverseString(self.Edit1.Text);
      ShowMessage('Reture String: ' + retStr);
   finally
      Server.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.1IP上頭,設定好一個名為DemoDB的資料庫,並且設定好一個使用者帳號,名為DemoDB,讓它可以接受10.10.10.1連線,並且在上頭新增了四筆資料:
接著,在Server端的ServerMethodUnit1裡頭,我加了TFDConnection元件,把它的驅動程式設定為MySQL,另在DataModule裡面加入TFDPhysMySQLDriverLink, TFDSchemaAdapter, TFDQuery, TFDStanStorageBinLink這幾個元件,加入以後,畫面如下圖所示:

接著開始設定。首先,把FDConnection設定好,如下畫面中所示:

接著,把FDQuery1Connection屬性設定為FDConnection1,這樣一來,FDQuery1SQL指令就可以透過FDConnection1來執行,送到MySQL伺服器去,而查詢的結果,也會經由FDConnection1回傳到FDQuery1裡面來。
FDQuery1SchemaAdapter屬性,則用下拉選單設定為FDSchemaAdapter1,這是為了等下要轉換成其他格式回傳給Client之用。
設定好了,我們就可以新增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;
begin
  if FgetRecordCountCommand = nil then
  begin
    FgetRecordCountCommand := 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;
begin
   self.FDQuery1.Active := False;
   try
      self.FDQuery1.SQL.Text := SQLCmd;
      self.FDQuery1.Active := True;
      Result := IntToStr(self.FDQuery1.RecordCount);
   finally
      self.FDQuery1.Active := False;
   end;
end;

直接執行傳來的SQL指令,執行後,把FDQuery1.RecordCount轉為字串回傳,執行的畫面如下:
















Client端的實作,則是放在按鈕btnGetRecordCount Event Handler裡面:

procedure TForm3.btnGetRecordCountClick(Sender: TObject);
var
   server : TServerMethods1Client;
   retStr : String;
begin
   if not self.SQLConnection1.Connected then
      self.SQLConnection1.Connected := True;

   server := TServerMethods1Client.Create(SQLConnection1.DBXConnection);
   try
      retStr := server.getRecordCount(self.Ed_SQLCmd.Text);
      ShowMessage('Reture count: ' + retStr);
   finally
      Server.Free;
   end;
end;

所以執行的畫面,就如上面第二張的截圖,出現了Return Count: 4, 因為我只在資料庫裡面建立了四筆資料,所以select * from testRec 查詢執行後,取得了四筆資料,回傳4這個字串。

擴增Server端功能- StreamGet

接著,我們要把查詢到的資料回傳給Client,由於回傳的資料我們要用Serialization(序列化)處理後,直接回傳到Client,所以就直接回傳TStreamClient端接到了資料,再把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;
begin
  if FStreamGetSQLCommand = nil then
  begin
    FStreamGetSQLCommand := 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);
var
   Server: TObject;
   LMemStream: TMemoryStream;
begin
   server := TServerMethods1Client.Create(SQLConnection1.DBXConnection);
   LMemStream := CopyStream(TServerMethods1Client(Server).StreamGetSQL('select * from testRec'));
   try
      if LMemStream <> nil then begin
         LMemStream.Position := 0;
         self.FDSchemaAdapter1.LoadFromStream(LMemStream,
             TFDStorageFormat.sfBinary);

         self.FDMemTable1.First;
         ShowMessage(self.FDMemTable1.FieldByName('name').AsString);
      end;
   finally
      LMemStream.Free;
      Server.Free;
   end;
end;
 這裡面的CopyStream,是Delphi範例裡面的程式碼,我覺得用起來沒問題,所以就Copy來繼續用了。
上面這段程式,是我抓了第一筆資料裡面的name欄位來顯示的,想不起來資料長怎樣嗎?我們回頭看一下MySQL裡面建好的資料:

第一筆資料的nameApple,所以執行結果也會顯示Apple:


取回的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我也只寫過一次,要我直接寫PHPRESTful server端還比較快,連我都可以這麼快摸清楚,您以前就會Midas了,一定可以看著我摸索的過程、我發現的一些眉角,立刻做出簡單的DataSnap程式的。
當然,一如過去的幾篇文章,我也把範例程式碼完整的放在這裡,您可以下載回去看看我實作的簡單範例。

下一篇,您可能會以為是DataSnap直接做RESTful server,答案是YES and NO,是的,我們會介紹DataSnap透過RESTful 資料進行傳輸,不過,我會多跳一步,我們直接看『DataSnap WebBroker Application』,用Delphi WebBroker應用程式,先用VCL應用程式,再改為ISAPI格式,這樣一來,各種排列組合的效能,就可以攤開來做比較了,您覺得呢?