A quick guide to tiOPF
This guide is designed to be read in conjunction with the tiOPF 2 and the associated demos.
tiOPF can be downloaded from the tiOPF Web Site
What is tiOPF
tiOPF is a Object Persistence Framework. That is, it is a framework based around saving your objects to, and loading them from, databases and/or flat files.
According to the website:
tiOPF is an Open Source framework for Delphi & Free Pascal that simplifies the mapping of an object oriented business model into a relational database. The framework is mature and robust having been in use on production sites since 1999. It is free, open source, and available for immediate download with full source code.
Key Features
From the website:
Some of the key features of the tiOPF include:
- Lets you build an object oriented application that can swap databases with the flick of a command line parameter, conditional define or registry switch.
Currently we have the following persistence layers available:
- Interbase/Firebird via IBX
- Firebird via FBLib
- Firebird via ZeosLib (experimental)
- Oracle via DOA
- MS Access & MS SQL-Server via ADO
- Paradox via BDE
- XML via MSDOM or FPC's DOM
- CSV files
- TAB files
- There is also a lightning fast, custom XML persistence layer for local databases, and
- a HTTP/XML layer & proxy server for building remote systems that can connect through corporate firewalls.
- Supports Delphi 5, 6, 7, 2006, 2007, BDS 2006, RDS 2007, Free Pascal for Win32 and Linux.
- Family of abstract base classes for building a complex object model.
- 32 Persistent object aware components for building complex GUIs.
- Has a Model-GUI-Mediator implementation to make standard GUI components object aware. An alternative to the persistent object aware components.
- 1518 Unit Tests to guarantee stability;
- 160 pages of documentation to get you started.
- Newsgroup for support.
- Automated, daily builds and unit testing.
Support
What are the advantages?
- tiOPF provides database independence.
You can change databases by changing the persistence layer.
This can be as simple as changing a couple of lines of code.
- Your code can be more object orientated rather than record orientated.
What are the disadvantages?
- A heavy learning curve.
- tiOPF is more code orientated and less RAD than using databases and data aware controls.
How do I create a business object?
Business objects descend from TtiObject.
All TtiObject descendants have a unique Object ID with a property name of OID. This is of type integer, 64 bit integer or GUID. The OID is populated when a new object is created. The OID is saved and loaded automatically.
Other fields needing persistence are declared as published.
Object initialisation is done in CreateNew.
Example - taken from Demo_Collection:
- TClient = class(TtiObject)
- private
- FClientID: TClientID;
- FClientName: TClientName;
- public
- constructor CreateNew(const pDatabaseName: string = '';
- const pPerLayerName: string = '');
- override;
- published
- property ClientName: TClientName read FClientName write FClientName;
- property ClientID : TClientID read FClientID write FClientID;
- end;
-
- ...
-
- constructor TClient.CreateNew(
- const pDatabaseName: string = '';
- const pPerLayerName: string = '');
- begin
- inherited;
-
- ClientName:= 'TEST ' + DateTimeToStr(Now);
- ClientID:= IntToStr(GetTickCount);
- end;
Business objects are normally stored in a TtiObjectList descendant.
This can be as simple a declaration as:
- TClients = class(TtiObjectList);
Commonly however Add and Items are reintroduced to provide type safety
Example - taken from MastApp:
- TEmployees = class(TtiObjectList)
- private
- protected
- function GetItems(i: integer): TEmployee; reintroduce;
- procedure SetItems(i: integer; const Value: TEmployee); reintroduce;
- public
- property Items[i:integer]: TEmployee read GetItems write SetItems;
- procedure Add(pObject: TEmployee); reintroduce;
- published
- end;
New items are created with CreateNew
Example - taken from Demo_Collection:
- procedure TFormCollection.btnInsertRowClick(Sender: TObject);
- var LClient: TClient;
- begin
- LClient:= TClient.CreateNew;
- FClients.Add(LClient);
- LV.Refresh(LClient);
- end;
Items are deleted by setting the Deleted property to True.
Note that they are only removed when the object list is persisted.
How do I persist it?
Examples from MastApp, using an access database.
There are 6 steps:
- Include your persistence layer units and OID units:
- uses
- ...
- ,tiOID ,tiQuery
- ,tiOIDInteger ,tiQueryADOAccess
- Set your default persistence layer:
- gTIOPFManager.DefaultPerLayerName:= 'ADOAccess';
- Connect to the database:
- gTIOPFManager.ConnectDatabase('dbdemos.mdb', '', '');
- Set your persistence mapping:
This depends on what mapping you are using, Automapping, DB Independent or hard coded. This is covered in a later section. Automapping is the simplest.
An object is mapped as follows:
-
- gTIOPFManager.ClassDBMappingMgr.RegisterMapping(TEmployee, 'Employee', 'OID', 'EmpNo', [pktDB] );
- gTIOPFManager.ClassDBMappingMgr.RegisterMapping(TEmployee, 'Employee', 'LastName', 'LastName' );
- gTIOPFManager.ClassDBMappingMgr.RegisterMapping(TEmployee, 'Employee', 'FirstName', 'FirstName' ) ;
- gTIOPFManager.ClassDBMappingMgr.RegisterMapping(TEmployee, 'Employee', 'PhoneExt', 'PhoneExt' ) ;
- gTIOPFManager.ClassDBMappingMgr.RegisterMapping(TEmployee, 'Employee', 'HireDate', 'HireDate' ) ;
- gTIOPFManager.ClassDBMappingMgr.RegisterMapping(TEmployee, 'Employee', 'Salary', 'Salary' ) ;
- gTIOPFManager.ClassDBMappingMgr.RegisterCollection(TEmployees, TEmployee);
- Override Save and Read and make them public (for automapping it is sufficient to call the inherited handler):
- Txxxx = class(TtiObjectList)
- public
- procedure Read; override;
- procedure Save; override;
- end;
-
- ...
-
- procedure Txxxx.Read;
- begin
- inherited;
- end;
-
- procedure Txxxx.Save;
- begin
- inherited;
- end;
- Read/Save your object lists:
- FEmployees:= TEmployees.Create;
- FEmployees.Read;
- FEmployees.SortByProps(['LastName', 'FirstName']);
- ...
- FEmployees.Save;
How do I create a database?
You can create a dataset as you would normally, and then map your objects to it.
However you can also create a database in code using the persistence layer.
Example - from Demo_CreateDatabase:
- procedure
- TFormMainCreateDatabase.btnDatabaseExistsClick(Sender: TObject);
- var
- LPerLayer: TtiPersistenceLayer;
- begin
- LPerLayer:= gTIOPFManager.PersistenceLayers.FindByPerLayerName(PersistenceLayerName);
- Assert(LPerLayer<>nil, '"' + PersistenceLayerName + '" not registered');
- if LPerLayer.DatabaseExists(DatabaseName, UserName, Password) then
- ShowMessage('Database <' + DatabaseName + '> exists.')
- else
- ShowMessage('Database <' + DatabaseName + '> does not exist.');
- end;
-
- procedure TFormMainCreateDatabase.btnCreateDatabaseClick(Sender: TObject);
- var
- LPerLayer: TtiPersistenceLayer;
- begin
- LPerLayer:= gTIOPFManager.PersistenceLayers.FindByPerLayerName(PersistenceLayerName);
- Assert(LPerLayer<>nil, '"' + PersistenceLayerName + '" not registered');
- LPerLayer.CreateDatabase(DatabaseName, UserName, Password);
- ShowMessage('Database "' + DatabaseName + '" has been created.');
- end;
Tables are created using TtiDBMetaDataTable.
Example - from Demo_CreateTable
-
- procedure TFormMainCreateTable.btnTableExistsClick(Sender: TObject);
- var
- lDBMetaData: TtiDBMetaData;
- lPooledDB : TPooledDB;
- lDatabase : TtiDatabase;
- begin
- lDBMetaData:= TtiDBMetaData.Create;
- try
- lPooledDB:= gTIOPFManager.DefaultDBConnectionPool.Lock;
- try
- lDatabase:= lPooledDB.Database;
- lDatabase.ReadMetaDataTables(lDBMetaData);
- if lDBMetaData.FindByTableName('Client') <> nil then
- ShowMessage('Table <Client> exists')
- else
- ShowMessage('Table <Client> does not exist');
- finally
- gTIOPFManager.DefaultDBConnectionPool.UnLock(lPooledDB);
- end;
- finally
- lDBMetaData.Free;
- end;
- end;
-
-
- TFormMainCreateTable.btnCreateTableClick(Sender: TObject);
- var
- lTableMetaData: TtiDBMetaDataTable;
- begin
- lTableMetaData:= TtiDBMetaDataTable.Create;
- try
- lTableMetaData.Name:= 'Client';
- lTableMetaData.AddField('OID', qfkString, 36);
-
- lTableMetaData.AddField('Client_Name', qfkString, 200);
- lTableMetaData.AddField('ACN', qfkString, 9);
- gTIOPFManager.CreateTable(lTableMetaData);
- finally
- lTableMetaData.Free;
- end;
- ShowMessage('Table ''Client'' created');
- end;
-
- procedure TFormMainCreateTable.btnDropTableClick(Sender: TObject);
- begin
- gTIOPFManager.DropTable('Client');
- ShowMessage('Table ''Client'' dropped');
- end;
How do I filter it.
The easiest way to filter an object list is by descending from TtiFilteredObjectList.
Example from Demo_CollectionWithCriteria
- TClients = class(TtiFilteredObjectList);
-
- ...
-
- FClients.Clear;
- FClients.Criteria.ClearAll;
-
-
-
- FClients.Criteria.AddLike('Client_Name', EditFilter.Text + '%');
-
- ...
-
- FClients.Read;
An object list can also be filtered using Hard Coded Visitors.
See Demo_CollectionWithFilter
How do I build a GUI?
One of the main Gui elements is TtiVTListView. Columns are added using AddColumn, passing in the property name, type, display name and display size. To populate, the Data property is set to a TtiObjectList.
Example from Demo_EditDataInGui
- LV.AddColumn('ClientID', vttkString, 'Client ID', 80);
- LV.AddColumn('ClientName', vttkString, 'Client name', 200);
-
- FClients:= TClients.Create;
- ...
- FClients.Read;
-
- LV.Data:= FClients;
Add, Edit and Delete capabilites are provided using the OnEdit, OnInsert and OnDelete events.
- procedure TForm2.LVItemEdit(pVT: TtiCustomVirtualTree; pData: TtiObject;
- pItem: PVirtualNode);
- begin
- if TFormClientEdit.Execute(pData) then
- pVT.Refresh(pData);
- end;
-
- procedure TForm2.LVItemInsert(pVT: TtiCustomVirtualTree; pData: TtiObject;
- pItem: PVirtualNode);
- var
- lClient: TClient;
- begin
- lClient:= TClient.CreateNew;
- if TFormClientEdit.Execute(lClient) then
- begin
- FClients.Add(lClient);
- pVT.Refresh(lClient);
- end
- else
- lClient.Free;
- end;
-
- procedure TForm2.LVItemDelete(pVT: TtiCustomVirtualTree; pData: TtiObject;
- pItem: PVirtualNode);
- begin
- if tiPerObjAbsConfirmAndDelete(pData as TClient) then
- pVT.Refresh;
- end;
tiOPF also comes with a number of object aware controls such as TtiPerAwareEdit. These use the LinkToData method to attach to a TtiObject
- property Databuffer : TtiObject read FDataBuffer write FDataBuffer;
-
- ...
-
- paeOID.Value:= DataBuffer.OID.AsString;
- paeClientName.LinkToData(DataBuffer, 'ClientName');
- paeClientID.LinkToData(DataBuffer,'ClientID');
A GUI can also be built using standard non-db components and mediating views. Base edit and list mediator controls are provided in the GUI directory.
There is a demo in \tiOPF2_Demos\GenericMediatingViews.
How do I use data aware controls?
Use TTiDataset and TtiNestedDataset. These are included in the latest svn.
They may not yet be available in the download version
TtiDataset is linked to an object list using the method LinkObject defined as follows:
- procedure LinkObject(AObjectList: TtiObjectList; AClass: TtiObjectClass);
-
-
-
- DatasetClients_.LinkObject(FClients, TClient);
TtiNestedDataset is linked to an existing TtiDataset or TtiNestedDataset using the properties DataSetField and ObjectClass as follows:
- NestedDataset_PhoneNumbers.DataSetField:= DatasetClients_PhoneNumbers;
- NestedDataset_PhoneNumbers.ObjectClass:= TPhoneNumber;
See the dataset demo and unit tests for further information.
Useful units and functions.
Unit |
Contains |
tiObject | TtiObject and TtiObjectList |
tiFilteredObjectList | TtiFilteredObjectList |
tiCriteria | the TPerCriteria objects used in TtiFilteredObjectList |
tiOPFManager | gTIOPFManager |
tiAutoMap | Automapping |
tiVisitorDBAutoGen | TVisDBAutoGenRead and Update used in DB Independent Visitors |
tiVisitorDB | TVisOwnedQrySelect and Update used in Hard Coded Visitors |
tiOID | base OID. Needs to be included in your business model unit. |
tiOIDGUID | GUID OID. Needs to be included in your project at least once if you require guid OIDs |
tiOIDInteger | Integer OID. Needs to be included in your project at least once if you require int OIDs |
tiQuery | Contains the base TtiQuery object |
Standard Persistence layers Either include the required unit, or use the LINK_XXX conditional define.
See Demo_LoadPersistenceLayerIfDef or Demo_LoadPersistenceLayerUses.
Persistence |
Conditional |
tiQueryXML | LINK_XML |
tiQueryIBX | LINK_IBX |
tiQueryBDEParadox | LINK_BDEPARADOX |
tiQueryADOAccess | LINK_ADOACCESS |
tiQueryADOSQLServer | LINK_ADOSQLSERVER |
tiQueryCSV | LINK_CSV |
tiQueryTAB | LINK_TAB |
tiQueryXMLLight | LINK_XMLLIGHT |
tiQueryDOA | LINK_DOA |
tiQueryRemote | LINK_REMOTE |
tiQuerySqldbIB | LINK_SQLDB_IB |
tiQuerySqldbPQ | LINK_SQLDB_PQ |
tiQueryFBL | LINK_FBL |
tiQueryZeosIBFB | LINK_ZEOS_FB |
ADVANCED TOPICS
How do I encapsulate and associate objects?
Encapsulation is normally done by including an object or object list inside another.
A one to many relationship can be modelled by including a TtiObjectList containing the children, inside the parent. Alternately, the child can contain an instance of the parent.
A third possibility is to store just the OID of the associated object. This is useful when access to the object itself is not required.
If the encapsulated object is published, then it will be saved and loaded together with it's owner. If it is public, then the saving and loading will need to be handled in code.
Examples from MastApp:
- TOrder = class(TtiObject)
- private
- ...
- published
-
- property OrderNo: integer read GetOrderNo;
- property CustNo: TOID read GetCustNo write SetCustNo;
- property SaleDate: TDateTime read FSaleDate write FSaleDate;
- property ShipDate: TDateTime read FShipDate write FShipDate;
- property EmpNo: TOID read GetEmpNo write SetEmpNo;
- ...
- property OrderItems: TOrderItems read FOrderItems;
- end;
-
- TOrderItem = class(TtiObject)
- private
- ...
- function GetDescription: string;
- function GetPart: TPart;
- procedure SetPartNo(const Value: double);
- function GetListPrice: Currency;
- function GetTotalPrice: Currency;
- protected
- FOrderNo: TOID;
- FPartNo: double;
- FPart: Tpart;
- function GetCaption: string; override;
- procedure AssignClassProps(ASource: TtiObject); override;
- public
- ...
- property Part: TPart read GetPart;
- property Description: string read GetDescription;
- property ListPrice: Currency read GetListPrice;
- property TotalPrice: Currency read GetTotalPrice;
- published
- property OrderNo: TOID read GetOrderNo write SetOrderNo;
-
- property PartNo: double read FPartNo write SetPartNo;
- ...
- end;
-
- function TOrderItem.GetPart: TPart;
- begin
- if not assigned(FPart) then
- begin
- FPart:= TPart.Create;
-
- end;
- if FPart.ObjectState = posEmpty then
- begin
- FPart.OID.AsString:= FloatToStr(FPartNo);
- FPart.Read();
- end;
- result:= FPart;
- end;
-
- procedure TOrderItem.SetPartNo(const Value: double);
- begin
- if FPartNo <> Value then
- begin
- FPartNo:= Value;
- if assigned(FPart) then FPart.ObjectState:= posEmpty;
- end;
- end;
Visitors
tiOPF uses the Visitor pattern extensively. Reading and Saving objects is done using visitors. An in depth knowledge of visitors is not required to use tiOPF successfully.
An executive summary is as follows:
Visitor objects iterate over a collection of objects and perform an operation on each acceptable object. In tiOPF terms, visitors descend from TtiVisitor and operate on descendants of TtiObject (this includes TtiObjectList).
Important features of TtiVisitor are:
Visited Property - the object currently being operated on.
AcceptVisitor virtual method - used to determine if the Visited object should be operated on.
Execute virtual method - that actually performs the operation on Visited.
For more information, see chapter 2 of the concepts manual.
Persistence mapping
As mentioned, there are 3 types of persistence mapping, AutoMapping, DB Independent Visitors and Hard Coded Visitors. Automapping will work with both flat files (xml, csv etc) and databases. DB Independent and hard Coded will only work with databases.
Most demos allow you to choose the mapping on start up so you can compare them.
- Automapping:
- Automapping is the simplest persistence mapping. Simply call;
gTIOPFManager.ClassDBMappingMgr.RegisterMapping for each field to be persisted,
and call
gTIOPFManager.ClassDBMappingMgr.RegisterCollection for each collection object.
this is done once, at start up.
Example from Demo_Collection
-
- gTIOPFManager.ClassDBMappingMgr.RegisterMapping(TClient, 'Client', 'OID', 'OID', [pktDB]);
- gTIOPFManager.ClassDBMappingMgr.RegisterMapping(TClient, 'Client', 'ClientName', 'Client_Name' );
- gTIOPFManager.ClassDBMappingMgr.RegisterMapping(TClient, 'Client', 'ClientID', 'Client_ID' );
- gTIOPFManager.ClassDBMappingMgr.RegisterCollection(TClients, TClient);
- DB Independent Visitors:
- DB Independent Visitors (DBIV) require creating 4 visitors for each class to be persisted; read, create, update and delete visitor objects. These objects need to be registered at start up. This provides more control than automapping at the cost of considerably more code.
- TVisClient_Read = class(TVisDBAutoGenRead)
- protected
- function AcceptVisitor: boolean; override;
- procedure Init ; override;
- procedure SetupParams ; override;
- procedure MapRowToObject; override;
- end;
-
- TVisClient_Create = class(TVisDBAutoGenUpdate)
- protected
- function AcceptVisitor: boolean; override;
- procedure SetupParams ; override;
- end;
-
- TVisClient_Update = class(TVisDBAutoGenUpdate)
- protected
- function AcceptVisitor: boolean; override;
- procedure SetupParams ; override;
- end;
-
- TVisClient_Delete = class(TVisDBAutoGenDelete)
- protected
- function AcceptVisitor: boolean; override;
- procedure SetupParams ; override;
- end;
See Demo_Collection, Client_DBIndependentVisitors_Svr.pas for implementation details.
Under the hood, the visitors create a sql statement based on the parameters supplied (table name and field names).
- function TVisClient_Create.AcceptVisitor: boolean;
- begin
- result:= (Visited is TClient) and (Visited.ObjectState = posCreate);
- Log([ClassName, Visited.ClassName, Visited.ObjectStateAsString, Result ]);
- end;
-
- procedure TVisClient_Create.SetupParams;
- var
- LData: TClient;
- begin
- LData:= Visited as TClient;
- TableName:= 'Client';
- QueryType:= qtInsert;
- QueryParams.SetValueAsString('OID', LData.OID.AsString);
- QueryParams.SetValueAsString('Client_Name', LData.ClientName);
- QueryParams.SetValueAsString('Client_ID', LData.ClientID);
- end;
The advantage over automapping is that you have more control over the makeup of the sql.
- Hard Coded Visitors
- Hard Coded Visitors also require the creation and registration of 4 visitors. However, they simply provide an empty Query object, and creation of the sql is left to the developer.
- TVisClient_Read = class(TVisOwnedQrySelect)
- protected
- function AcceptVisitor: boolean; override;
- procedure Init ; override;
- procedure SetupParams ; override;
- procedure MapRowToObject; override;
- end;
-
- TVisClient_Create = class(TVisOwnedQryUpdate)
- protected
- function AcceptVisitor: boolean; override;
- procedure Init ; override;
- procedure SetupParams ; override;
- end;
-
- TVisClient_Update = class(TVisOwnedQryUpdate)
- protected
- function AcceptVisitor: boolean; override;
- procedure Init ; override;
- procedure SetupParams ; override;
- end;
-
- TVisClient_Delete = class(TVisOwnedQryUpdate)
- protected
- function AcceptVisitor: boolean; override;
- procedure Init ; override;
- procedure SetupParams ; override;
- end;
-
- ...
-
- function TVisClient_Create.AcceptVisitor: boolean;
- begin
- result:= (Visited is TClient) and (Visited.ObjectState = posCreate);
- Log([ClassName, Visited.ClassName, Visited.ObjectStateAsString, Result ]);
- end;
-
- procedure TVisClient_Create.Init;
- begin
- Query.SQLText:= 'Insert into Client (OID, Client_Name, Client_ID) ' +
- 'Values ' + '(:OID,:Client_Name,:Client_ID)';
- end;
-
- procedure TVisClient_Create.SetupParams;
- var
- lData: TClient;
- begin
- lData:= Visited as TClient;
- lData.OID.AssignToTIQuery('OID', Query);
- Query.ParamAsString['Client_Name']:= lData.ClientName;
- Query.ParamAsString['Client_ID']:= lData.ClientID;
- end;
HCV should be used when you need precise control of the sql, or when you sql doesn't fall into the "Select * from ..." Table mould.
Which persistence mapping should I use?
For flat files (xml etc), use automapping.
Both Automapping and DBIV use "Select * from ..." queries, so if this is not suitable (bringing back too many unwanted fields) then use HCV.
Otherwise the difference in coding required is such that you are best off starting with automapping and then replacing it as required.
Note: it is possible to mix and match. E.g. use a HCV to select the data and then use Automapping to perform the updates.
Feature required |
Automapping |
DB Independent |
Hard Coded |
Flat files |
Yes |
No |
No |
Use stored procedures |
No |
No |
Yes |
Swap databases easily |
Yes |
Yes |
Maybe |
Control over sql |
No |
Limited |
Complete |
Performance |
None |
Limited |
Complete |
More to come ... please stay tuned.