維's profileIT : 是工作還是嗜好?PhotosBlogListsMore ![]() | Help |
|
November 28 平行處理機制,平行Linq和Delphi Prism 在上篇我從一些蛛絲馬跡來猜測Delphi除了朝原生64位元發展之外,也可能會在非同步,平行執行方向進行突破的發展。當這篇文章貼出之後,很快的我的朋友就問我『這樣的』Delphi會在什麼時候出現,說實話我不知道,因為這也是我從一些蛛絲馬跡『猜測』的,所以我當然不知道,只能等時間來告訴我們了,呵呵。 Delphi Prism程式語言中提供了許多非同步,平行執行的機制,除了上次我們看到的async和parallel之外,當我們使用這些非同步,平行執行的機制結合PFX 框架時,也能夠很方便的在使用Linq時也享受平行執行的好處,例如下面是傳統使用Linq的一個典型,簡單的範例: class method ConsoleApp.Linq; var words : sequence of string; begin words := ['嗨', '世界' , 'Linq', 'Parallel Linq' , 'Delphi Prism', 'Delphi 2009', 'BCB 2009', '3rdRail']; var rQuery := from word in words select word; for each s in rQuery do Console.WriteLine(s); Console.ReadLine; end; 當我們使用Delphi Prism編譯並且執行之後,會看到如下的結果: 我們從上圖的執行結果可以看到,它輸出的結果次序和我們在程式碼中定義sequence的次序是一樣。 但是如果我們稍微修改一點程式碼,在words之後呼叫擴展方法AsParallel如下所示: class method ConsoleApp.PLinq; var words : sequence of string; begin words := ['嗨', '世界' , 'Linq', 'Parallel Linq' , 'Delphi Prism', 'Delphi 2009', 'BCB 2009', '3rdRail']; var rQuery := from word in words.AsParallel select word; rQuery.ForAll<string>(word -> Console.WriteLine(word)); Console.ReadLine; end; 那麼我們會看到如下的執行結果: 從上圖中可以發現每次執行平行查詢時執行的結果次序都不同,而且輸出的次序也不是程式碼中定義的次序,這個證明了當使用Delphi Prism的平行機制結合PFX框架時的確是以多個平行執行的核心來處理的。 使用.NET Reflector反組編PLinq方法也可以看到它的確是呼叫PFX框架中提供的平行處理機制: class method ConsoleApp.PLinq; begin ParallelEnumerable.ForAll<String>(ParallelQuery.AsParallel<String>(array of(['嗨', '世界', 'Linq', 'Parallel Linq', 'Delphi Prism', 'Delphi 2009', 'BCB 2009', '3rdRail'])), delegate (word: String) begin Console.WriteLine(word) end; Console.ReadLine end; 和呼叫Linq方法是截然不同的。 class method ConsoleApp.Linq; begin var rQuery: IEnumerable<String> := array of(['嗨', '世界', 'Linq', 'Parallel Linq', 'Delphi Prism', 'Delphi 2009', 'BCB 2009', '3rdRail']); if (rQuery <> nil) then {pseudo} goto Label_0058; rQuery.GetEnumerator; var enumerator: IEnumerator<String> := nil; if (enumerator <> nil) then while enumerator.MoveNext do begin var s: String := enumerator.Current; Console.WriteLine(s) end; Console.ReadLine end; Delphi Prism提供的先進非同步,平行執行機制突然讓原生Delphi 32/64有了新的發展方向。 November 24 使用Delphi開發分散式JSON應用系統 在12月我又受委託將舉行4場有關DataSnap和分散式應用系統開發的技術研討會, 在這次的研討會中我將討論下面相關的內容:
同樣的, 這是免費的技術研討會, 日期是在12/09~12/12, 如果您有興趣的話, 您可以在下面的URL報名參加: http://www.sinter.com.tw/codegear/seminars/delohi200920081120.html November 21 非同步,平行執行技術,從Delphi到Delphi Prism,再到未來的Delphi? 前一陣子Delphi的現任總架構師Allen Bauer討論了許多有關非同步和平行處理的文章,而更早MS也在.NET平台上推出Microsoft Parallel Extensions to Framework 3.5套件,開始進行對於平行開發技術的演進,這個趨勢是想見而知的,因為現今的CPU都是多核心,每一個核心又都支援多執行緒執行的能力,因此軟體技術如何搭配硬體的進步,進而發揮硬體的計算能力是軟體突破的重要方向之一。如果各位嗅覺靈敏的話,從Allen的一系列文章以及他的研究方向來看,各位應該可以開始猜測未來Delphi的發展方向了。 除了這些蛛絲馬跡之外,CodeGear最近又宣佈了Delphi Prism,Delphi Prism除了是一個獨立的產品之外,也將和Delphi/C++Builder 2009整合為RAD Studio 2009,提供在.NET方面的解決方案。不過Delphi Prism並不是Delphi.NET,Delphi Prism是.NET平台上使用類似Delphi語法的程式語言,並且包含了許多先進的程式語言功能。有趣的是Delphi Prism已經在程式語言本身加入了許多非同步,平行執行功能,當結合Microsoft PFX framework時,能夠自動使用PFX中的平行執行技術。 今天我在CodeGear的部落格上看到了Andreano討論有關Delphi Prism的文章,其中就有討論到Delphi Prism程式語言中使用關鍵字async來建立非同步執行的範例,我覺得蠻有意思的,因為這代表現在可以開始討論Delphi Prism了。在Andreano的範例中,他使用了async來宣告一個方法,在Delphi Prism中使用async關鍵字宣告的方法就合格成為一個非同步執行的方法,讓我使用一個範例來說明一下。 在下面的程式碼中宣告了5個類別方法,以及兩個使用async宣告的非同步方法CallLoops和CallParallelLoops: type ConsoleApp = class public class method Main; class method CallAsyncMethod; class method CallAsyncWithParallel; class method Linq; class method PLinq; method CallLoops(iID : Integer) ; async; method CallParallelLoops(iID : Integer); async; end; 接著我在類別方法Main中呼叫其他四個類別方法: class method ConsoleApp.Main; begin ConsoleApp.CallAsyncMethod; ConsoleApp.CallAsyncWithParallel; ConsoleApp.Linq; ConsoleApp.PLinq; end; 在CallAsyncMethod中,它在一個迴圈中呼叫了使用async宣告的CallLoops方法,因此.NET會產生四個執行緒來執行CallLoops: class method ConsoleApp.CallAsyncMethod; begin Console.WriteLine('開始執行非同步方法'); with lConsoleApp := new ConsoleApp() do begin for i : Integer := 0 to 4 do lConsoleApp.CallLoops(i); end; Console.WriteLine('結束執行非同步方法'); Console.ReadLine; end; 而CallLoops方法只是很簡單的執行一個迴圈,列印訊息並且使用Thread在每一次迴圈暫時暫停0.1秒。 method ConsoleApp.CallLoops(iID : Integer) ; begin for i : Integer := 0 to 4 do begin Console.WriteLine('執行緒' + iID.ToString + ' : ' + i.ToString); System.Threading.Thread.Sleep(100); end; Console.WriteLine('執行緒' + iID.ToString + '執行完成'); end; 讓我們看看這個執行的結果,從下圖中讀者可以看到.NET果然以四個執行緒,以不定的次序執行每一個執行緒,所以在Delphi Prism中要開發非同步執行的方法非常的簡單的,只要使用關鍵字async來宣告方法即可。 然而讀者如果仔細觀察上圖的執行狀態,會發現雖然四個執行緒以不定的次序執行,但是每一個執行緒在執行CallLoops方法的迴圈時仍然是以順序的方式在執行迴圈,這代表CallLoops雖然是在獨立的執行緒中執行,但仍然沒有使用平行執行技術。但在Delphi Prism中我們只需要使用一個parallel關鍵字就可以立刻使用Microsoft PFX framework提供的平行技術。 例如下面的CallAsyncWithParallel使用了幾乎和CallAsyncMethod一樣的程式碼,只是CallAsyncWithParallel呼叫了非同步方法CallParallelLoops: class method ConsoleApp.CallAsyncWithParallel; begin Console.WriteLine('開始執行非同步, 並行方法'); with lConsoleApp := new ConsoleApp() do begin for i : Integer := 0 to 4 do lConsoleApp.CallParallelLoops(i); end; Console.WriteLine('結束執行非同步, 並行方法'); Console.ReadLine; end; 而CallParallelLoops也幾乎和CallLoops一樣,只是注意CallParallelLoops在呼叫for迴圈時使用了另外一個關鍵字parallel。 method ConsoleApp.CallParallelLoops(iID : Integer) ; begin for parallel i : Integer := 0 to 4 do begin Console.WriteLine('執行緒' + iID.ToString + ' : ' + i.ToString); System.Threading.Thread.Sleep(100); end; Console.WriteLine('執行緒' + iID.ToString + '執行完成'); end; 在Delphi Prism中如果使用關鍵字parallel而且開發人員參考了Microsoft PFX framework,那麼Delphi Prism便會編譯出如下的程式碼:
下圖是CallAsyncWithParallel執行的結果,讀者可以仔細觀察,CallAsyncWithParallel也產生了四個執行緒來執行,但是每一個執行緒在執行CallParallelLoops迴圈時卻不是以順序的方式來執行,而是使用平行的方式執行for迴圈。 從上面的討論中我們可以瞭解Delphi Prsim提供了非常方便的非同步和平行處理的機制,讓開發人員能夠在結合PFX時開發出平行執行架構。 在下次的文章中讓我們再看看Delphi Prism在支援Linq和平行Linq時是多麼的方便。 November 20 DBX框架篇 : 第1章 初探DBX4框架 - 21-2 執行SQL命令 : TDBXCommand類別 在DBX4框架中TDBXCommand類別是使用來執行SQL命令的,在上一小節中我們已經在開發人員必須呼叫TDBXConnection的CreateCommand方法來建立TDBXCommand物件,再藉由TDBXCommand物件來執行SQL命令。 下面的表格列出了TDBXCommand類別中最常使用的方法和特性: 要使用TDBXCommand物件來執行SQL命令,開發人員必須把 SQL命令指定給Text特性,例如 aCommand.Text := ‘Select count(*) from DBXTESTTABLE’; 如果要執行擁有動態參數的SQL命令,那麼動態參數必須使用’?’字元來代表,例如下面的SQL命令藉由在執行時指定“研討會名稱”欄位的實際數值從DBXTESTTABLE資料表中選擇符合條件的結果資料集: aCommand.Text := ‘Select * from DBXTESTTABLE where “研討會名稱” = ? ’; 那麼如何在應用程式執行時實際設定SQL命令中的動態參數值呢? 這就必須使用TDBXParameter類別。 1-2-1 代表SQL敘述中動態參數的類別 : TDBXParameter TDBXParameter類別物件可代表SQL命令中的每一個以’?’字元代表的動態參數,因此SQL命令中擁有多少個以’?’字元代表的動態參數,那麼開發人員就必須為每一個以’?’字元代表的動態參數建立一個相對應的TDBXParameter物件,然後設定TDBXParameter物件中正確的參數特性值,最後再呼叫TDBXCommand物件的Parameters特性的AddParameter方法把每一個TDBXParameter物件加入到TDBXCommand物件,最後才能呼叫TDBXCommand的ExecuteQuery或是ExecuteQuery真正的執行SQL命令。 TDBXParameter類別也擁有許多的方法和特性,下面的表格列出了比較重要的方法和特性: 現在讓我們看看如何使用TDBXParameter來執行動態SQL,假設我們需要執行下面的SQL敘述: select * from SEMINARS where “主講人” = ? 由於在上面的SQL敘述中包含一個以’?’字元代表的動態參數,因此我們需要建立一個TDBXParameter物件,設定它的資料型態和數值再加入到TDBXCommand物件中才能呼叫ExecuteQuery來執行此SQL敘述。下面的程式碼展示了如何執行上面的動態SQL敘述: 001 procedure TForm5.執行SQL命令; 002 var 003 aCommand : TDBXCommand; 004 aParameter : TDBXParameter; 005 aReader: TDBXReader; 006 begin 007 if (Assigned(dbx4Conn)) then 008 begin 009 aCommand := dbx4Conn.CreateCommand; 010 try 011 aCommand.CommandType := TDBXCommandTypes.DbxSQL; 012 aCommand.Text := Self.edtSQL.Text; 013 aParameter := aCommand.CreateParameter; 014 aParameter.DataType := TDBXDataTypes.WideStringType; 015 aParameter.Value.AsString := Self.edt主講人.Text; 016 aCommand.Parameters.AddParameter(aParameter); 017 aReader := aCommand.ExecuteQuery; 018 while (aReader.Next) do 019 begin 020 Self.ListBox1.Items.Add(aReader.Value['名稱'].GetWideString + ' : ' + aReader.Value['主講人'].GetWideString); 021 end; 022 finally 023 FreeAndNil(aReader); 024 FreeAndNil(aCommand); 025 end; 026 end; 012行設定了TDBXCommand要執行的SQL敘述之後,013行建立TDBXParameter物件,014行設定TDBXParameter物件的資料型態,015行設定實際動態參數的數值,016行把TDBXParameter物件加入到TDBXCommand物件,017行執行動態SQL敘述,018行之後藉由TDBXReader物件來處理結果資料集,在下一小節中將說明TDBXReader類別。 下圖是上面程式碼執行動態SQL敘述的結果: 圖1-4使用TDBXCommand和TDBXParameter執行動態SQL敘述 在使用TDBXCommand物件執行命令時,開發人員必須指定TDBXCommand物件執行的命令種類,而這些TDBXCommand物件能夠執行的命令種類是由TDBXCommandTypes類別定義。TDBXCommandTypes類別定義了數個常數值(const)來定義不同的命令種類,開發人員需要使用它來設定TDBXCommand物件的CommandType特性,類似上面的011行程式碼。 下面的表格整理了TDBXCommandTypes定義的常數值以及這些常數值使用的意義: 從上表可知,TDBXCommand不但可以執行SQL命令,執行後端預儲程序,存取資料庫元資料,更可以執行DataSnap 2009中加入的DataSnap伺服器輸出的方法。例如,假設我們有一個DataSnap 2009開發的JSON伺服器,它輸出了一個GetSpeakerCount方法能夠回傳後端Speaker資料表中所有記錄的總數值,那麼我們就可以使用下面的程式碼在用戶端使用TDBXCommand物件來執行遠端JSON伺服器中的GetSpeakerCount方法並且取得執行結果: function TDSServerModule2Client.GetSpeakerCount: Integer; begin if FGetSpeakerCountCommand = nil then begin FGetSpeakerCountCommand := FDBXConnection.CreateCommand; FGetSpeakerCountCommand.CommandType := TDBXCommandTypes.DSServerMethod; FGetSpeakerCountCommand.Text := 'TDSServerModule2.GetSpeakerCount'; FGetSpeakerCountCommand.Prepare; end; FGetSpeakerCountCommand.ExecuteUpdate; Result := FGetSpeakerCountCommand.Parameters[0].Value.GetInt32; end; 我們只需要如上設定TDBXCommand物件的CommandType特性值為TDBXCommandTypes.DSServerMethod命令種類之後就可以執行遠端服務方法了。 在稍後討論如何開發DataSnap 2009分散式應用程式時會再詳細的說明如何在用戶端執行遠端JSON伺服器輸出的方法。 1-2-2 存取結果資料集 : TDBXReader類別 當開發人員使用TDBXCommand執行命令時,TDBXCommand的ExecuteQuery方法會回傳TDBXReader物件,在DBX4框架中TDBXReader通常使用來代表執行命令之後回傳到用戶端的結果物件,如果開發人員執行的結果是資料集,那麼就可以使用TDBXReader來取得結果資料集,如果執行命令的結果是單一數值,那麼也會以結果資料集的形式儲存在TDBXReader物件中,開發人員仍然可以使用TDBXReader物件來存取結果數值。 TDBXReader是一個單向,只能往前存取的資料結構型態,類似TDataSet。當開發人員取得TDBXReader物件並且欲存取其中的執行結果時,一開始先要呼叫它的Next方法以便把cursor指到TDBXReader物件中第一筆的資料位置,那麼在處理完每一筆資料之後再呼叫Next指到下一筆資料,而在每一筆資料位置上,開發人員是藉由存取TDBXReader的Value特性來取得每一個欄位的執行結果,下面的表格整理了TDBXReader類別中最重要的方法和特性的說明: TDBXReader的Value特性是一個複載的特性,開發人員可以使用名稱或是索引值來存取TDBXReader物件中每一筆資料中每一個欄位其中的數值,在TDBXReader中Value有如下的複載的宣告: property Value[const Ordinal: TInt32]: TDBXValue read GetValue; default; property Value[const Name: UnicodeString]: TDBXValue read GetValueByName; default; 例如在前面執行SQL敘述的範例中,我們藉由TDBXReader複載的Value來存取結果資料集中'名稱'欄位的數值如下: aReader.Value['名稱'].GetWideString; 如果'名稱'欄位是SEMINARS資料表中的第二個欄位,那麼我們也可以使用索引值的方式來如下它的數值如下: aReader.Value[1].GetWideString; 因此使用TDBXReader物件通常擁有下面的兩個型式:
aDBXReader. Value[0].Get資料型態
begin aDBXReader. Value[0].Get資料型態; aDBXReader. Value[1].Get資料型態; … end; 或是: while (aDBXReader.Next) begin for iCount := 0 to aDBXReader. ColumnCount – 1 do begin aDBXReader. Value[iCount].Get資料型態; aDBXReader. Value[iCount].Get資料型態; end; … end; 此外,當開發人員實際在使用DBX4開發資料庫或是分散式應用程式時,應該大都是使用DBX4的元件,只有在很少的情形下才會直接使用TDBXReader,不過如果實的遇上必須使用TDBXReader時,那麼TDBXReader可以轉換為TDataSet嗎? 答案是可以的,在DXB4框架中TCustomSQLDataSet類別提供了一個重載建構函式,這個建構函式可以接受TDBXReader參數並且根據它建立TDataSet物件,如此一來就可以完成轉換TDBXReader物件為TDataSet物件的須要。下面即即是此TCustomSQLDataSet重載的建構函式宣告: TCustomSQLDataSet = class(TWideDataSet) … constructor Create(AOwner: TComponent; DBXReader: TDBXReader; AOwnsInstance: Boolean); reintroduce; overload; 因此我們可以使用如下簡單的程式碼就可以轉換TDBXReader物件為TDataSet物件: aDataSet := TCustomSQLDataSet.Create(nil, aCommand.ExecuteQuery, True); 在取得了TDataSet物件之後,它就更容易使用而且能夠和DBX4的元件一起工作了。 1-2-3 TDBXValue類別 在上面的討論中,當我們使用TDBXReader的Value特性存取執行結果數值時,實際上是取得TDBXValue類別物件,每一個TDBXValue物件中包含一個執行結果數值,或是包含一個執行結果資料集欄位的數值。 TDBXValue類別定義了許多GetXXXX方法讓開發人員取得執行結果數值,而XXXX就是不同的執行結果數值的資料型態。例如,如果執行結果是整數值,那麼就呼叫GetInt32方法來取得結果,如果是字串數值,就呼叫GetWideString,開發人員可以藉由判斷TDBXValue的ValueType特性來判斷其中數值的資料型態。 下面的表格整理了TDBXValue類別重要的方法和特性: 因此在一般的應用中,我們經常會使用如下的程式碼樣例來使用TDBXValue: case aDBXValue.ValueType.DataType of TDBXDataTypes.DateType: Result := IntToStr(aDBXValue.GetDate); TDBXDataTypes.WideStringType: Result := aDBXValue.GetWideString; TDBXDataTypes.Int32Type: Result := IntToStr(aDBXValue.GetInt32); TDBXDataTypes.DoubleType: Result := FloatToStr(aDBXValue.GetDouble); else ... end; November 14 DBX框架篇 : 第1章 初探DBX4框架 DBX技術從Delphi 6開始歷經了2次重大的改變,從Delphi 6一直到Delphi 2006是DBX技術的第1階段,在這段DBX的目的是在發展出一個輕量級,可跨平台的通用資料存取框架,以取代BDE/IDAPI成為Delphi/C++Builder的標準資料存取技術。在這個階段中,DBX技術是由VCL框架中的DBX相關類別/介面和DBX驅動程式組成,由於當時的DBX驅動程式是使用C/C++撰寫的,因此VCL中相關的DBX類別/介面也大量的使用了指標和靜態連結的方式來實作,這樣的架構在日後的擴展和維護上比較麻煩。 從Delphi 2006之後,DBX進入第二個階段,這是因為DBX除了提供Win32的存取能力之外,也需要在.NET框架 2.0之後也改變架構的ADO.NET之上加入DBX的技術,以取代BDP.NET技術,同時DBX在此時也決定採取了另外一個重大的變革,那就是將重新使用Object Pascal程式語言來實作所有的DBX驅動程式。由於DBX驅動程式改用了Object Pascal實作,因此在VCL框架中和DBX驅動程式相關的程式碼再也不需要大量使用指標來呼叫其中的函式,這個優勢讓VCL中的DBX框架有了重新使用更好的方式來實作的機會,再加上Object Pascal也能夠執行於.NET平台之後(CodeGear在.NET平台上的Object Pascal稱為Delphi程式語言),DBX框架也終於可以使用相同的設計架構執行於Win32和.NET平台,因此第二階段的DBX改以現代化的類別和介面的架構為設計,實作基礎,讓DBX真正成為完全以物件導向思維設計出來的框架,在讀者隨著本書的章節閱讀之後也將對DBX框架有更完整的瞭解。 現在讓我們先開始討論DBX4框架中的骨架類別,並且從這些最基礎而重要的類別中逐漸展開我們對於DBX4框架的掌握。 任何資料存取技術都由一些固定的樣版類別或是物件來完成,例如首先需要連結到資料來源,需要執行SQL命令,需要存取資料庫中的物件資訊等,所有的資料存取技術都是由這些基本的類別/物件提供的服務再逐漸的提供更多豐富的功能。DBX也不例外,在DBX框架中也是以這些基本而重要的類別為核心架構而成,例如:
1-1 使用DBX4技術連結資料庫 – TDBXConnectionFactory類別 DBX框架中的TDBXConnectionFactory類別嚴格的說是一個抽象父類別,它的類別靜態方法GetConnectionFactory可以回傳一個從它繼承下來的實體類別,這實體類別將實作如何載入DBX驅動程式和資料來源連結設定等重要的資訊。在目前的DBX4框架實作中,這個實體類別是TDBXIniFileConnectionFactory。 TDBXIniFileConnectionFactory會根據dbxdrivers.ini檔案中的驅動程式設定以載入驅動程式,並且根據dbxconnections.ini檔案中設定的資訊來設定連結資訊。 因此在DBX4要和資料來源連結,首先我們可以使用下面的程式碼來取得TDBXIniFileConnectionFactory物件: TDBXConnectionFactory.GetConnectionFactory. 接著在TDBXConnectionFactory類別中的GetConnection方法即可讓我們取得可連結資料來源的TDBXConnection物件。GetConnection有兩個原型宣告,它是複載(override)方法: function GetConnection( const ConnectionName : UnicodeString; const UserName: UnicodeString; const Password: UnicodeString) : TDBXConnection; overload; function GetConnection(ConnectionProperties : TDBXProperties) : TDBXConnection; overload; 第一個GetConnection重載方法接受一個連結名稱,資料來源的登錄使用者名稱和密碼來回傳TDBXConnection物件,第二個GetConnection重載方法則接受一個TDBXProperties參數而回傳TDBXConnection物件。 讓我們先解釋如何使用第一個GetConnection方法,它的第一個參數ConnectionName其實就是讀者在Delphi中使用DataExplorer建立的連結別名,例如下圖所示,假設我們有一個InterBase的連結別名IB_UCONNECTION,那麼我們就可以使用下面的程式碼來呼叫GetConnection以取得連結到InterBase的TDBXConnection物件: 圖1-1 DataExplorer顯示存在dbxconnections.ini中的連結資訊 var dbx4Conn : TDBXConnection; begin … dbx4Conn := TDBXConnectionFactory.GetConnectionFactory.GetConnection( ‘IB_UCONNECTION’, 'sysdba', 'masterkey'); … 或是建立下面的TDBXProperties物件,設定必要的特性值之後再傳遞TDBXProperties物件給GetConnection來取得和資料來源的連結: var dbxProp : TDBXProperties; dbxConn : TDBXConnection; begin dbxProp := TDBXProperties.Create; try dbxProp[TDBXPropertyNames.ConnectionName] := 'IB_UCONNECTION'; dbxProp[TDBXPropertyNames.UserName] := 'sysdba'; dbxProp[TDBXPropertyNames.Password] := 'masterkey'; dbxConn := TDBXConnectionFactory.GetConnectionFactory.GetConnection(dbxProp); Assert(Assigned(dbxConn), TDBXPropertyNames.ConnectionName +'連結失敗'); finally dbxProp.Free; dbxConn.Close; dbxConn.Free; end; 上面程式碼中的TDBXPropertyNames類別中定義了所有資料來源可能需要設定的特性值名稱,例如下面是TDBXPropertyNames中的一些定義: TDBXPropertyNames = class Const ConnectionName = 'ConnectionName'; UserName = 'User_Name'; Password = 'Password'; … end; 上面的程式碼就設定了TDBXProperties物件要連結的資料來源名稱,登錄的使用者名稱和密碼等資訊,GetConnection方法就根據這些資訊取得資料來源的連結並且回傳代表此連結的TDBXConnection物件。 TDBXConnectionFactory類別最重要的功能當然就是回傳TDBXConnection物件,因為有了TDBXConnection物件之後開發人員就可以使用它來執行各種工作,例如執行SQL命令,存取資料來源物件等,稍後本書都會說明。除此之外,TDBXConnectionFactory類別也可以讓開發人員取得所有資料來源的連結資訊,也就是圖1-1中DataExplorer顯示的資訊,以及所有dbExpress驅動程式資訊以及目前執行中的應用程式中註冊的dbExpress驅動程式資訊。 例如TDBXConnectionFactory的GetConnectionItems可以取得圖1-1中的所有資料來源連結: aSL := TStringList.Create; TDBXConnectionFactory.GetConnectionFactory.GetConnectionItems(aSL); 而GetDriverNames可以取得所有在dbxdrivers.ini檔案中的驅動程式名稱: aSL := TStringList.Create; TDBXConnectionFactory.GetConnectionFactory.GetDriverNames(aSL); GetRegisteredDriverNames則可以取得在目前的應用程式中註冊的dbExpress驅動程式,只有在應用程式中真正註冊的驅動程式才能使用來連結相對的資料來源。 aSL := TStringList.Create; TDBXConnectionFactory.GetConnectionFactory.GetRegisteredDriverNames(aSL); 例如下圖是使用上面的三個方法取得資料來源連結資訊,驅動程式以及這個範例應用程式中註冊的驅動程式,從圖中讀者就可以猜到這個範例是使用InterBase做為範例資料庫,因為目前註冊的驅動程式是InterBase,稍後我們會說明什麼是在應用程式中註冊的驅動程式。 圖1-2 TDBXConnectionFactory可以存取資料來源和驅動程式資訊 在離開TDBXConnectionFactory之前,我們必須再討論一下前面我們看到的GetConnectionFactory方法。 GetConnectionFactory的功能是回傳TDBXConnectionFactory物件,但它實際回傳的是使用Singleton設計樣例的TDBXIniFileConnectionFactory物件。 GetConnectionFactory方法首先會試著載入dbxdrivers.ini和dbxconnections.ini這兩個重要的檔案內容,載入的次序是 1. 先從應用程式目前的目錄中搜尋,如果沒有, 2. 再從系統註冊器中的HKEY_CURRENT_USER\Software\CodeGear\BDS\6.0\dbExpress中找尋, 3. 最後再後從系統註冊器中的HKEY_LOCAL_MACHINE\Software\CodeGear\BDS\6.0\dbExpress中找尋, 因此在您的系統註冊器中應該可以看到如下的資訊,記載了dbxdrivers.ini和dbxconnections.ini檔案的所在地: 圖1-3 系統註冊器中記錄了dbxdrivers.ini和dbxconnections.ini所在地 之後,GetConnectionFactory會建立唯一的一個TDBXIniFileConnectionFactory物件,再呼叫由TDBXIniFileConnectionFactory物件複載TDBXConnectionFactory的兩個抽象方法LoadDrivers和LoadConnections來分析dbxdrivers.ini和dbxconnections.ini中的資料來源資訊和驅動程式資訊: procedure LoadDrivers; virtual; procedure LoadConnections; virtual; 並且為每一個資料來源建立一個相對的TDBXProperties物件,而TDBXProperties物件中記錄的資訊就是LoadDrivers和LoadConnections方法從dbxdrivers.ini和dbxconnections.ini中分析出來的資訊。 最後當開發人員呼叫GetConnection要取得TDBXConnection物件時,TDBXConnectionFactory便會根據開發人員欲開啟的資料來源相關的TDBXProperties物件中記錄的資訊來真正的載入dbExpress驅動程式。 1-1-1 TDBXIniFileConnectionFactory類別 TDBXIniFileConnectionFactory是從TDBXConnectionFactory繼承下來的實體類別,當開發人員呼叫TDBXConnectionFactory. GetConnectionFactory方法後實際取得的物件也就是TDBXIniFileConnectionFactory物件,在目前的DBX框架實作中使用了Singleton設計樣例,也就是說整個DBX應用程式中只會建立一個TDBXIniFileConnectionFactory物件。 TDBXIniFileConnectionFactory類別最重要的就是複載TDBXConnectionFactory宣告的兩個抽象LoadDrivers和LoadConnections,這在上節中已經說明過了: TDBXIniFileConnectionFactory = class(TDBXConnectionFactory) … protected procedure LoadDrivers; override; procedure LoadConnections; override; … end; TDBXIniFileConnectionFactory提供了ConnectionsFile特性可以存取資料來源設定檔,也就是dbxconnections.ini的完整檔名,DriversFile特性則可以存取dbxdrivers.ini的完整檔名。例如下面的程式碼可以分別取得目前的DBX應用程式載入和使用的dbxconnections.ini和dbxdrivers.ini檔案。 (TDBXConnectionFactory.GetConnectionFactory as TDBXIniFileConnectionFactory).ConnectionsFile (TDBXConnectionFactory.GetConnectionFactory as TDBXIniFileConnectionFactory).DriversFile 1-1-2 TDBXConnection類別 TDBXConnection類別物件是真正代表連結到資料來源的物件,TDBXConnection類別主要的功能是讓開發人員可以建立執行SQL命令的TDBXCommand物件,啟動資料庫交易,確定資料庫交易或是取消交易,存取MetaData物件以處理資料來源物件以及存取資料來源中特定資訊等功能,可以說TDBXConnection是開發人員實際執行有關資料庫處理工作中最基礎而重要的類別。 TDBXConnection提供了許多的方法和特性讓開發人員能夠進行重要的工作,下面的表格列出了其中最重要的方法/特性以及簡單的說明:
上述的一些方法都擁有複載(override)的原型,例如BeginTransaction方法就提供了下面的兩個原型宣告: function BeginTransaction(Isolation: TDBXIsolation): TDBXTransaction; overload; virtual; function BeginTransaction: TDBXTransaction; overload; virtual; 讓我們看看如何使用TDBXConnection物件讀者就可以瞭解如何使用上面的方法了。假設現在我們希望藉由DBX4執行下面的SQL命令: select count(*) from DBXTESTTABLE 我們需要使用下面的步驟來執行這個SQL命令: 1. 取得TDBXConnection物件 2. 使用TDBXConnection物件建立TDBXCommand物件,稍後的小節會討論TDBXCommand類別 3. 使用TDBXCommand物件執行SQL命令 4. 使用TDBXReader物件擷取執行結果,稍後的小節會討論TDBXReader類別 前面我們已經討論如何解決步驟1的方法,步驟2的TDBXCommand物件可以藉由呼叫TDBXConnection物件的CreateCommand方法來建立,例如下面的程式碼即是如何使用DBX4執行SQL命令的範例,在008行呼叫CreateCommand方法建立TDBXCommand物件。 接著步驟3的工作在010到012行完成,010行先設定TDBXCommand物件的CommandType特性值為執行SQL命令,011行設定TDBXCommand物件要執行的SQL命令,最後012行呼叫TDBXCommand的ExecuteQuery方法執行SQL命令。 001 procedure TForm1.執行SQL命令; 002 var 003 aCommand : TDBXCommand; 004 aReader: TDBXReader; 005 begin 006 if (Assigned(dbx4Conn)) then 007 begin 008 aCommand := dbx4Conn.CreateCommand; 009 try 010 aCommand.CommandType := TDBXCommandTypes.DbxSQL; 011 aCommand.Text := Self.edtSQL.Text; 012 aReader := aCommand.ExecuteQuery; 013 if aReader.Next then 014 begin 015 if aReader.ValueType[0].DataType = TDBXDataTypes.Int64Type then 016 Self.Caption := IntToStr(aReader.Value[0].GetInt64) 017 else 018 Self.Caption := IntToStr(aReader.Value[0].GetInt32); 019 end; 020 finally 021 FreeAndNil(aReader); 022 FreeAndNil(aCommand); 023 end; 024 end; 025 end; TDBXCommand的ExecuteQuery方法在執行完SQL命令之後會回傳TDBXReader物件,TDBXReader物件包含了執行SQL命令的結果資料集,藉由TDBXReader的方法開發人員就可以取得執行結果。由於在這個範例中執行的結果只是一個數值,因此013行先呼叫TDBXReader的Next方法把指標指到第一筆的執行結果,再藉由存取它的Value特性值來取得最後的結果。 讀者需要注意的是,在使用完TDBXCommand和TDBXReader物件之後都需要釋放它們,這在021和022行可以看到。 再讓我們使用另外一個範例來看看如何使用TDBXConnection物件控制資料庫交易。下面的程式碼展示了如何使用DBX4在資料庫中建立資料表,這是由DBX4框架中的MetaData等相關類別提供的服務,在稍後的章節中我們會詳細的說明這些MetaData類別,在這裡讓我們先集中焦點在TDBXConnection類別。 在下面的013行中呼叫了TDBXConnection的BeginTransaction啟動資料庫交易,之後的程式碼就執行在這個資料庫交易環境,當一切正確的執行後033行呼叫TDBXConnection的CommitFreeAndNil確定資料庫交易完成,如果在015~033行中發生的錯誤就會產生例外狀況,此時035行呼叫了TDBXConnection的RollbackFreeAndNil來取消資料庫交易。 001 procedure TForm1.建立測試資料表; 002 var 003 aProvider: TDBXMetaDataProvider; 004 Transaction: TDBXTransaction; 005 MetaDataTable: TDBXMetaDataTable; 006 anIndex : TDBXMetaDataIndex; 007 aColumn : TDBXInt32Column; 008 begin 009 if (Assigned(dbx4Conn)) then 010 begin 011 aProvider := DoGetDBXMetaProvider(dbx4Conn); 012 try 013 Transaction := dbx4Conn.BeginTransaction; 014 try 015 aProvider.DropTable(TESTTABLENAME); 016 MetaDataTable := TDBXMetaDataTable.Create; 017 MetaDataTable.TableName := TESTTABLENAME; 018 aColumn := TDBXInt32Column.Create('序號'); 019 aColumn.Nullable := False; 020 MetaDataTable.AddColumn(aColumn); 021 MetaDataTable.AddColumn(TDBXUnicodeVarCharColumn.Create('研討會名稱', 32)); 022 MetaDataTable.AddColumn(TDBXUnicodeVarCharColumn.Create('主講人', 10)); 023 MetaDataTable.AddColumn(TDBXDateColumn.Create('日期')); 024 aProvider.CreateTable(MetaDataTable); 025 026 anIndex := TDBXMetaDataIndex.Create; 027 anIndex.TableName := MetaDataTable.TableName; 028 anIndex.IndexName := 'pidx_序號'; 029 anIndex.Unique := True; 030 anIndex.AddColumn('序號'); 031 aProvider.CreatePrimaryKey(anIndex); 032 033 dbx4Conn.CommitFreeAndNil(Transaction); 034 except 035 dbx4Conn.RollbackFreeAndNil(Transaction); 036 end; 037 finally 038 anIndex.Free; 039 MetaDataTable.Free; 040 aProvider.Free; 041 dbx4Conn.RollbackIncompleteFreeAndNil(Transaction); 042 end; 043 end; 044 end; 請注意的是在041行又呼叫了TDBXConnection的RollbackIncompleteFreeAndNil方法,為什麼呢?這是因為在上面的except敘述中呼叫了TDBXConnection的RollbackFreeAndNil來取消資料庫交易,但是RollbackFreeAndNil本身也可能發生而又產生例外狀況,這有可能造成一些資源未被釋放,因此才在037行開始的finally程式區塊中呼叫RollbackIncompleteFreeAndNil來釋放任何可能未被釋放的資源,而RollbackIncompleteFreeAndNil本身不會產生例外狀況,它只是試著釋放應該被釋放的資源而已。 因此在使用TDBXConnection物件來啟動資料庫交易時,可以使用如下的設計樣例來撰寫程式碼: aTransaction := TDBXConnection物件.BeginTransaction; try //實際工作程式碼 TDBXConnection物件.CommitFreeAndNil(aTransaction); Except TDBXConnection物件. RollbackFreeAndNil(Transaction); Finally TDBXConnection物件. RollbackIncompleteFreeAndNil(Transaction) End; November 06 第3章 使用DBX4的測試框架類別(2) : 如何客製化自動產生測試資料的流程 面展示了如何使用TDbxDataGenerator自動產生測試資料,雖然DBX4框架提供了簡單的自動產生資料的能力,但在實際的開發中開發人員一定會需要產生有意義的測試資料,那麼開發人員如何能夠讓TDbxDataGenerator使用開發人員的方式來產生測試資料呢? 答案就在TDBXDataGeneratorColumn類別。 當TDbxDataGenerator要產生測試資料時,它會使用DBX4的元資料相關類別來判斷欲產生測試資料的資料表的結構,然後根據資料表的欄位型態來建立TDBXDataGeneratorColumn衍生類別的物件來產生測試資料。也許讓我們來看看TDBXDataGeneratorColumn類別的定義,再以解釋讀者就更容易瞭解這整個流程。 下面是TDBXDataGeneratorColumn的定義,它是一個抽象類別,其中定義了許多的虛擬方法以便讓衍生類別繼承並且加以實作。 TDBXDataGeneratorColumn = class abstract public constructor Create(const InMetaDataColumn: TDBXMetaDataColumn); procedure Open; virtual; destructor Destroy; override; function GetString(const Row: Int64): UnicodeString; virtual; abstract; function GetBoolean(const Row: Int64): Boolean; virtual; function GetInt8(const Row: Int64): Byte; virtual; function GetInt16(const Row: Int64): SmallInt; virtual; function GetInt32(const Row: Int64): Integer; virtual; function GetInt64(const Row: Int64): Int64; virtual; function GetDouble(const Row: Int64): Double; virtual; function GetSingle(const Row: Int64): Single; virtual; function GetBytes(const Row: Int64): TBytes; virtual; function GetDecimal(const Row: Int64): UnicodeString; virtual; function GetYear(const Row: Int64): Integer; virtual; function GetMonth(const Row: Int64): Integer; virtual; function GetDay(const Row: Int64): Integer; virtual; function GetHour(const Row: Int64): Integer; virtual; function GetMinute(const Row: Int64): Integer; virtual; function GetSeconds(const Row: Int64): Integer; virtual; function GetMilliseconds(const Row: Int64): Integer; virtual; procedure SetValue(const Row: Int64; const Value: TDBXWritableValue); virtual; abstract; protected procedure SetDataGenerator(const DataGenerator: TDBXCustomDataGenerator); virtual; function GetColumnName: UnicodeString; virtual; function GetDataType: Integer; virtual; function GetMetaDataColumn: TDBXMetaDataColumn; virtual; private function TypeNotSupported: TDBXDataGeneratorException; protected FMetaDataColumn: TDBXMetaDataColumn; FDataGenerator: TDBXCustomDataGenerator; public property DataGenerator: TDBXCustomDataGenerator write SetDataGenerator; property ColumnName: UnicodeString read GetColumnName; property DataType: Integer read GetDataType; property MetaDataColumn: TDBXMetaDataColumn read GetMetaDataColumn; end; 例如假設欲產生測試資料的資料表的第一個欄位型態是整數序列號,那麼TDbxDataGenerator便會建立TDBXInt32SequenceGenerator的物件來產生測試資料,由於TDBXInt32SequenceGenerator是提供產生整數序列號的測試資料,因此它只複載了TDBXDataGeneratorColumn的GetInt32虛擬方法以提供整數序列號數值,複載GetString虛擬方法以字串的型態提供整數序列號數值, TDBXInt32SequenceGenerator並不需要複載所有TDBXDataGeneratorColumn的虛擬方法。 TDBXInt32SequenceGenerator = class(TDBXDataGeneratorColumn) public constructor Create(const MetaData: TDBXMetaDataColumn); procedure Open; override; function GetInt32(const Row: Int64): Integer; override; function GetString(const Row: Int64): UnicodeString; override; procedure SetValue(const Row: Int64; const Value: TDBXWritableValue); override; end; 而例如假設欲產生測試資料的資料表的第一個欄位型態是字串型態,那麼TDbxDataGenerator便會建立TDBXWideStringSequenceGenerator的物件來產生測試資料,TDBXWideStringSequenceGenerator的定義如下,它的使用方式和前面介紹的TDBXInt32SequenceGenerator是一樣的,只是它是負責提供產生字串型態的測試資料: TDBXWideStringSequenceGenerator = class(TDBXDataGeneratorColumn) public constructor Create(const MetaData: TDBXMetaDataColumn); procedure Open; override; function GetString(const Row: Int64): UnicodeString; override; procedure SetValue(const Row: Int64; const Value: TDBXWritableValue); override; end; 在DBX4框架中類似TDBXInt32SequenceGenerator類別以提供各種不同型態的測試資料類別超過10個以上,而且開發人員還可以自己定義/實作客製化類別,稍後我們就會討論。下面的表格說明了這些類別:
讓我們看看TDBXWideStringSequenceGenerator如何產生測試資料,下面是TDBXWideStringSequenceGenerator的GetString方法,它會在TDbxDataGenerator要為字串型態的欄位產生測試資料時呼叫: function TDBXWideStringSequenceGenerator.GetString(const Row: Int64): UnicodeString; var Value: UnicodeString; begin Value := 'W' + IntToStr(Row); if FMetaDataColumn.FixedLength then while Length(Value) < FMetaDataColumn.Precision do Value := Value + ' '; Result := Value; end; 從上面的實作程式碼可以知道,TDBXWideStringSequenceGenerator.GetString在產生測試資料時是以’W’字元為起頭並且加入正在產生測試資料的列行數的數值,例如為第一筆資料產生的是W1,第2筆就是W2等以此類推,這也解釋了為什麼圖3-2中『研討會名稱』和『主講人』欄位的測試資料就是這樣的內容。 瞭解了上面討論的原理之後,那麼我們如何讓TDbxDataGenerator產生我們需要的測試資料而不是內定的簡單而無意義的測試資料呢? 答案很簡單,我們只需要進行下面的兩項工作:
首先讓我們看看如何完成上面的第二個步驟,這個工作很簡單,我們只需要建立客製化TDBXDataGeneratorColumn衍生類別物件,並且加入到TDbxDataGenerator物件之中即可。 例如在下面的程式碼中,我們首先在047行中藉由元資料類別取得測試資料表的每一個欄位物件,然後在049到067行中一一的判斷每一個欄位物件的資料型態,接著就建立相對應的TDBXDataGeneratorColumn客製化衍生類別物件,再呼叫TDbxDataGenerator的AddColumn方法把它加入到TDbxDataGenerator物件之中。 001 procedure TForm5.加入欄位物件(aProvider : TDBXMetaDataProvider; DataGenerator: TDbxDataGenerator); 002 003 function 建立整數欄位(cName : string) : TDBXInt32SequenceGenerator; 004 var 005 col : TDBXInt32Column; 006 begin 007 col := TDBXInt32Column.Create(cName); 008 try 009 Result := TDBXInt32SequenceGenerator.Create(col); 010 finally 011 FreeAndNil(col); 012 end; 013 end; 014 015 function 建立字串欄位(cName : string; iLength : Integer) : TDBXWideStringSequenceGenerator; 016 var 017 col : TDBXUnicodeVarCharColumn; 018 begin 019 col := TDBXUnicodeVarCharColumn.Create(cName, iLength); 020 try 021 if (col.ColumnName = '主講人') then 022 Result := T主講人產生器.Create(col) 023 else 024 if (col.ColumnName = '研討會名稱') then 025 Result := T研討會產生器.Create(col) 026 else 027 Result := TDBXWideStringSequenceGenerator.Create(col); 028 finally 029 FreeAndNil(col); 030 end; 031 end; 032 033 function 建立日期欄位(cName : string) : TDBXDateSequenceGenerator; 034 var 035 col : TDBXDateColumn; 036 begin 037 col := TDBXDateColumn.Create(cName); 038 try 039 Result :=TDBXDateSequenceGenerator.Create(col); 040 finally 041 FreeAndNil(col); 042 end; 043 end; 044 var 045 cols : TDBXColumnsTableStorage; 046 begin 047 cols := 取得欄位物件(aProvider, TESTTABLENAME); 048 049 while (cols.InBounds) do 050 begin 051 case cols.DbxDataType of 052 TDBXDataTypes.UnknownType, TDBXDataTypes.Int32Type : 053 begin 054 DataGenerator.AddColumn(建立整數欄位(cols.ColumnName)); 055 end; 056 TDBXDataTypes.WideStringType : 057 begin 058 DataGenerator.AddColumn(建立字串欄位(cols.ColumnName, cols.Precision)); 059 end; 060 TDBXDataTypes.DateType : 061 begin 062 DataGenerator.AddColumn(建立日期欄位(cols.ColumnName)); 063 end; 064 end; 065 cols.Next; 066 end; 067 end; 那麼步驟1如何完成? 也很簡單,我們只需要定義從TDBXDataGeneratorColumn或是它的衍生類別繼承下來的客製化類別,再於其中撰寫如何產生測試資料的程式碼即可。 例如下面就是上面程式碼使用的『T研討會產生器』類別,這個類別負責在測試資料表的『研討會名稱』欄位中產生測試資料: 001 unit u研討會產生器; 002 003 interface 004 uses 005 DBXCommon, 006 DBXCommonTable, 007 DBXMetaDataProvider, 008 SysUtils, 009 DBXCustomDataGenerator; 010 011 type 012 T研討會產生器 = class(TDBXWideStringSequenceGenerator) 013 public 014 constructor Create(const MetaData: TDBXMetaDataColumn); 015 procedure Open; override; 016 function GetString(const Row: Int64): UnicodeString; override; 017 procedure SetValue(const Row: Int64; const Value: TDBXWritableValue); override; 018 private 019 dataArray : array[0..5] of UnicodeString; 020 iPos : Integer; 021 procedure InitailizeData; 022 end; 023 024 implementation 025 026 { TDBXMyWideStringSequenceGenerator } 027 028 constructor T研討會產生器.Create( 029 const MetaData: TDBXMetaDataColumn); 030 begin 031 inherited Create(MetaData); 032 InitailizeData; 033 end; 034 035 function T研討會產生器.GetString( 036 const Row: Int64): UnicodeString; 037 begin 038 Result := dataArray[iPos mod 6]; 039 Inc(iPos); 040 end; 041 042 procedure T研討會產生器.InitailizeData; 043 begin 044 dataArray[0] := 'Delphi 2009產品技術發表會'; 045 dataArray[1] := 'C++Builder 2009產品技術發表會'; 046 dataArray[2] := 'Delphi 2009 Unicode程式設計'; 047 dataArray[3] := 'Delphi 2009 DataSnap程式設計'; 048 dataArray[4] := 'C++Builder 2009 Unicode程式設計'; 049 dataArray[5] := 'C++Builder 2009 DataSnap程式設計'; 050 iPos := 0; 051 end; 052 053 procedure T研討會產生器.Open; 054 begin 055 inherited open; 056 end; 057 058 procedure T研討會產生器.SetValue(const Row: Int64; 059 const Value: TDBXWritableValue); 060 begin 061 Value.SetWideString(GetString(Row)); 062 end; 063 end. 正是由於我們提供了客製化的『T研討會產生器』類別,因此TDbxDataGenerator才在圖3-3中產生了客製化的測試資料,如何?整個客製化的流程很簡單吧。 Ok,希望現在各位就瞭解如何使用和整合DBX4框架來自動測試任何的測試資料了。 |
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|