分享
 
 
 

用DELPHI的RTTI实现数据集的简单对象化

王朝delphi·作者佚名  2006-01-09
窄屏简体版  字體: |||超大  

[Mental Studio]猛禽[Blog]

在《强大的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参数是在编译时就确定地传给函数了,并没有一个动态的表来记录,所以只能采用现在这样的方法来将就。

猛禽 Jan.28-05

 
 
 
免责声明:本文为网络用户发布,其观点仅代表作者个人观点,与本站无关,本站仅提供信息存储服务。文中陈述内容未经本站证实,其真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。
2023年上半年GDP全球前十五强
 百态   2023-10-24
美众议院议长启动对拜登的弹劾调查
 百态   2023-09-13
上海、济南、武汉等多地出现不明坠落物
 探索   2023-09-06
印度或要将国名改为“巴拉特”
 百态   2023-09-06
男子为女友送行,买票不登机被捕
 百态   2023-08-20
手机地震预警功能怎么开?
 干货   2023-08-06
女子4年卖2套房花700多万做美容:不但没变美脸,面部还出现变形
 百态   2023-08-04
住户一楼被水淹 还冲来8头猪
 百态   2023-07-31
女子体内爬出大量瓜子状活虫
 百态   2023-07-25
地球连续35年收到神秘规律性信号,网友:不要回答!
 探索   2023-07-21
全球镓价格本周大涨27%
 探索   2023-07-09
钱都流向了那些不缺钱的人,苦都留给了能吃苦的人
 探索   2023-07-02
倩女手游刀客魅者强控制(强混乱强眩晕强睡眠)和对应控制抗性的关系
 百态   2020-08-20
美国5月9日最新疫情:美国确诊人数突破131万
 百态   2020-05-09
荷兰政府宣布将集体辞职
 干货   2020-04-30
倩女幽魂手游师徒任务情义春秋猜成语答案逍遥观:鹏程万里
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案神机营:射石饮羽
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案昆仑山:拔刀相助
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案天工阁:鬼斧神工
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案丝路古道:单枪匹马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:与虎谋皮
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:李代桃僵
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:指鹿为马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:小鸟依人
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:千金买邻
 干货   2019-11-12
 
推荐阅读
 
 
 
>>返回首頁<<
 
靜靜地坐在廢墟上,四周的荒凉一望無際,忽然覺得,淒涼也很美
© 2005- 王朝網路 版權所有