用DELPHI的RTTI实现数据集的简单对象化
[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参数是在编译时就确定地传给函数了,并没有一个动态的表来记录,所以只能采用现在这样的方法来将就。