In the article "Strong DELPHI RTTI - A discussion on the need to understand multiple development languages", I said that I used DELPHI's RTTI to implement simple objectification of data sets. This article will introduce my implementation method in detail.
Let’s start with a simple example: Suppose there is an ADODataSet control that connects to the Roswen database, and SQL is:
select * from Employee
Now you need to display the four fields EmployeeID, FirstName, LastName, and BirthDate in its content into the ListView. The traditional code is as follows:
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;
There are several main problems here:
1. First of all, there are a lot of codes that are very verbose. For example, FieldByName and AsXXX, etc., especially AsXXX, you must always remember what type of each field is, which is easy to make mistakes. Moreover, if some incompatible types cannot be automatically converted, errors will not be found until runtime.
2. You need to process the current record movement in the loop yourself. Like Next above, otherwise a dead loop will occur once you forget it. Although this problem is easy to detect and deal with, programmers should not be entangled with such small details.
3. The most important thing is that the field name is passed through the String parameter. If it is written incorrectly, it will not be discovered until the runtime, which increases the possibility of potential bugs, especially if the test does not completely cover all FieldByName, it is likely to make such The problem will only appear if it is delayed to the customer. This kind of wrong field names is easy to happen, especially when the program uses multiple tables, it is easy to confuse the field names of different tables.
In this era ruled by OO, when we encounter operations related to data sets, we still have to often fall into the details of the relational database mentioned above. Of course, there is also a way to get rid of them now, that is, O/R mapping, but O/R mapping is too different from traditional development methods after all, especially for some small applications, there is no need to exaggerate it. In this case, , all we need is a simple dataset objectification scheme.
Inspired by Java and other dynamic languages, I thought of using DELPHI's powerful RTTI to implement this simple dataset objectification scheme. The following is the dataset objectified application code that implements the same functions as traditional code:
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( 'yyy-mm-dd', TDateTime( emp.BirthDate ) ) ); End; Finally emp.Free; End; end;
The usage is very simple. The most important thing is to first define a proxy class, which uses the Published attribute to define all fields, including their types, and then you can manipulate the data set in an object way. This proxy class is derived from TMDataSetProxy, which uses RTTI to implement the mapping from attribute operations to field operations. When using it, just simply use the corresponding unit. The implementation units of this class will be explained in detail below.
On the surface, there is an extra proxy class that defines the data set, which seems to have more code, but this is a one-time thing, especially when the program needs to reuse the same structure of data sets many times, the code will be made. The amount is greatly reduced. What's more, the definition of this proxy class is very simple. It just defines a series of attributes based on the field name and field type, without any implementation code. The attribute access functions GetXXX/SetXXX used are all implemented in the base class TMDataSetProxy.
Now let’s look at the loop corresponding to the original code:
1. FieldByName and AsXXX are not needed, and they have become attribute operations on proxy classes. Moreover, the type of attribute corresponding to each field has been defined before, so you don’t need to consider what type it is every time you use it. of. If the wrong type is used, an error will be reported during compilation.
2. Use a ForEach to perform record traversal, and no longer have to worry about forgetting the vicious loop caused by Next.
3. The biggest advantage is that the field name becomes a property, so you can enjoy the benefits of field name verification at compile time. Unless the field name is written incorrectly when defining the proxy class, it can be discovered at compile time.
Now start discussing TMDataSetProxy. The code of its implementation is as follows:
(************************************************* **********************Dataset proxy implemented with RTTI can simply objectify the dataset. Copyright (c) 2005 by Mental Studio.Author: RaptorDate : Jan.28-05****************************************** *********************)unit MDSPComm;interfaceUses 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; began 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; began 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);beginBeginEdit; FDataSet.FieldByName( FPropList.PropNames[aIndex] ).AsString := aValue;end;procedure TMDataSetProxy.SetVariant(aIndex: Integer; aValue: Variant); began BeginEdit; FDataSet.FieldByName( FPropList.PropNames[aIndex]).Value := aValue;end;function TMDataSetProxy.ForEach: Boolean; began 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.
The TMPropList class is an encapsulation of some functions of RTTI's attribute operation. Its function is to use some RTTI functions defined by DELPHI in the TypInfo unit to implement a TPersistent derived class to maintain its Published property list information. The proxy class obtains the attribute name through this attribute list, and finally operates through this attribute name and the corresponding fields in the dataset.
TMDataSetProxy is the base class of the dataset proxy class. The most important part is to create a property list in AfterConstruction.
The operation of attributes only implements four data types: Integer, Double/Float, String, and Variant. If necessary, you can derive your own proxy base class on this basis to implement the implementation of other data types. Moreover, the attribute operation implementations of these implemented types are defined as virtual functions, and you can also use it in the derived base class The implementation of the replacement of it. However, for types that are not very commonly used, it is recommended that you define the actual proxy class before implementing it. For example, in the previous example, assuming that TDateTime is not a commonly used type, you can do this:
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;In this way, you can directly use BirthDate as the TDateTime type.
In addition, taking advantage of this, it is possible to provide unified operations for some custom special data types.
Also, BeginEdit was called before all SetXXX to avoid runtime errors caused by forgetting to use DataSet.Edit.
ForEach is implemented to be reusable. After each ForEach completes a traversal, the current record is moved to the first record for the next loop. In addition, EndEdit is called before Next to automatically submit the changes.
This dataset objectification scheme is a very simple solution. The biggest problem now is that the Index parameters of attributes must be strictly in the order of the attributes when they are defined, otherwise the wrong field will be taken. This is because DELPHI is still a native development language after all. The only way to distinguish different properties of the same type when calling GetXXX/SetXXX is through Index, and this Index parameter is passed to the function accurately at compile time, and there is no dynamic , so you can only use the current method to register.