Monday, July 9, 2012

From TParams to TDatasetRecord and beyond

It has been a while since my last blog post. due to many job problems and the economical crisis here at Hellas (Greece). Anyway, for those who have followed my previous posts on "A journey to TParams", i will try to get TDatasetRecord (the class derived from TParams) a bit further, by explaining some aspects of creating derived classes with dataset field mappings.

TDatasetRecord is the class created to to represent a single dataset record. In fact it does not just represents a record, it mainly holds and provides access to record field values even disconnected from the actual dataset. As a descentant of TParams it uses TParam object to fullfil these requirements. Field values can be accessed by using normal TParam(s) methods and properties or TDatasetRecord.Field method, ie:

  aRecord.FindParam('LastName').AsString; 
   //or
  aRecord.ParamByName('LastName').AsString; 
   //or
  aRecord.Field('LastName').AsString;

This is useful, if you agree, but what is so big deal with? As is for now, nothing big, nothing fancy, nothing ... except, if we could have something that really goes into object relational mapping.

First of all, we should be able to access the field as a property of the class and do something like this:

  aContactRecord.LastName.AsString;

So lets get in detail with Delphi and do it. 
Of course we have to define a class derived from TDatasetRecord specifically for our contact dataset entity as sample and then make some cooking deriving just a base class method and defining properties.


The key method to derive is FieldNames. FieldNames class method is intended to return a name from a predefined list of field names for a specific class derived from TDatasetRecord. Base class's implementation just returns an empty string, but derived ones may change it by overriding this method.

FieldNames method can be overriden to return a string -the fieldname- defined in a const array of strings based on it's index. This method is used by internal functionality of TDatasetrecord to build it's fields -TParam- structure  by forcing which fields will be taken in account. This ensures that the record class will always have the same fields structure and at specified positions in the list of fields (actually TParams collection). For more detail you may refer to the implementation of TDatasetRecord.CreateFields method.

Next method of TDatasetRecord to take into consideration is GetParam, which provides access to internal TParam objects based on FieldNames method. Here is it's implementation:

function TDatasetRecord.GetParam(Index: integer): TParam;
begin
  Result := ParamByName(FieldNames(Index));
end;

Now that we have a way to define the internal fields structure and a field object access method by index we can go on and define properties with index specifiers. Index specifiers allow several properties to share the same access method while representing different values and thus GetParam method of TDatasetRecord can do this job for our contact class properties and map fieldnames to TParam objects.

Now let's built it.
Suppose we have to deal with a dataset of contacts with the following fields available:

  • ContactID
  • FirstName
  • LastName
  • DateOfBirth
  • Father
  • Mother
  • Phones
  • Address
  • SocialSecurity
  • CompanyName
  • BankAccount
So, here is the interface section:

  TContactRecord = class(TDatasetRecord)
  private
    function GetFullName: string;
  protected
    class function FieldNames(Index: integer): string; override;
  public
    procedure UpdateDataset;
    procedure AppendDataset;
    property ContactID                : TParam index  0 read GetParam; //ContactID
    property FirstName                : TParam index  1 read GetParam; //FirstName
    property LastName                 : TParam index  2 read GetParam; //LastName
    property CompanyName              : TParam index  3 read GetParam; //CompanyName
    property Phones                   : TParam index  4 read GetParam; //Phones
    property Address                  : TParam index  5 read GetParam; //Address
    property FullName: string read GetFullName;
  end;

And the implementation section:

class function TContactRecord.FieldNames(Index: integer): string;
const Names: array[0..5] of string = (
             'ContactID',                  //0 ContactID
             'FirstName',                  //1 ContactName
             'LastName',                   //2 LastName
             'CompanyName',                //3 CompanyName
             'Phones'.                     //4 CompanyName
             'Address'                     //5 CompanyName
                    );
begin
 if (Index < Low(Names)) or (Index > High(Names)) then Result := ''
 else Result := Names[Index];
end;
 
function TContactRecord.GetFullName: string;
begin
  Result := Lastname + ', ' + FirstName;
end;


Notes:

  • Field position is irrelevant as TDatasetRecord maps it's fields by name, not by position. 
  • We may not want to include all data fields in our class.
  • I also have added a new property that returns the fullname of the contact by concatenating FirstName and LasName. Just a hint of extending the class...