在《强大的DELPHI RTTI--兼谈需要了解多种开发语言》一文中,我说了一下我用DELPHI的RTTI实现了数据集的简单对象化。本文将详细介绍一下我的实现方法。
首先从一个简单的例子说起:假设有一个ADODataSet控件,连接罗斯文数据库,SQL为:
select * from Employee
现在要把它的内容中EmployeeID, FirstName, LastName,BirthDate四个字段显示到ListView里。传统的代码如下:
//先要设置AdoDataSet的CommandText属性
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
//先要设置AdoDataSet1的CommandText属性
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.关于属性中的index specifier
index specifiers allow several properties to share the same access method while representing different values. An index
specifier consists of the directive index followed by an integer constant between -2147483647 and 2147483647. If a property
has an index specifier, its read and write specifiers must list methods rather than fields(如果使用了索引属性,则必须是存取
函数,而不是字段). For example,
type
TRectangle = class
private
FCoordinates: array[0..3] of Longint;
function GetCoordinate(Index: Integer): Longint;
procedure SetCoordinate(Index: Integer; Value: Longint);
public
property Left: Longint index 0 read GetCoordinate write SetCoordinate;
property Top: Longint index 1 read GetCoordinate write SetCoordinate;
property Right: Longint index 2 read GetCoordinate write SetCoordinate;
property Bottom: Longint index 3 read GetCoordinate write SetCoordinate;
property Coordinates[Index: Integer]: Longint read GetCoordinate write SetCoordinate;
...
end;
property Left: Longint index 0 read GetCoordinate write SetCoordinate; 表示属性Left使用
存取函数GetCoordinate, index 0 表示这个属性的索引是0, 并作为参数传入存取函数。
-----------------------------------------------------------------------------
2.改进
这个设计要求在建立TDSPEmployee对象时传入参数AdoDataSet1,里面必须填入CommandText,这个又涉及到数据表的细节了。
我觉得应该把Select字句封装在子类里面, 但留出Where字句的内容给客户(虽然封装不好,但灵活性较高)
所以在原先的定义中加了一个构造函数,多了一个criteria参数,就是where字句的内容
原先我想只传入criteria, 而不传入数据集,但发现要在类里面创建AdoDataSet,还要传入Connection,参数还是没有少,而且要动态生成会增加开销; 所以还是传入现成的DataSet为好,可以设个专用的AdoDataset, 传入后修改它的CommandText。
-----------------------------------------------------------------------------------------
代码如下:
TDSPEmployee = class(TMDataSetProxy)
public
constructor Create(aDataSet:TDataSet; criteria: String);
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;
constructor TDSPEmployee.Create(aDataSet:TDataSet; criteria: String);
begin
(aDataset as TAdoDataSet).CommandText:='select EmployeeID, FirstName, LastName, BirthDate from Employees' //select 字句是固定的,最后可以由上面的属性来生成
+ criteria;
inherited Create(aDataSet);
end;
------------------------------------------------------------------------------------------
在按Ctrl + Shift + C 来自动生成框架代码时,会在类的私有部分加上GetInteger这些函数的声明,要删掉它们,这是个bug,如下
TDSPEmployee = class(TMDataSetProxy)
private
function GetInteger(const Index: Integer): Integer;
function GetString(const Index: Integer): String;
function GetVariant(const Index: Integer): Variant;
procedure SetInteger(const Index, Value: Integer);
procedure SetString(const Index: Integer; const Value: String);
procedure SetVariant(const Index: Integer; const Value: Variant);
public
constructor Create(aDataSet:TDataSet; criteria: String);
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;