Developer’s Guide Version 2
Table of Contents
What is an object-oriented database?
Design rational behind
the Spider Object Database
Native
Object Pascal
Simplicity
Database
independence
Spider Object Database's Architecture
Database design guidelines with Spider Object Database
Making a class
persistent
Streaming
Class Hierarchies
Class
Registration
Class
Versioning
Creating the
Object-Relational Mapping
Field
Types and Indices
Defining
Indices
Using
Autoincrement Fields
Using a TObjectDatabase
Instance
Using
Transactions
Choosing a Driver
Database
Architecture
SQL
or ISAM?
Performance
Size
of the database
Number
of simultaneous users
Robustness
Comparison
Table
Using the TObjectFile
Class
Traversing
Searching
Inserting
Updating
Deleting
Using the TObjectUpdate
Class
Inserting
Updating
Deleting
Using the TObjectQuery
Class
Specifying
the query
Traversing
Object
lookup
The purpose of this guide is to provide the developer with the fundamental skills in order to be proficient with the Spider Object Database. Creating databases using objects as the basic storage unit is a very different approach than traditional relational databases. Although some of the concepts to relational databases, such as normalization and avoiding data redundancy still apply, the developer will need to acquire very solid object-oriented modeling skills. This is essential, as the entire database will be built upon the classes created within the programming language. There lies the fundamental difference between traditional database systems; the information stored in an object-oriented database does not require any conversion to be used within the native programming language. This guide does not discuss object-oriented analysis, design, and programming topics. There are many articles and books on the subject.
For a detailed reference of the library, consult the online help files.
What is an object-oriented database?
An object-oriented database is a database management system that handles the persistent storage and retrieval of objects. Unlike a relational database, whose characteristics may be easily expressed using mathematical set theory, it is very hard to describe an object-oriented database system using mathematical equations. Some objects may be as simple as one dimensional records in a relational database, but many objects are complex in nature. Objects are often inherited from other objects, and can recursively contain other objects.
Object-oriented databases evolved in academic circles during the 1980's. Since the 90's, commercial acceptance has continuously grown because of the need to store complex data types (objects). Often cited examples are CAD/CAM (computer assisted design/manufacturing), engineering, and multimedia related programs. For examples, data structures used by 3D CAD programs are very hard to express using the row/column approach in relational database. This situation is referred to as an impedance mismatch.
The term impedance mismatch has its roots in electrical circuit theory. This term is often used in object-oriented circles to advocate object-oriented databases (OODB). OODB's are based on persistent objects; entire databases are composed of objects, instead of one dimensional records. So what is this concept of impedance mismatch? In electrical terms, when two circuits are coupled together, in order to get the maximum amount of power transfer from one to the other, the output impedance of one circuit must the same as the input impedance of the other. When the impedance is not the same, the maximum power transfer is not obtained. So what does this have to do with objects? The analogy is really between the efficiency of the power transfer and the software development efficiency.
A good example is in Delphi itself. Forms are created at design time by dropping components on the form. As you probably know, the design time information is stored in a file with the DFM extension. This file is actually a persistent version of the form that was created at design time. The form's properties, and the components on the form are streamed to the DFM file. Forms can be very complex, with many nested components. Imagine if this information would be maintained in relational tables? There would be an impedance mismatch; the form information would have to be converted into a different format, using tables and rows.
Design rational behind the Spider Object Database
Most existing OODBM systems focus on C++, Java, or SmallTalk languages. Object Pascal has a very well designed object model, so Spider Object Database's goal is to provide an object-oriented database system that directly supports Object Pascal. Once a class is written in Object Pascal, instances of the class can be stored in a database without any binding or conversion code!
Component or modular based software is evolving at a phenomenal rate. This is a great concept, system requirements are forever more demanding, so software construction must become more and more component or modular based. This allows software development to maintain a reasonable pace to meet the demand. On paper this approach seems simple. In practice, the interaction of multiple software layers leads to side effects. As software leaders keep pushing new component standards, the complexity of deploying component software, especially in networked environments, is often overlooked. Network administrators constantly deal with version and deployment problems related to external DLL, OLE, OCX, ActiveX, ... software modules. Delphi, especially with its internal (native) VCL component framework, encourages simplicity. Spider Object Database continues along those lines, providing a class library that does not impose any additional external software modules.
Spider Object Database's architecture provides complete isolation from the underlying physical database. This is very important as databases vary tremendously with respect to performance, size, and robustness. Any database system, object-oriented or not, should not impose a particular physical database. The choice of the underlying database must be determined from the requirements of the application itself. Almost all "front end" relational development tools, including Delphi, have adequately achieved database independence. The same should apply to object-oriented database systems.
Spider Object Database's Architecture
The object-oriented database abstraction provided by Spider Object Database is divided into five primary classes. A TObjectDatabase instance is created to perform database level operations such as opening/closing sessions and transaction control (commit/rollback).
A TObjectFile instance is analogous to a TTable instance in a relational database. It allows reading and modifying objects in an object file.
A TObjectQuery allows reading objects from an object file based on an SQL query. A TObjectUpdate instance allows adding, updating, or deleting objects in an object file.
SQL based engines will use the TObjectQuery and TObjectUpdate classes. ISAM (indexed sequential access method) based databases will use the TObjectFile class to read and modify objects. Some databases, such as Turbo Power's Flash Filer and Elevate Software's DBISAM allow both ISAM and SQL access.
The TOdbDriver instance provides
the link with the underlying database. The database independence is achieved
by deriving new classes from this abstract class. This class defines the
interface which TObjectDatabase, TObjectFile, TObjectQuery, and
TObjectUpdate
instances use to communicate with the underlying database.
Database design guidelines with Spider Object Database
In order for any database to be usable, the data contained must be structured in a meaningful manner. This structure is defined by a data model. Databases have been successfully created with different models, such as hierarchical, network, and relational. Hierarchical and network databases have been used extensively in the mainframe world, but the most popular model since the 1980's has been the relational model. The object-oriented data model has evolved from object-oriented programming languages, which wanted to store objects directly into a database without having to convert objects into a different format.
Object-oriented database system are generally divided into two broad categories; pure and hybrid. Hybrid systems are built by extending a relational database system to support objects. Pure systems are built from the ground up, concentrating strictly on an object-oriented approach without any constraints imposed by an existing model. These systems often use a proprietary data format.
A hybrid approach is used with Spider Object Database to take advantage of numerous existing database systems, in order to achieve database independence. The relational model is also very well established with a proven model. Some concepts in the relational model may be argued to be universal; a relation establishes a conceptual link between two entities, independent of the object model.
Spider Object Database makes a distinction between two types of relations, external and internal. An external relationship is borrowed from the relational model. An internal relationship is managed by the object itself. It is the internal relationships which provides support for complex objects. Internal relationships are created using container classes.
Here is an example of a typical of a customer invoicing database using the relational model.
In this relational model four tables are required. The three relations are external.
Let’s analyze this example with the Spider Object Database approach. This requires identifying the object identities. In a relational model, all one-to-many relations must be expressed with relations between tables. With an object-oriented approach, many one-to-many relations may be eliminated, as an object can contain a list of other objects, by using aggregated container objects. Many container classes all with built-in persistent support, including as lists, stacks, queues, and arrays, are available to allow creating internal relations.
An invoice object contains a list of invoice items. This immediately eliminates the external relation between invoice and invoice items.
A product is an autonomous object. The one-to-many relation between products and invoice items remains external, as invoice items are already contained within an invoice.
The one-to-many relationship between Customer and Invoice objects is interesting. If the same rational as the relationship between Invoice and Invoice Items is used, it would be immediately apparent that the relationship should be internal, as a customer can be modelled as "containing" a list of invoices . This brings up the issue of lock granularity. In a multi-user environment, objects should always be locked before being updated. If the Customer object is to have an internal relationship with the Invoice object, this has the implication that only one invoice for a customer can be created at the same time. The decision whether or not this relationship should be external or internal is dependent on the application requirements. Here is the same customer invoicing database using the Spider Object Database model, using an internal relationship between Customer and Invoice objects.
This model yields only two object files, as opposed to four in the relational model.
Persistent objects are objects that persist beyond their normal life span. All objects that are created in a Delphi application are only accessible during the execution of the application. This is the normal life span for a non persistent object. The idea of persistent objects is to give the ability to objects to store themselves on some persistent stream (usually files), so that they may later be reconstructed. The action of storing an object on a persistent stream is called streaming, which is why persistent objects are also known as streamable objects.
Object streaming in Delphi's VCL library is very component centric. The documentation indicates that classes derived from TPersistent may be stored on streams. In reality, the methods of TStream, WriteComponent and ReadComponent, clearly indicate that the streaming architecture in Delphi is focus primarily on component streaming. In addition, the streaming mechanism in Delphi is virtually an undocumented feature.
The Spider Object Database uses a streaming architecture that is easy to comprehend and non-intrusive. It is not required to derive all streamable objects from a common base class. This is very important for object-oriented languages that support single inheritance. This means that existing class hierarchies need not be changed to implement persistent objects. All that is required to make an object streamable is to write two routines, and to register the class.
To make a class persistent two procedures must be created; one to read the class's data members, and another to write the class's data members. Then the class must be registered, usually in the initialization section of a unit. If the class changes over time, multiple versions may be required.
The read and write procedures must have the same signature as specified by the TObjectReadProc type definition and the TObjectWriteProc type definition respectively.
TObjectReadProc = procedure(
const Obj : TObject;
const Reader : TObjectReader );
TObjectWriteProc = procedure(
const Obj : TObject;
const Writer : TObjectWriter );
The object to be read or written is
passed in the Obj parameter. The TObjectReader and TObjectWriter objects
provide methods to stream various database types. Below is a table showing
the various methods of the TObjectReader/TObjectWriter classes used to
stream the data belonging to Obj.
Method | Purpose |
ReadInteger / WriteInteger | Read and write data members of types such as Byte, Word, Integer, and Cardinal. |
ReadFloat / WriteFloat | Read and write data members of types such as Single, Double, and Extended. |
ReadString / WriteString | Read and write data members of type String. |
Read / Write | Read and write raw data in binary form. |
ReadChildObject / WriteChildObject | Read and write object data members. |
It is generally easier to first create the write procedure. After the write procedure is created, the read procedure has the same structure as the write procedure, with the obvious exception the data is read instead of written.
Here is a simple template for read and write procedures:
unit ExampleObject;
interface
uses
ObjPerst; { TObjectReader, TObjectWriter }
TExampleObject = class(TObject)
{ basic types }
Data1 : String;
Data2 : Double;
{ binary data }
BufSize : Integer;
pBuffer : ^TByteArray;
{ child objects }
ChildObject : TChildObject;
constructor Create;
destructor Destroy; override;
end;
procedure WriteExampleObject(
const Obj : TObject;
const Writer : TObjectWriter );
procedure ReadExampleObject(
const Obj : TObject;
const Reader : TObjectReader );
implementation
constructor TExampleObject.Create;
begin
inherited Create;
BufSize := 4096;
GetMem(pBuffer, BufSize);
ChildObject := TChildObject.Create;
end;
destructor TExampleObject.Destroy;
begin
ChildObject.Free;
FreeMem(pBuffer, BufSize);
inherited Destroy;
end;
procedure WriteExampleObject(
const Obj : TObject;
const Writer : TObjectWriter );
var
ExampleObject : TExampleObject;
begin
ExampleObject := Obj as TExampleObject;
{ basic types }
Writer.WriteString(ExampleObject.Data1);
Writer.WriteFloat(ExampleObject.Data2);
{ binary data }
Writer.WriteInteger(ExampleObject.BufSize);
Writer.Write(ExampleObject.pBuffer^, BufSize);
{ child objects }
Writer.WriteChildObject(ExampleObject.ChildObject);
end;
procedure ReadExampleObject(
const Obj : TObject;
const Reader : TObjectReader );
var
ExampleObject : TExampleObject;
begin
ExampleObject := Obj as TExampleObject;
{ basic types }
ExampleObject.Data1 := Reader.ReadString;
ExampleObject.Data2 := Reader.ReadFloat;
{ binary data }
ExampleObject.BufSize := Reader.ReadInteger;
GetMem(ExampleObject.pBuffer, ExampleObject.BufSize);
Reader.Read(ExampleObject.pBuffer^, ExampleObject.BufSize);
{ child objects }
ExampleObject.ChildObject := Reader.ReadChildObject;
end;
initialization
RegisterStreamable(
TExampleObject,
[TObjectReadProc(@ReadExampleObject)],
[TObjectWriteProc(@WriteExampleObject)]
);
end.
Notice that if the class has dynamically allocated data members other than objects, the read procedure is responsible to allocate memory, just like it is done in the constructor. For child objects, the memory is allocated automatically by the streaming mechanism in the ReadChildObject method.
Some Delphi versions have problems with the array of TProcedure style of parameters. It may be necessary to typecast as shown in the RegisterStreamable calls above.
Often classes are derived from other classes to extended the functionality of an object. Derived classes should make use of the read and write procedures of the ancestor class to avoid rewriting code. Following is a unit containing a class derived from the TExampleObject class.
unit DerivedObject;
interface
uses
ExampleObject,
ObjPerst;
{ TObjectReader, TObjectWriter }
TDerivedObject = class(TExampleObject)
{ new data
member added in derived class }
NewDataMember
: String;
end;
procedure WriteDerivedObject(
const Obj
: TObject;
const Writer
: TObjectWriter );
procedure ReadDerivedObject(
const Obj
: TObject;
const Reader
: TObjectReader );
implementation
procedure WriteDerivedObject(
const Obj
: TObject;
const Writer
: TObjectWriter );
var
DerivedObject
: TDerivedObject;
begin
DerivedObject
:= Obj as TDerivedObject;
{ write the
data members of the ancestor }
WriteExampleObject(Obj,
Writer);
{ write the
data members in the current class }
Writer.WriteString(DerivedObject.NewDataMember);
end;
procedure ReadDerivedObject(
const Obj
: TObject;
const Reader
: TObjectReader );
var
DerivedObject
: TDerivedObject;
begin
DerivedObject
:= Obj as TEDerivedObject;
{ read the
data members of the ancestor }
ReadExampleObject(Obj,
Reader);
{ read the
data members in the current class }
DerivedObject.NewDataMember
:= Reader.ReadString;
end;
initialization
{$ifdef Win32}
RegisterStreamable(
TDerivedObject,
[TObjectReadProc(@ReadDerivedObject)],
[TObjectWriteProc(@WriteDerivedObject)]
);
{$else}
RegisterStreamable(
TDerivedObject,
[ReadDerivedObject],
[WriteDerivedObject]
);
{$endif}
end.
In the initialization section of both the ExampleObject and DerivedObject units, the classes are registered with the streaming system using the RegisterStreamable procedure. This allows the streaming system to associate classes with their read and write procedures. If multiple classes share the same read and write procedures, all the most derived classes must be registered individual. For example, the WriteGraphic and ReadGraphic procedures found in the ObjPerst unit are used to stream TBitmap, TIcon, and TMetaFile classes. Eventhough all three classes share the TGraphic ancestor, each class must be registered individual for the streaming system to properly recreate the classes when reading the object data from a stream.
The RegisterStreamable procedure requires as parameters the object type, and the read/write procedures. There may be multiple read and write procedures to support class versioning.
Classes evolve with time. Class versioning allows for the streaming mechanism to function with previous class versions. When a class changes by adding new data members to the class, the new data members must be stored, but the previous versions of the class should still be readable.
The versioning mechanism used by the Spider Object Database, is to create different versions of the read and write procedures for a class. These procedures are registered with RegisterStreamable.
With the following class:
TPerson = class(TObject)
Name : String[50];
Email : String[80];
end;
Initially, the first read and write procedures may have been,
procedure ReadPerson1(
const Obj
: TObject;
const Reader
: TObjectReader );
begin
(Obj as TPerson).Name
:= Reader.ReadString;
end;
procedure WritePerson1(
const Obj
: TObject;
const Writer
: TObjectWriter );
begin
Writer.WriteString((Obj
as TPerson).Name);
end;
If the Email data member was added at a later time, new read and write procedures may be written as follows:
procedure ReadPerson2(
const Obj
: TObject;
const Reader
: TObjectReader );
begin
ReadPerson1(Obj,
Reader);
(Obj as TPerson).Email
:= Reader.ReadString;
end;
procedure WritePerson2(
const Obj
: TObject;
const Writer
: TObjectWriter );
begin
WritePerson1(Obj,
Writer);
Writer.WriteString((Obj
as TPerson).Email);
end;
Finally, the class is registered,
RegisterStreamable(
TPerson,
[ReadPerson1,
ReadPerson2],
[WritePerson1,
WritePerson2]
);
Creating the Object-Relational Mapping
Once a class is persistent, it may stored within streams (file, memory, etc...). The TObjectStream class in the ObjPerst.Pas unit, allows creating a stream of objects. In order to store objects inside a database other considerations are involved. Random access to the objects will be required, and the efficient use of indices will speed up queries. This is the role of the object-relational mapping. The abstract class, TORMapping, provides the interface to create the object-relational mapping.
The primary purpose of the object-relational
mapping is to define the indices within a file of objects. To define indices,
first the fields participating in the indices must be defined. This leads
to two methods in TORMapping, namely, DefineFields and DefineIndices. Here
is a typical object-relational mapping class. This is a simplified example
from TestObj.Pas:
interface
type
TTestObjectMapping =
class(TORMapping)
public
function
GetObjectType : TClass; override;
procedure
DefineFields; override;
procedure
DefineIndices; override;
procedure
UpdateFieldsFromObj( const Obj : TObject
); override;
{
This method is overriden when the table has an autoincrement field }
procedure
UpdateObjFromFields( const Obj : TObject
); override;
end;
implementation
function TTestObjectMapping.GetObjectType
: TClass;
begin
Result
:= TTestObject;
end;
procedure TTestObjectMapping.DefineFields;
begin
AddField('Name',
oftString, 41);
AddField('Age',
oftInteger, 0);
AddField('Weight',
oftFloat, 0);
AddField('Id',
oftAutoInc, 0);
end;
procedure TTestObjectMapping.DefineIndices;
begin
AddIndex(
'Name', [iaPrimary], ['Name']);
AddIndex(
'NameCI', [iaUnique, iaCaseInsensitive], ['Name']);
AddIndex(
'AgeName', [], ['Age','Name']);
AddIndex(
'AgeDesc', [iaDescending], ['Age']);
AddIndex(
'Weight', [], ['Weight']);
AddIndex(
'Id', [iaUnique], ['Id']);
end;
procedure TTestObjectMapping.UpdateFieldsFromObj(
const Obj : TObject );
begin
FieldByName('Name').AsString
:= (Obj as TTestObject).Name;
FieldByName('Age').AsInteger
:= (Obj as TTestObject).Age;
FieldByName('Weight').AsFloat
:= (Obj as TTestObject).Weight;
FieldByName('Id').AsInteger
:= (Obj as TTestObject).Id;
end;
procedure TTestObjectMapping.UpdateObjFromFields(
const Obj : TObject );
begin
{
only update autoincrement field }
(Obj
as TTestObject).Id := FieldByName('Id').AsInteger;
end;
The GetObjectType method simply returns the type of the object for which will be handled by the object-relational mapping.
The UpdateFieldsFromObj method has the responsibility to obtain the field values given an object. The UpdateObjFromFields is only overriden when there is an autoincrement field defined. This allows updating an object from an autoincrement field value assigned by the database engine. Note that not all drivers support autoincremented fields.
The DefineFields method defines all the fields that will be used in the indices of the object file. When using SQL databases, fields not participating in indices may also be defined to be used in SQL queries. The AddField method is part of the TORMapping base class. See the online help file for more details.
The example show all field types available with the Spider ODB. The TOdbFieldType type defines which data types may be used to create an index.
TOdbFieldType = (oftString, oftFloat, oftInteger, oftAutoInc);
For simplicity, only three basic types, string, float, and integer are used. This is sufficient to index almost any type of information. For instance date and time values may be indexes as string types. The functions DateToDateStr and TimeToTimeStr can be used to convert Delphi’s TDateTime into sortable strings.
Note that to define string fields, the size passed as a parameter to AddField must include the length byte, thus it is always one more than the maximum length of the string. The field size parameter to AddField only applies to string fields; passing 0 for other fields is recommended.
All object-relational mappings must define one primary index. This is done by creating an index with the iaPrimary attribute. There may be additional unique indices, which are created using the iaUnique attribute. The primary index is the index used when using the TObjectUpdate class.
The DefineIndices method defines the indices that will be used with the object file. The AddIndex method is part of the TORMapping base class. See the online help file for more details.
Autoincrement fields are used in conjunction with an index definition. An autoincrement field is like an integer field, but its value is automatically incremented by one every time an object is inserted into an object file. The autoincrement is performed by the database engine, so for this reason, the UpdateObjFromFields method of TObjectFile must be overriden to update an inserted object with the value incremented by the database engine. For example, the Id field in the TTestObject class defines an autoincrement field.
procedure TTestObjectMapping.DefineFields;
begin
...
AddField('Id',
oftAutoInc, 0);
end;
procedure TTestObjectMapping.DefineIndices;
begin
...
AddIndex(
'Id', [iaUnique], ['Id']);
end;
Using a TObjectDatabase Instance
Before accessing any object file with the Spider ODB classes, a TObjectDatabase must be created to establish a connection with the database. The constructor of the TObjectFile, TObjectQuery, and TObjectUpdate classes require a TObjectDatabase instance.
The TObjectDatabase instance may be created in the OnCreate handler of the main form and destroyed in the OnDestroy handler. Below is typical example on how to create an object database and object file within the main form of an application.
type
TMainForm = class(TForm)
procedure
FormCreate(Sender: TObject);
procedure
FormDestroy(Sender: TObject);
private
{ Private
declarations }
CompanyDatabase
: TObjectDatabase;
EmployeeFile
: TPersonFile;
public
{ Public
declarations }
end;
procedure TMainForm.FormCreate(Sender:
TObject);
var
ServerName,
UserName, Password : String;
begin
CompanyDatabase
:= TObjectDatabase.Create(
TBdeOdbDriver,
‘ABC_Supplies’ );
ServerName
:= ‘’;
UserName
:= ‘’;
Password
:= ‘’;
CompanyDatabase.Open(ServerName,
UserName, Password);
EmployeeFile
:= TPersonFile.Create(
CompanyDatabase, ‘Employee’, TPerson );
EmployeeFile.Open;
end;
procedure TMainForm.FormDestroy(Sender:
TObject);
begin
EmployeeFile.Close;
EmployeeFile.Free;
CompanyDatabase.Close;
CompanyDatabase.Free;
end;
The TObjectDatabase constructor requires two parameters; a driver class, and the name of the database.The driver class is a class derived from TOdbDriver. The database name is the name of the database to connect to. This may be an alias, or a directory name depending on which driver is employed.
The connection to the database is initiated when the Open method of the TObjectDatabase object is invoked. It requires the ServerName, UserName, and Password parameters. These parameters must be set as required by the underlying driver.
There are three methods in the TObjectDatabase class used for transaction processing; StartTransaction, Commit, and RollBack. Transactions are used for "all or nothing" changes to a database.
A transaction begins by invoking StartTransaction and must later be terminated with the Commit method to accept the changes, or the RollBack method to ignore all changes.
The classical example to explain the transaction concept is the transfer of money from one bank account to another. Two operations must be performed to accomplish this task. The first is to withdraw the amount from the first account, and the second is to deposit the amount into the second account. Without a transaction, if a failure occurs after the withdrawal, the balance of the second account will not be correct. Below is the pseudo code to properly use transactions in this example.
Database.StartTransaction;
try
Account1.Subtract(Amount);
Account2.Add(Amount);
Database.Commit;
except
Database.Rollback;
raise;
end;
The choice of the driver is very important when deploying an application. Here are some factors to consider.
There are three main types of database architectures, single-user, shared file multi-user, and client/server multi-user. Single user is fine for desktop applications where the data is only access by one application at a time. In the shared file multi-user architecture, every application has direct access to the data and is responsible to make changes to the shared data. In the client/server approach, only the server has access to the shared data, and the applications (clients) send requests using a network protocol to the server in order to access the data.
SQL (structured query language) is "the" standard for database access. Most database servers are SQL based. The Spider ODB supports SQL backends, and requires a minimal knowledge of SQL. All modifications to a database are done with the TObjectUpdate class, where the SQL statements are created automatically for the user. The queries in the database are done with the TObjectQuery class, and requires the user to be familiar with the SQL select statement.
ISAM access (indexed sequential access method) often provides a more intuitive access to the data. All ISAM access, both modifications and queries are performed with the TObjectFile class.The primitives act directly on an entire object file, and are straight foward; insert, delete, update, first, next, last, previous. ISAM access is often associated with desktop databases, but surprisingly, there are some ISAM based database that offer robust client/server operation. For example, Btrieve, now part of Pervasive.SQL, is used in critical applications using an ISAM access.
Some drivers support both SQL and ISAM access.
How quickly can objects be read/written to the database? Most database engines offer good performance especially with modern operating systems. Most operating systems generally perform extensive disk caching, allowing most drivers to achieve a good performance. Some drivers may suffer abnormal performance degradation as the size or the number of simultaneous users increases. It is important to test the driver and underlying database engine under the appropriate conditions. As the number of users increase beyond a certain threshold (somewhere between 10-30 users), only the client/server architecture will provide adequate performance.
How many objects will be stored in the database? All drivers will support thousands of objects, but when dealing with millions or even more objects, this is an important consideration. Many database engines, especially engines with originated on the desktop, are limited to 2 or 4 gigabyte file sizes. Some database engines also store all tables into one file, limiting the total database size. The Spider Object Database makes extensive use of BLOB fields, so a database which stores BLOB’s separately from the primary file may be considered.
As stated previously, there are two choices for multi-user databases; the shared file and client/server architectures. The shared file model is simpler to deploy, but beyond a certain number of users (about 10-30), its performance will degrade considerably. This is because each application must access the shared data directly, created a large amount of network traffic. The client/server approach becomes the only alternative when performance degrades due to a high number of simultaneous users.
If the database must be available 24 hours/day, 7 days/week, the database must be very robust. Robustness is often associated with file corruption. Database files sometimes become corrupted after the abnormal termination of applications. Single user and shared file multi-user databases are more subject to file corruption, as the applications themselves make changes to the database. For example, a system crash while updating an object may cause file corruption. Client/server systems are less subject to file corruption, as only the server performs changes to the database, and the server is often designed in a very robust manner in order to recuperate by rolling back partial changes even after a system failure.
Driver | Multi-user
shared file |
Multi-user
client/server |
Transaction
support |
ISAM | SQL | Comments |
TBdeOdbDriver, Borland Database Engine (Paradox and Dbase) |
|
|
|
|
Yes | Included with Delphi |
TBtrieveOdbDriver, by Pervasive Software |
|
|
|
Yes | No | |
TQdbOdbDriver, Quick Database components by Robert R. Marsh |
|
|
|
Yes | No | Lightweight driver which compiles directly in executable file. Supports only one index of type string. |
TDbIsamOdbDriver, by Elevevate Software |
|
|
|
Yes | Yes | Compiles directly in executable file. Client/server may be available in future releases. |
TAdvantageOdbDriver, by Extended Systems |
|
|
|
Yes | Yes | |
FlashFiler, by Turbo Power |
|
|
|
Yes | Yes | |
Interbase Express, by Borland | No | Yes | Yes | No | Yes |
This section contains sample code constructs often used with the TObjectFile class.
This fragment may be used to traverse all the objects of an object file.
var
Person :
TPerson;
if PersonFile.FindFirst then
repeat
Person :=
PersonFile.CurrentObj as TPerson;
…
until not PersonFile.FindNext;
This example shows how to search for a person called ‘John Smith’ in a TPersonFile.
var
Person :
TPerson;
if not PersonFile.IsEmpty then
begin
PersonFile.IndexName
:= 'LastFirstName';
Person :=
(PersonFile.CurrentObj as TPerson);
Person.LastName
:= 'Smith';
Person.FirstName
:= 'Joe';
if PersonFile.FindExact
then
begin
Person := (PersonFile.CurrentObj as TPerson);
…
end;
end;
This example shows how to insert a new person into a TPersonFile.
var
Person :
TPerson;
Person := TPerson.Create;
try
Person.LastName
:= 'Smith';
Person.FirstName
:= 'John';
Person.SocialInsuranceNo
:= '111-111-111';
Person.Email
:= 'jsmith@servername.com';
Person.BirthDate
:= EncodeDate(1965, 12, 31);
PersonFile.CurrentObj
:= Person;
PersonFile.Insert;
finally
Person.Free;
end;
An update operation must be performed after positioning the object file with a find operation.
var
Person :
TPerson;
if not PersonFile.IsEmpty then
begin
PersonFile.IndexName
:= 'LastFirstName';
Person :=
(PersonFile.CurrentObj as TPerson);
Person.LastName
:= 'Smith';
Person.FirstName
:= 'Joe';
if PersonFile.FindExact
then
begin
Person := (PersonFile.CurrentObj as TPerson);
Person.Email := := 'jsmith@newservername.com';
PersonFile.Update;
end;
end;
This example deletes the first object in a TPersonFile. After the deletion, the object file is positioned to the next object.
if PersonFile.FindFirst then
PersonFile.Delete;
This section contains sample code constructs to be used with the TObjectUpdate class.
This example shows how to insert a new person using a TObjectUpdate.
var
Person :
TPerson;
Person := TPerson.Create;
try
Person.LastName
:= 'Smith';
Person.FirstName
:= 'John';
Person.SocialInsuranceNo
:= '111-111-111';
Person.Email
:= 'jsmith@servername.com';
Person.BirthDate
:= EncodeDate(1965, 12, 31);
PersonUpdate.Insert(Person);
finally
Person.Free;
end;
In order to peform an update, the object to update must be identified by the primary key defined in the object-relational mapping.
var
Person :
TPerson;
Person := TPerson.Create;
try
Person.LastName
:= 'Smith';
Person.FirstName
:= 'John';
Person.SocialInsuranceNo
:= '111-111-111';
Person.Email
:= 'jsmith@newdomain.com';
Person.BirthDate
:= EncodeDate(1965, 12, 31);
PersonUpdate.Update(Person);
finally
Person.Free;
end;
An update will completely the existing object matching the values of the primary. Generally, the current object should have been read with a TObjectQuery before updating it.
In order to delete an object, the object to delete must be identified by the primary key defined in the object-relational mapping.
var
Person :
TPerson;
Person := TPerson.Create;
try
Person.LastName
:= 'Smith';
Person.FirstName
:= 'John';
PersonUpdate.Delete(Person);
finally
Person.Free;
end;
This section contains sample code constructs to be used with the TObjectQuery class.
An SQL query needs to be specified to retrieve objects using the TObjectQuery class. The select section of the statement must include at minimum the "object" field name from the object-relational mapping that was used to create the TObjectQuery instance. The query must be supplied in the Select property.
For example, this is simplest select statement for an object file called PSonFile:
select object
from PSonFile
Fields defined by the object-relational mapping may be used to specify the where clause. The fields used in the where clause must be included in the select part of the statement. For example, to select all persons having reached 18 years of age:
PersonQuery.Select.Clear;
PersonQuery.Select.Add('select
object, age');
PersonQuery.Select.Add('from
PSonFile');
PersonQuery.Select.Add('where
age >= 18');
PersonQuery.Open;
Once a query has been specified and opened, it is possible to navigate through the objects of the query using the FindFirst, FindNext, FindLast, and FindPrior methods. For example to traverse all the objects in a query:
var
Person :
TPerson;
if PersonQuery.FindFirst then
repeat
Person :=
PersonQuery.CurrentObj as TPerson;
…
until not PersonQuery.FindNext;
It is often needed to lookup an object based on its primary key. The TObjectQuery class allows searching for a unique object based on the primary key, without having to contruct a query statement. The Search method will search for an object based on the primary key fields from the object. For example, to lookup the information on 'John Smith':
var
Key
: TPerson;
Person
: TPerson;
begin
Key
:= TPerson.Create;
try
Key.LastName := 'Smith';
Key.FirstName := 'John';
Person := PersonQuery.Search(Key);
if Person <> nil then
begin
...
end;
finally
Key.Free;
end;
end;
The Spider Object Database is designed with an open architecture. New drivers may created to access other databases, or even create multi-tier solutions, where the driver could send requests to a middle tier rather than to a database engine directly.
The TOdbDriver class provides abstract methods which must be overriden when creating a new driver. The online help file under the TOdbDriver entry details all the methods which must be overriden. A good example is OdbDbIsm unit which implements a driver for DBISAM Database Engine which supports both ISAM and SQL access.
During the development of a driver, the automated tests in DriverTest should be used to test the correct operation of the driver. A good example, is to review the TDbIsamOdbDriverTest class in the OdbDbIsm unit. The test cases must be registered in the RegTests unit.