程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> 更多編程語言 >> Delphi >> 用DELPHI的RTTI實現數據集的簡單對象化

用DELPHI的RTTI實現數據集的簡單對象化

編輯:Delphi
在《強大的DELPHI RTTI--兼談需要了解多種開發語言》一文中,我說了一下我用Delphi的RTTI實現了數據集的簡單對象化。本文將詳細介紹一下我的實現方法。

      首先從一個簡單的例子說起:假設有一個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參數是在編譯時就確定地傳給函數了,並沒有一個動態的表來記錄,所以只能采用現在這樣的方法來將就。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved