TOCIObject properties: "late object creation"

greenspun.com : LUSENET : OCI Best Practices : One Thread

TOCIObject sub-classes, such as TLegalObject, have properties that represent data on the sub-system's primary table and associated tables. The interface to data on the primary table is usually implemented using TFundamentalObjects, such as TIntegerObject, TBooleanObject, TDateTimeObject, etc.

When the TOCIObject sub-class is first created there is no need to initially populate the properties with data. Instead, we use a technique called "late object creation" to delay the I/O until a calling module references a property. Furthermore, we also code these classes so that when we do I/O for a table all properties associated with the table are populated.

For example, TMiscellaneousIndividualObject has two properties: DOB and SSN. Both of these fields are found on the MSC_INDIV table. When the object is created, InitializeObject simply fills query parameters with the ID of the object:

procedure TMiscellaneousIndividualObject.InitializeObject; begin inherited; QueryMSC_INDIV.ParamByName('id_msc').AsString := ID; end; So how do we get data into the properties? We accomplish this by associating a getter with each propery: TMiscellaneousIndividualObject = class(TMiscellaneousAbstractObject) private ... FDOB: TDateTimeObject; FSSN: TStringObject; ... function GetDOB: TDateTimeObject; function GetSSN: TStringObject; ... public ... property DOB: TDateTimeObject read GetDOB; property SSN: TStringObject read GetSSN; ... end; // class Each getter is coded like GetDOB: function TMiscellaneousIndividualObject.GetDOB: TDateTimeObject; begin if (FDOB = NIL) then begin FDOB := TDateTimeObject.CreateWithOwner(Self, Now); FDOB.Broadcaster.AddListener(FieldChangedListener); QueryMSC_INDIV.Open; FDOB.Value := QueryMSC_INDIVDOB.AsDateTime; end; Result := FDOB; end; By doing this the object isn't created until some module references it. Note that the property doesn't have a setter. That is because the property is read-only. The property's VALUE can change, but not the reference to the fundamental object.

The next question is: how do we ensure that all relevant properties are filled with data when the query is opened? We do this by coding an AfterOpen event handler for the query:

procedure TMiscellaneousIndividualObject.QueryMSC_INDIVAfterOpen(DataSet: TDataSet); begin DOB.Value := QueryMSC_INDIVDOB.AsDateTime; SSN.Value := QueryMSC_INDIVSSN.AsString; end; This works recursively, although we avoid an infinite loop becuase the AfterOpen event handler is only run when the query's Active property goes from FALSE to TRUE so the AfterOpen is only run the first time Open is encountered.

The sequence of events is:

  1. Some line of code references DOB, which runs GetDOB.
  2. GetDOB determines that the private instance variable FDOB is NIL.
  3. FDOB is assigned an instance of TDateTimeObject.
  4. The query is opened, which as a side effect, runs the AfterOpen event handler. AfterOpen assigns a value to DOB, which recursively runs GetDOB.
  5. This time the object exists, so GetDOB simply returns a reference to the object.
  6. AfterOpen then references SSN, which runs GetSSN.
  7. GetSSN creates an object and stores the reference in FSSN, then tries to open the query. Since the query is already open at this point, the AfterOpen event handler is not run. The getter assigns data to its Value property, and finishes, returning the SSN reference to the original call to AfterOpen, which finishes making the assignment to the object's Value property (the same assignment!)
Note that for which ever property is first referenced the assignment to the Value property is made twice. Our fundamental object Value setter makes sure that data is only assigned if it has changed, so this redundant assignment has no effect.

Despite the redundant assignment the example given above is good because the getters work whether or not we use the AfterOpen event. If you knew you were always going to use the AfterOpen, and you knew none of the fields would be removed from the table (as maintenance is done to the database) you could code the getter:

function TMiscellaneousIndividualObject.GetDOB: TDateTimeObject; begin if (FDOB = NIL) then begin FDOB := TDateTimeObject.CreateWithOwner(Self, Now); FDOB.Broadcaster.AddListener(FieldChangedListener); QueryMSC_INDIV.Open; // Fill this and other properties as a side-effect end; Result := FDOB; end; This saves you some typing, but may lead to bugs as the database is maintained.

-- Anonymous, July 07, 1998

Answers

Point of clarification: writing the AfterOpen event handler is REQUIRED by standard. You must always code an AfterOpen event which assigns property values to the values found in the newly opened result set.

For example:


procedure TMiscellaneousIndividualObject.QueryMSC_INDIVAfterOpen(DataSet: TDataSet);
begin
  DOB.Value := QueryMSC_INDIVDOB.AsDateTime;
  SSN.Value := QueryMSC_INDIVSSN.AsString;
end;


-- Anonymous, July 15, 1998

Moderation questions? read the FAQ