首先從一個簡單的例子說起:假設有一個ADODataSet控件,連接羅斯文數據庫,SQL為:
select * from Employee
現在要把它的內容中EmployeeID, FirstName, LastName,BirthDate四個字段顯示到ListVIEw裡。傳統的代碼如下:
With ADODataSet1 Do Begin Open; While Not Eof Do Begin With ListView1.Add Do Begin Caption := IntToStr( FieldByName( 'EmployeeID' ).AsInteger ); SubItems.Add( FieldByName( 'FirstName' ).AsString ); SubItems.Add( FieldByName( 'LastName' ).AsString ); SubItems.Add( FormatDateTime( FIEldByName( 'BirthDate' ).AsDateTime ) ); End; Next; End; Close; End;
這裡主要存在幾個方面的問題:
1、首先是有很多代碼非常冗長。比如FIEldByName和AsXXX等,特別是AsXXX,必須時時記得每個字段是什麼類型的,很容易搞錯。而且有些不兼容的類型如果不能自動轉換的話,要到運行時才能發現錯誤。
2、需要自己在循環裡處理當前記錄的移動。如上面的Next,否則一旦忘記就會發生死循環,雖然這種問題很容易發現並處理,但程序員不應該被這樣的小細節所糾纏。
3、最主要的是字段名通過String參數傳遞,如果寫錯的話,要到運行時才會發現,增加了潛在的BUG可能性,特別是如果測試沒有完全覆蓋所有的FIEldByName,很可能使這樣的問題拖到客戶那邊才會出現。而這種寫錯字段名的情況是很容易發生的,特別是當程序使用了多個表時,還容易將不同表的字段名搞混。
在這個由OO統治的時代裡,碰到與數據集有關的操作時,我們還是不得不常常陷入上面說的這些關系數據庫方面的細節問題中。當然現在也有擺脫它們的辦法,那就是O/R mapping,但是O/R mapping畢竟與傳統的開發方式差別太大,特別是對於一些小的應用來說,沒必要這麼誇張,在這種情況下,我們需要的只是一個簡單的數據集對象化方案。
在Java及其它動態語言的啟發下,我想到了用Delphi強大的RTTI來實現這個簡單的數據集對象化方案。下面是實現與傳統代碼同樣功能的數據集對象化應用代碼:
Type TDSPEmployee = class(TMDataSetProxy) published Property EmployeeID : Integer Index 0 Read GetInteger Write SetInteger; Property FirstName : String Index 1 Read GetString Write SetString; Property LastName : String Index 2 Read GetString Write SetString; Property BirthDate : Variant Index 3 Read GetVariant Write SetVariant; end; procedure TForm1.ListClick(Sender: TObject); Var emp : TDSPEmployee; begin emp := TDSPEmployee.Create( ADODataSet1 ); Try While ( emp.ForEach ) Do With ListVIEw1.Items.Add Do Begin Caption := IntToStr( emp.EmployeeID ); SubItems.Add( emp.FirstName ); SubItems.Add( emp.LastName ); SubItems.Add( FormatDateTime( 'yyyy-mm-dd', TDateTime( emp.BirthDate ) ) ); End; Finally emp.Free; End; end;
用法很簡單。最主要的是要先定義一個代理類,其中以Published的屬性來定義所有的字段,包括其類型,之後就可以以對象的方式來操作數據集了。這個代理類是從TMDataSetProxy派生來的,其中用RTTI實現了從屬性操作到字段操作的映射,使用時只要簡單地Uses一下相應的單元即可。關於這個類的實現單元將在下面詳細說明。
表面上看多了一個定義數據集的代理類,好像多了一些代碼,但這是一件一勞永逸的事,特別是當程序中需要多次重用同樣結構的數據集的情況下,將會使代碼量大大減少。更何況這個代理類的定義非常簡單,只是根據字段名和字段類型定義一系列的屬性罷了,不用任何實現代碼。其中用到的屬性存取函數 GetXXX/SetXXX都在基類TMDataSetProxy裡實現了。
現在再來看那段與原代碼對應的循環:
1、FIEldByName和AsXXX都不需要了,變成了對代理類的屬性操作,而且每個字段對應的屬性的類型在前面已經定義好了,不用再每次用到時來考慮一下它是什麼類型的。如果用錯了類型,在編譯時就會報錯。
2、用一個ForEach來進行記錄遍歷,不用再擔心忘記Next造成的死循環了。
3、最大的好處是字段名變成了屬性,這樣就可以享受到編譯時字段名校驗的好處了,除非是定義代理類時就把字段名寫錯,否則都能在編譯時發現。
現在開始討論TMDataSetProxy。其實現的代碼如下:
(****************************************************************** 用RTTI實現的數據集代理,可以簡單地將數據集對象化。 Copyright (c) 2005 by Mental Studio. Author : 猛禽Date : Jan.28-05 ******************************************************************) unit MDSPComm; interface Uses Classes, DB, TypInfo; Type TMPropList = class(TObject) private FPropCount : Integer; FPropList : PPropList; protected Function GetPropName( aIndex : Integer ) : ShortString; function GetProp(aIndex: Integer): PPropInfo; public constructor Create( aObj : TPersistent ); destructor Destroy; override; property PropCount : Integer Read FPropCount; property PropNames[aIndex : Integer] : ShortString Read GetPropName; property Props[aIndex : Integer] : PPropInfo Read GetProp; End; TMDataSetProxy = class(TPersistent) private FDataSet : TDataSet; FPropList : TMPropList; FLooping : Boolean; protected Procedure BeginEdit; Procedure EndEdit; Function GetInteger( aIndex : Integer ) : Integer; Virtual; Function GetFloat( aIndex : Integer ) : Double; Virtual; Function GetString( aIndex : Integer ) : String; Virtual; Function GetVariant( aIndex : Integer ) : Variant; Virtual; Procedure SetInteger( aIndex : Integer; aValue : Integer ); Virtual; Procedure SetFloat( aIndex : Integer; aValue : Double ); Virtual; Procedure SetString( aIndex : Integer; aValue : String ); Virtual; Procedure SetVariant( aIndex : Integer; aValue : Variant ); Virtual; public constructor Create( aDataSet : TDataSet ); destructor Destroy; override; Procedure AfterConstruction; Override; function ForEach : Boolean; Property DataSet : TDataSet Read FDataSet; end; implementation { TMPropList } constructor TMPropList.Create(aObj: TPersistent); begin FPropCount := GetTypeData(aObj.ClassInfo)^.PropCount; FPropList := Nil; if FPropCount > 0 then begin GetMem(FPropList, FPropCount * SizeOf(Pointer)); GetPropInfos(aObj.ClassInfo, FPropList); end; end; destructor TMPropList.Destroy; begin If Assigned( FPropList ) Then FreeMem( FPropList ); inherited; end; function TMPropList.GetProp(aIndex: Integer): PPropInfo; begin Result := Nil; If ( Assigned( FPropList ) ) Then Result := FPropList[aIndex]; end; function TMPropList.GetPropName(aIndex: Integer): ShortString; begin Result := GetProp( aIndex )^.Name; end; { TMRefDataSet } constructor TMDataSetProxy.Create(aDataSet: TDataSet); begin Inherited Create; FDataSet := aDataSet; FDataSet.Open; FLooping := false; end; destructor TMDataSetProxy.Destroy; begin FPropList.Free; If Assigned( FDataSet ) Then FDataSet.Close; inherited; end; procedure TMDataSetProxy.AfterConstruction; begin inherited; FPropList := TMPropList.Create( Self ); end; procedure TMDataSetProxy.BeginEdit; begin If ( FDataSet.State <> dsEdit ) AND ( FDataSet.State <> dsInsert ) Then FDataSet.Edit; end; procedure TMDataSetProxy.EndEdit; begin If ( FDataSet.State = dsEdit ) OR ( FDataSet.State = dsInsert ) Then FDataSet.Post; end; function TMDataSetProxy.GetInteger(aIndex: Integer): Integer; begin Result := FDataSet.FieldByName( FPropList.PropNames[aIndex] ).AsInteger; end; function TMDataSetProxy.GetFloat(aIndex: Integer): Double; begin Result := FDataSet.FieldByName( FPropList.PropNames[aIndex] ).AsFloat; end; function TMDataSetProxy.GetString(aIndex: Integer): String; begin Result := FDataSet.FieldByName( FPropList.PropNames[aIndex] ).AsString; end; function TMDataSetProxy.GetVariant(aIndex: Integer): Variant; begin Result := FDataSet.FieldByName( FPropList.PropNames[aIndex] ).Value; end; procedure TMDataSetProxy.SetInteger(aIndex, aValue: Integer); begin BeginEdit; FDataSet.FieldByName( FPropList.PropNames[aIndex] ).AsInteger := aValue; end; procedure TMDataSetProxy.SetFloat(aIndex: Integer; aValue: Double); begin BeginEdit; FDataSet.FieldByName( FPropList.PropNames[aIndex] ).AsFloat := aValue; end; procedure TMDataSetProxy.SetString(aIndex: Integer; aValue: String); begin BeginEdit; FDataSet.FieldByName( FPropList.PropNames[aIndex] ).AsString := aValue; end; procedure TMDataSetProxy.SetVariant(aIndex: Integer; aValue: Variant); begin BeginEdit; FDataSet.FIEldByName( FPropList.PropNames[aIndex] ).Value := aValue; end; function TMDataSetProxy.ForEach: Boolean; begin Result := Not FDataSet.Eof; If FLooping Then Begin EndEdit; FDataSet.Next; Result := Not FDataSet.Eof; If Not Result Then Begin FDataSet.First; FLooping := false; End; End Else If Result Then FLooping := true; end; end.
其中TMPropList類是一個對RTTI的屬性操作部分功能的封裝。其功能就是利用Delphi在TypInfo單元中定義的一些 RTTI函數,實現為一個TPersistent的派生類維護其Published的屬性列表信息。代理類就通過這個屬性列表來取得屬性名,並最終通過這個屬性名與數據集中的相應字段進行操作。
TMDataSetProxy就是數據集代理類的基類。其最主要的部分就是在AfterConstruction裡創建屬性列表。
屬性的操作在這裡只實現了Integer, Double/Float, String, Variant這四種數據類型。如果需要,可以自己在此基礎上派生自己的代理基類實現其它數據類型的實現,而且這幾個已經實現的類型的屬性操作實現都被定義為虛函數,也可以在派生基類裡用自己的實現取代它。不過對於不是很常用的類型,建議可以定義實際的代理類時再實現。比如前面的例子中,假設 TDateTime不是一個常用的類型,可以這樣做:
TDSPEmployee = class(TMDataSetProxy) protected function GetDateTime(const Index: Integer): TDateTime; procedure SetDateTime(const Index: Integer; const Value: TDateTime); published Property EmployeeID : Integer Index 0 Read GetInteger Write SetInteger; Property FirstName : String Index 1 Read GetString Write SetString; Property LastName : String Index 2 Read GetString Write SetString; Property BirthDate : TDateTime Index 3 Read GetDateTime Write SetDateTime; end; { TDSPEmployee } function TDSPEmployee.GetDateTime(const Index: Integer): TDateTime; begin Result := TDateTime( GetVariant( Index ) ); end; procedure TDSPEmployee.SetDateTime(const Index: Integer; const Value: TDateTime); begin SetVariant( Index, Value ); end;
這樣下面就可以直接把BirthDate當作TDateTime類型使用了。
另外,利用這一點,還可以為一些自定義的特別的數據類型提供統一的操作。
另外,在所有的SetXXX之前都調用了一下BeginEdit,以避免忘記使用DataSet.Edit導致的運行時錯誤。
ForEach被實現成可以重復使用的,在每次ForEach完成一次遍歷後,將當前記錄移動最第一條記錄上以備下次的循環。另外,在Next之前調用了EndEdit,自動提交所作的修改。
這個數據集對象化方案是一種很簡單的方案,現在存在的最大的一個問題就是屬性的Index參數必須嚴格按照屬性在定義時的順序,否則就會取錯字段。這是因為Delphi畢竟還是一種原生開發語言,調用GetXXX/SetXXX時區別同類型的不同屬性的唯一途徑就是通過Index,而這個 Index參數是在編譯時就確定地傳給函數了,並沒有一個動態的表來記錄,所以只能采用現在這樣的方法來將就。