Object Persisten Framework in Delphi
Object Persisten Framework in Delphi
December 6, 2006
Abstract
In the series about Object Persistence Frameworks for Object Pascal, InstantOb-
jects is the first framework to be investigated. The architecture of this free package is
discussed and investigated using a small contacts application.
1 Introduction
InstantObjects is a free framework for Object Persistence for the Object Pascal language. It
goes back a long way: in a previous life, it was the property of a commercial company, but
was released to the public, and ever since it is managed by a group of enthousiasts. It has
been around for quite a while now, and can therefore be considered quite stable and usable.
The current version is 2.0, and it can be downloaded from the InstantObjects website:
http://www.instantobjects.org/
Installation is rather straightforward (more about this below) and it comes with several
demonstration projects, and a help file.
InstantObjects can store objects in many databases: it comes with several so-called con-
nection brokers, which allow to store data in a multitude of databases: NexusDB, BDE,
IBX, DBX, UIB, ADS, ISAM, ZeosDBO and ADO. It also features an XML broker, which
stores the data in XML files.
To create the classes which should be stored in the database, InstantObjects comes with a
class editor dialog. It allows to define the properties that should be made persistent, and
specifies some options for storage in the database. It does not allow editing of methods,
only properties. The class editor dialog interacts with the Delphi IDE: it creates and modi-
fies the code for all the properties.
The presentation layer works through standard Data-Aware controls. This has the advan-
tage that all of the data-aware controls out there can be used with InstantObjects, but also
has all drawbacks related to DB-Aware controls. Of course, the presentation layer does not
need to be used.
InstantObjects creates the database for the model, in any of the supported databases. Since
applications and models evolve through time, InstantObjects also offers a mechanism to
update the database from one version of the model to the next: the ’evolve’ feature.
All these features make InstantObjects an easy-to-use object persistence framework, and
any Delphi programmer should be able to start to work with it in no time.
1
2 Installation in Delphi
InstantObjects comes as a series of packages which must be installed in the IDE. The IDE
needs to have TDataset support, so not all versions of Delphi are possible (more specifically,
the free versions will not work). The packages are located in the Source subdirectory of the
distribution. There are 3 sets of packages:
1. The InstantObjects core packages: Located in the Core and Design subdirectories
of the Source directory. The design package installs InstantObjects support in the
IDE. These packages must always be installed.
2. The database brokers: this is a series of packages, one for each database access
engine in Delphi. Each of them handles the connection to a database. Obviously,
only the ones for which the appropriate database components are installed should be
installed, i.e. it makes no sense to install the ZeosDBO connector if ZeosDBO is
not installed. The XML engine can always be installed. Each package will install a
TInstantConnector descendent in the component palette.
3. Catalogs read metadata from the database engine. Currently catalogs for Interbase
and MS-SQL are shipped with InstantObjects.
3 InstantObjects Architecture
InstantObjects is built around several basic classes:
TInstantObject This is the base class for all persistent objects. Any object that needs to
be persistent in an InstantObjects model, descends fro TInstantObjects.
TInstantConnector This class is never used directly, but a lot of descendents of this class
exist: each of these descendents encapsulates the access to a database. For almost
each database technology present in Delphi, a corresponding TInstantConnector
descendent exists.
TInstantSelector This class allows to retrieve a set of classes from the database, and rep-
resents (’exposes’) them as records in a TDataset. This is what allows to use
DB-aware controls to edit the objects.
TInstantExposer This class allows to edit a single object (or an attribute of an object that
is a list of objects) as a record in a TDataset.
Many more classes exist in InstantObjects, some auxiliary components are even installed
on the component palette, such as the following.
2
TInstantDBEvolver This component allows to adapt a database from one version of a
model to the next version.
Create creates a new instance. The database connector can be specified in AConnector.
If none is specified, the default connector is used. (This is the TInstantConnector
in the application whose property IsDefault is set to True).
Retrieve With this constructor, a new instance is created, and the attributes are fetched
from the database, using AObjectID as the unique ID for the instance. If the ID
cannot be found in the database, then an empty instance is created if CreateIfMissing
is True.
Clone This creates a new instance and clones all attributes from the Source instance.
Needless to say, the classes should be compatible. Here also, a connector can be
specified.
Id this is the unique ID of this object. By default, this is a string of 32 characters and is
filled with A GUID. (although this can be changed).
IsChanged a boolean property indicating whether the object has any changes which have
not yet been written to the database.
RefCount Shows how many times the object is referenced by other objects in the applica-
tion. If the refcount is zero, then the object can safely be destroyed.
Owner if the object is owned (i.e. it’s a part or parts attribute of another class) then this
property returns the owning object.
Dispose Removes the instance from the database, but not from memory. This means that
the object can be stored again in the database (but with a different ID). Note that
Destroying or freeing the object is not the same as disposing it.
Abandon Calling this will break all connections with the database: the object can no
longer be stored or disposed.
There are many more methods and properties but these are the main ones. All the others
are documented in the help file of InstantObjects.
3
Figure 1: The InstantObjects Model Explorer
Since each class in a model will descend from this class, it’s important to know the above
methods.
There are many more classes in InstantObjects which are involved in persistence and
database mappings, but normally one is not confronted with them: they are used inter-
nally, and are automatically handled by InstantObjects. The classes presented here should
be the only classes that one encounters in a vanilla InstantObjects application.
4
Class Name the name of the class, obviously.
Base class is the class from which the new class should be descended. All classes used
in an InstantObjects application should at least descend from TInstantObject,
which is the base class for persistent objects.
Unit This is the name of one of the units that were selected to contain the model classes:
The model editor will insert the class declaration and basic implementation in that
unit.
Persistence determines how the data for this class will be stored. This can be either
Stored, in which case the data will be stored in a table, or Embedded. Embed-
ded storage means that the data will be stored with the data of the object that uses
this class. For example, the TAddressType which indicates the type of an address
(it’s a property of the TAddress class) is a separate class, but is stored embedded,
which means that the data is stored together with the TAddress data.
Storage Name is the name of the table in which the data for this class will be stored. Note
that the same table can be used to store data of more than 1 class: InstantObjects can
differentiate between the objects.
After this, the storage of the class is determined: InstantObjects knows enough to handle
storage for the class. To determine which data should be stored, the attributes (or properties)
of the class must be defined. This happens also in the class editor dialog, on the second tab.
A simple list of attributes is displayed, and attributes can be added, modified or deleted.
For each attribute, several things must be specified:
Name The name of the attribute. this can be any valid pascal identifier.
Type The type of the attribute: this can be a basic type, or a reference to another object
(more on this below). In some cases (notably strings), the maximum size must be
indicated.
Storage name This is the name under which the attribute will be stored (a field name).
Storage kind determines how references to other objects are stored. More on this below.
Part this is for the case when the referenced object exists only as a part of the current
object, and will never be instanced by itself: it is ’part’ of the owning class. For
instance, in the contacts application, the TAddressType exists only as an attribute
of a TAddress. (a one-way relationship)
Parts Has the same meaning as TPart, but there are multiple objects. In the contact
application, TAddress exists as a Parts of TContact.
Reference should be used when an object refers to another objects, which can be instanti-
ated by itself. In the case of the contacts application, TCity refers to TCountry:
the list of countries can be maintained by itself. (a two-way relationship)
References is similar to Reference, but more than one object can be referenced.
5
Figure 2: The attributes editor
Which reference should be chosen depends entirely on the needs of the model. Roughly,
one could say that parts are owned by the source class: if the source class instance is
deleted, it’s parts will also be deleted. For a reference, the referenced class will not be
deleted.
Note that InstantObjects does not enforce referential integrity at the database level (al-
though this feature is planned). This means that it is possible to delete an object while there
are still references to it, leaving the database in an inconsistent state. It is of course possible
to create the necessary foreign keys manually.
For regular (primitive) attributes, the storage always happens in the table of the class of
which the attributes are part. For references to other objects (parts or references) the storage
can be done in 2 ways. Which way is used, depends on the value of Storage Kind. This
can have one of 2 values:
embedded the parts or references are stored in a blob field in the table of the referring
class.
external here, the parts are stored in a separate table, and the links between part and own-
ing object are stored in an intermediate table. The same is true for references: the
references are stored in an intermediate table.
Other than the definition and storage options for an attribute, which constitute the main part
of the attribute’s definition, InstantObjects also allows to define some other characteristics
of the attribute.
Visibility the pascal visibility of the attribute. To be usable in the presentation layer, the
visibility should be at least ’Published’.
6
Edit Mask an edit mask to be used when editing this attribute.
Valid characters allowed characters when providing a value for this attribute.
Display width the preferred display with for display in DB-Aware grids.
When all the classes and their attributes have been entered, InstantObjects can create the
database. InstantObjects completely controls the creation of the database, based on the
definitions of the classes, attributes and their storage properties. A database can be created
for all broker types that were installed in the IDE.
For the purposes of the article, a UIB (Unified Interbase) connection is used: it’s very
lightweight. The following objects are defined in the modeler for the contact application:
TAddressType the type of an address, it has a single string attribute Name, and it’s persis-
tence type is embedded.
TCity Has 2 string attributes Zip and Name, a reference to a TCountry, an attribute of
type TAddressType (a part) and is stored.
TAddress consists of several string attributes: Street, Nr, Telephone1 and Telephone2
as well as Fax, plus a reference to a TCity. it’s persistence is set to stored.
TContact has several string attributes: FirstName, LastName, Email and Mobile.
The Addresses attribute is a parts, referring to a TAddress object.
TBusinessContact descends from TContact and has 2 string attributes: Title and Company.
The storage name is set to Contact
2. By contrast, TCity is stored separately: this means that each address refers to a city.
The application can maintain and set a list of cities. It also means that a new TCity
must be created before it can be referred to in an address.
4. The TBusinessContact object has the same storage name as TContact: this
shows that it is possible to store multiple classes in 1 table.
6. The telephone, fax and mobile numbers could have been defined as a separate TTelephone
class, and stored embedded in the owning classes. This was not done, below the con-
sequences of this decision will be shown.
7
It’s instructive to have a look at the tables which InstantObjects creates based on these
definitions. Opening the database in a firebird management tool (such as FlameRobin),
reveals the following tables:
Address this table keeps the addresses. It contains columns CityClass and CityID
which form the reference to the TCity instance. The AddressType attribute
(though a class in itself) is stored in the AddressType field.
City this table keeps the cities. It contains columns CountryClass, CountryID wich
hold the reference to the TCountry instance.
Contact This table holds the TContact and TBusinessContact data. The Addresses
attribute is not stored in this table.
Contact_Adresses This table holds the references between TContact and TAddress
instances.
Country This holds the TCountry data.
PersonalContact this table holds the attribute (spousename) of the TPersonalContact
class. The attributes inherited from TContact are stored in the Contact table.
Each of the tables, except the Contact_Adresses table, contains 3 standard InstantO-
bject fields:
Class contains the classname of the stored object. This allows to store data of multiple
classes in 1 table (especially important for Descendent classes). It’s length is 32
characters.
ID a unique ID, identifying the instance. Together with the Class field, the ID field
forms the primary key of the table.
UpdateCount this field is incremented each time the instance writes changes to itself to
disk: this way, changes to an object in another application can be detected: if the
object needs to write itself to disk, it first checks the update count on disk: if it
matches with the update count in memory, it’s safe to write the changes to disk. If
the count does not match, it knows the object’s data have been changed by another
application, and appropriate action can be taken.
8
TAddress = class(TInstantObject)
_Telephone1: TInstantString;
_Telephone2: TInstantString;
private
function GetTelephone1: string;
function GetTelephone2: string;
procedure SetTelephone1(const Value: string);
procedure SetTelephone2(const Value: string);
published
property Telephone1: string read GetTelephone1 write SetTelephone1;
property Telephone2: string read GetTelephone2 write SetTelephone2;
end;
Note that the _telephone1 field is of type TInstantString, and therefor the de-
clared type of the Telephone1 property (String) does not match, and a read and write
specifier must be used to get or set the property value. Luckily, InstantObjects generates
all needed code.
The write procedure looks as follows:
This method is generated by the InstantObjects modeler. In this method, some business-
rules can be implemented. For instance, code could be inserted to check whether the value
is a valide telephone number, and refuse any invalid telephone numbers:
begin
If (S<>’’) then
begin
{Insert code to check validity. Raise exception if S
does not contain a valid telephone number}
end;
end;
Note that the empty value should be accepted as a valid telephone number.
The above code presents the consequence of the decision to define all phone numbers as
a string attribute, instead of defining it as a separate class: In each write specifier for an
attribute (property) that represents a telephone number, the call to CheckPhoneNumber
has to be inserted. There are 4 occurrences in the contacts model, which all need to be
modified.
If, by contrast, a separate class TTelephone had been defined with a single attribute
Number, and all attributes had been declared of type TTelephone (with embedded stor-
age), then the check procedure could have been implemented in the write specifier for
9
TTelephone.Number, and all telephone numbers throughout the model would have
automatically been checked.
Obviously, the above is a simple example. More complex examples of business logic can
be found in the examples accompagnying InstantObjects. For the purpose of this article,
no other business rules will be implemented.
Both classes have a property ’ObjectClassName’ which is the classname of the object
which they will edit. (i.e. the name of a single instance). The two classes will be de-
scribed more in detail:
The TInstantSelector can be used whenever a selection of instances of a certain
object must be shown to the user: in a grid, in a lookup list. It allows to enter a SQL like
statement. It must be of the form
The names used must be a class name (not the storage name) and the attributes in the class.
The ANY keyword tells InstantObject that also descendent classes must be included. For
instance:
10
foObjects this will create a field for object references (be it parts or references) which
represents the ’Self’ of the object. This field will be of type integer, and contains the
pointer to the object. (note that this is not correct for 64-bit platforms). The Self of
an exposed class will also be included.
foThorough specifying this option will include fields of referenced objects in the fieldlist.
For instance. Note that this is independent of the foObjects
foRecurseNesting This option controls the recursive nesting of TDatasetField fields. Parts
and references are included in a TDatasetField containing a TDataset of their own:
if foRecurseNesting is specified, this process continues recursively (and may
lead to a lot of fields and data)
The instantselector is a Read/Write dataset: all objects in the dataset can be modified, and
the changes will be written back to the dataset. The dataset can be put in read-only mode,
but the mechanism in which changes are handled can also be controlled with the Options
property. It’s again a set with the following items:
eoAutoApply Changes made to the exposer’s current object (Subject) or objects contained
in the current object should be applied automatically. For TInstantObject de-
scendants this means that Store is called.
eoSyncEdit If this option is specified, then any changes that are made to an object directly,
are immediatly reflected in the dataset, even if it is in edit mode. (The default be-
haviour is not to modify the contents of the dataset buffer if it is in edit mode). This
option will be demonstrated later.
amContent in this mode, a parts or references attribute of an object is the source of the
data. each object in the parts or references list is a row in the dataset. The at-
tribute which should be examined is determined by the Containername prop-
erty. For example, in case the addresses of a TContact instance should be exposed
as a TDataset, the Subject property of the TinstantExposer would be set
to the TContact instance, the ContainerName would be Addresses. The
ObjectClassName would be set to TAddress, the class of 1 element in the list
of addresses.
amObject in this mode, the subject is treated as an object (or list of objects) which should
be displayed: in case of a list, the
11
eoDeferInsert By default, objects that are contained within another object (i.e. parts or
references) are immediatly inserted in the owning class as soon as a new row is
inserted in the dataset. If this option is on, then the objects are only inserted in the
owning class when the row is posted.
this will fetch a list of all contacts. The foObjects option should be set.
IEAddresses this is a TInstantExposer instance which is coupled with it’s MasterSource
property to ISContacts. Its ObjectClassName property is set to TAddress
and its ContainerName is set to Adresses. The Options property should
include eoSyncEdit.
ISCountries is a simple TInstantSelector with command
12
ISCitySearch is a simple TInstantSelector, again with command:
and again, foObjects should be included in the fieldoptions. This dataset will be
used to define the fields.
For all these datasets, persistent fields can be created in the fields editor; All display prop-
erties can be set; No database connection is needed for this: InstantObjects can retrieve all
needed information for this in the model.
With this, all is ready to code the forms. For the main form, there is not much to code, all
that needs to be done is to hook up the grid to the ISCOntacts dataset.
Furthermore, a method to open (and close) the database connection should be made:
procedure TMainForm.OpenDatabase;
begin
ContactsDataModule.ICContacts.Connected:=True;
ContactsDataModule.ISContacts.Open;
end;
This method can be called from the OnShow event handler of the form.
To maintain the list of cities and countries, 2 menu entries are made under the ’System’
menu. The OnClick handler of these menu entries have almost the same code: they open
a new form (modal) which shows the list of countries and cities:
The countries and cities forms hold no surprises. In fact, they can be copied almost entirely
from the traditional contacts application. All that must be done is to set the datasource
property of the grids to the correct datasources on the datamodule.
In the cities form, however, a problem which also existed in the traditional approach ap-
pears: in the grid showing the cities, a way must exist to choose the country in which the
city is located: obviously, entering the ID of a TCountry object will not do. In the tradi-
tional approach, this was solved with a lookup field: Delphi then knows how to display a
dropdown with the elements from the lookup dataset.
13
For InstantObjects, the same technique can be applied. It also reveals the reason why the
foObjects option had to be specified in the ISCities and ISCountries InstantSe-
lectors: In the ISCities a lookupfield is created (let’s call it DisplayCountry) which
uses as the KeyFields property the Country field, and has as LookupKeyField the
Self field from the ISCountries dataset. Both these fields are included thanks to the
foObjects fieldoption. The LookupResultField is set to Name, the name of the
country.
By displaying the DisplayCountry field instead of the Country.Name field in the
grid with fields, the grid allows to select a country from the list of countries: This is com-
pletely similar to the technique used in the traditional database programming. Note that it
does violate the idea that the data layer and GUI layer should be separated.
Double-clicking in the grid with contacts should open a new form which allows to edit
contact data and which allows to enter several addresses for the contact person. The same
action can be performed from a menu, with the following simple method:
Procedure TMainForm.ShowContacTDetails;
begin
If Assigned(AddressesForm) then
begin
AddressesForm.show;
AddressesForm.SetFocus;
end
else
begin
AddressesForm:=TAddressesForm.Create(Application);
AddressesForm.Show;
end;
end;
The addresses form looks exactly the same as the one in the traditional approach, and is
hooked up to the ISContacts dataset on the datamodule. The grid is hooked up to the
IEAddresses dataset, and will show 1 row per address of the contact person.
To select the city of an address, the same technique could be used as for the reference to
a country in the cities form: using a lookup field. However, this time the problem will
be solved slightly different: this will allow to demonstrate 2 important aspects of the fact
that the application is actually programmed using objects, and is not simply a database
application.
For the column of the City.Name field, the Buttonstyle property is set to cbsEllipsis.
This has the following effect (standard for a Delphi DBgrid): If the City field is edited, a
button with an ellipsis on it will be displayed. When the button is clicked, the DBGrid’s
OnEditButtonClick event handler is called. In the event handler, the following code
is entered:
The ShowSelectCityForm will show a new form in which a city can be selected, or
a new city can be defined if the city is not yet present in the database. The procedure is
14
coded as follows:
procedure TAddressesForm.ShowSelectCityForm;
Var
CA : TAddress;
begin
ContactsDataModule.DSAddresses.Edit;
CA:=ContactsDataModule.IEAddresses.CurrentObject as TAddress;
With TSelectCityForm.Create(Self) do
try
if ShowModal=mrOK then
CA.City:=SelectedCity;
Finally
Free;
end;
end;
First, the dataset is set in editmode (this could cause an insert in the dataset). Then
the address object which is being edited, is fetched and stored in a local variable. The
TSelectCity form is created and shown. If the user closes that form using the OK but-
ton, then the SelectedCity property of that form is stored in the City property of the
currently edited Address object.
This shows that
1. The datasets do actually represent objects which are actually being edited. It is easy
to forget this.
2. The objects can be manipulated without using the dataset mechanism. This explains
also why the eoSyncEdit option had to be set for the IEAddresses dataset:
Otherwise, the changed city would not be visible at once in the dataset ! (it’s in edit
mode)
The PostChanges is needed for the ISContacts to post the changes to the addresses,
which are stored in a TDatasetField of ISContacts: While the Contact data itself was
not changed, the addresses have changed. The call to PostChanges makes sure that
these changes are also posted to the database.
Remains to code the TSelectCityForm. This form contains a page control with 2
tabsheets: one which allows to search for a city based on name and zip code, and the
other which allows to create a new city. When the user clicks OK, the currently active tab
determines which city is returned.
The code for the select tab is quite simple. It contains 2 edit fields, a ’Search’ button, and
a DBGrid. The DB grid is coupled to the ISCitySearch dataset on the datamodule.
When the user clicks the Search button, the following code is executed:
15
procedure TSelectCityForm.BSearchClick(Sender: TObject);
begin
DoSearch;
end;
procedure TSelectCityForm.DoSearch;
Var
S : String;
begin
With ContactsDatamodule.ISCitySearch do
begin
Close;
Command.Clear;
Command.Add(’SELECT * FROM TCity’);
Command.Add(’WHERE’);
S:=’’;
If (EZip.Text<>’’) then
S:=’(Zip like ’’%’+EZip.Text+’%’’)’;
If (EName.Text<>’’) then
begin
If (S<>’’) then
S:=S+’ AND ’;
S:=S+’(Name like ’’%’+EName.Text+’%’’)’;
end;
Command.Add(S);
Open;
If Not (EOF and BOF) then
begin
GSearch.SetFocus;
BSearch.Default:=False;
BOK.Default:=True;
end;
end;
end;
This code is quite clear: depending on the values the user has entered, the Command
property of the ISCitySearch selector is coded with a simple search mechanism. If
the query returns a result, the focus is set on the grid (so the user can select a city in the
resulting set) and the OK button is made default (allowing the user simply to hit the enter
key to select his city).
The second tab allows to enter a new city, in case the search has not yielded any re-
sults. It contains no code, only some data-aware edit fields: one for a ZIP code, one
for a name, and a lookup combobox to select a country. These 3 controls are hooked
to a TInstantExposer instance: IENewCity, which is in amObject mode. It’s
ObjectClassName property is set to TCity.
When the user changes the active tabsheet, the following code is executed:
16
If Assigned(IENewCity.Subject) then
begin
IENewCity.Close;
IENewCity.Subject.Destroy;
end;
end
else
begin
IENewCity.Subject:=TCity.Create;
IENewCity.Open;
CBCountry.ListSource.Dataset.Open;
end;
end;
If the active page is the TSSelect page, then the IENewCity.Subject property is
examined. If it contains a TCity instance, this instance is destroyed. In the case the active
page is TSNew, then the Subject property is filled with a newly created TCity instance,
and the dataset is opened, together with the lookup dataset.
When the user presses the OK button, the following code is executed:
If the active page is the TSSelect page, then the currently selected city object is stored
for later reference. If the active page is the TSNew page, the newly created city object is
stored in the database by means of a PostChanges call, and is stored for later reference.
At that point, the form will close, and it’s SelectedCity property (which reads from
the FCity field) will be used to set the City attribute of the address being edited.
This shows that new objects can be created in code, and can be assigned to properties of
other objects: the resulting changes will be saved in the database, even if everything is
edited through the dataset exposers of InstantObjects.
With this, the contacts application is complete: it has the same functionality as the contacts
application created with traditional methods. The end result is visible in figure 3 on page
18
8 Conclusion
InstantObjects is a quite lightweight Object Persistence framework. Nevertheless, it is
fully functional, and is easy to understand; the gory details of persisting all objects are
nicely hidden from the programmer. It’s use of DB-Aware controls for the presentation
layer makes it extremely easy to create GUI’s for any model one wishes to create with it.
It also puts a lot of 3rd-part controls at one’s disposal. It’s simplicity comes with a price: it
currently lacks certain optimizations (loading partial property lists) which may cause some
17
Figure 3: The finished contacts application
performance degradation when large amounts of data must be treated. The fact that it needs
complete control over the database is easy, but can lead to problems when legacy data must
be used. However, for someone starting with OPF, InstantObjects is definitely worth a try;
any Delphi programmer should be very quickly up-to-speed with this nice tool.
18