Table of Contents
The following paragraphs outline design and functionality of the IOPC 2 library predecessors.
The common predecessor of IOPC, IOPC 2 and POLiTe 2 libraries - POLiTe - represents a persistence layer for C++ applications. The library itself is written in C++. Applications incorporate the library by including its header files and by linking its object code. The library offers following features:
Persistence of C++ objects derived from specific built-in base classes. Class hierarchies are mapped vertically.
Persistence of all simple numeric types and C strings (char*).
Query language for querying persistent objects.
Associations between persistent objects. Ability to combine more associations to manipulate indirectly associated instances.
Simple database access.
Common services like logging or locking.
Even though the library can be divided to several functional units, it compiles as one shared library. The architecture of the library is outlined in the Figure 3.1, “Architecture of the POLiTe library”. The main functional units are discussed in the following sections.
POLiTe contains several classes that provide database access. At the time, the library supports the Oracle 7 database and the code uses OCI[11] 7 interface to access it. The classes are accessed via common interface that can be used for implementing other RDBMs to the library.
The interface consists of a set of abstract classes - Database
, Connection
and Cursor
. Communication with the database flows exclusively through this interface and its implementation (OracleDatabase
, OracleConnection
and OracleCursor
). The interface Database
provides a logical representation of a database (e.g. an Oracle instance), Database
can create one or more connections (Connection
) to the database. The Connection
interface represents the communication channel with the database. Using the implementations of the Connection
interface it is possible to send SQL statements to database and receive responses in the form of cursors (Cursor
). The response consists of a set of one or more rows that can be iterated through the Cursor
.
Every persistent class maintainable by the POLiTe library has to be described by a set of pre-processor macro calls. These calls are included directly into the class definitions or near them. Description of the class attributes and the necessary mapping information has to be provided together with declaration of every persistent class. Metainformation covers class name, associated database table, parents, all persistent attributes with their data types and corresponding table columns and more. For complete list see [08]. Example 3.1, “Definition of a class in the POLiTe library” displays definition of our classes Person
and Student
in the POLiTe library.
Example 3.1. Definition of a class in the POLiTe library
class Person : public PersistentObject { // Declare the class, its direct predecessor(s) ... CLASS(Person); PARENTS("PersistentObject"); // ... and its associated table FROM("PERSON"); // Define member attributes dbString(name); dbShort(age); // Primary key OID is inherited from PersistentObject // Map other attributes MAP_BEGIN mapString(name,"#THIS.NAME",50); mapShort(age,"#THIS.AGE"); MAP_END; }; // Define method returning pointer to the prototype CLASS_PROTOTYPE(Person); // Define the solitary prototype instance Person_class PROTOTYPE(Person); class Student : public Person { CLASS(Student); PARENTS("Person"); FROM("STUDENT"); dbString(studcardid); MAP_BEGIN mapString(studcardid,"#THIS.STUDCARDID",20); MAP_END; }; CLASS_PROTOTYPE(Student); PROTOTYPE(Student); // Student_class class Employee : public Person { CLASS(Employee); PARENTS("Person"); FROM("EMPLOYEE"); dbInt(salary); MAP_BEGIN mapInt(salary,"#THIS.SALARY); MAP_END; }; CLASS_PROTOTYPE(Employee); PROTOTYPE(Employee); // Employee_class
Because the library needs to keep track of the dirty status of persistent objects, the programmer has to maintain this flag either by himself or better he should restrict manipulation with persistent attributes to the use of getter and setter methods defined by the macros. For every class T
described by these macros the library creates an associated template class prototype Proto<T>
. The solitary instance of this prototype class holds information about the metamodel described by the macros and provides the actual database mapping. Prototypes are registered within the ClassRegister
. Using ClassRegister
, the library and/or application can search for prototypes by their names, and access methods needed for CRUD[12] operations.
Persistent classes inherit their behaviour from one of four base classes defined in the library - the Object
, ImmutableObject
, DatabaseObject
or PersistentObject
class. Depending on what the parent is, different features of the persistence are supported:
Object
- instances of descendants of this class can be obtained as database query results. These objects do not have any database identity and can represent results from complex queries containing aggregate functions. More instances can thus be the same.ImmutableObject
- instances of this class have a database identity mapped to one or more column(s) in the associated table (or view) and represent concrete rows in database tables or views. They can be loaded repetitively, but theImmutableObject
class descendants still do not propagate changes made to them back to the database. To use this class as a query result, the query has to return rows that match rows in corresponding database tables or views.The
DatabaseObject
class is much the same as theImmutableObject
, but changes are propagated back to the database.The
PersistentObject
class offers the most advanced persistence options. ThePersistentObject
defines and maintains a unique attribute OID that holds the identity of everyPersistentObject
's instance within the database. Unlike previous classes, persistence of whole type hierarchies is expected and supported.
As mentioned before, the library offers vertical mapping for descendants of the PersistentObject
. Tables related to mapped inheritance hierarchies are joined using the surrogate OID key. If using DatabaseObject
descendants, the object model can be created upon an existing (and in case of ImmutableObject
descendants even upon the read-only) database tables with arbitrary keys. In this case, however, no inheritance between classes is allowed.
Associations in the POLiTe library are not modelled primarily as references but as instances of the Relation
class. There are five subclasses of this class - OneToOneRelation
, OneToManyRelation
, ManyToOneRelation
, ManyToManyRelation
and ChainedRelation
according to cardinality of the association. Their names describe which kind of relation between the underlying tables they manage. ChainedRelation
is built from other relations and it can be used to define relation for indirectly associated objects. Example 3.2, “Associations in the POLiTe library” demonstrates how a one-to-many relation between the Employee
and Student
classes can be created and used.
Example 3.2. Associations in the POLiTe library
OneToManyRelation<Employee, Student> Employee_Student_Supervisor( "EMPLOYEE_STUDENT_SUPERVISOR", dbConnection ); // Let's suggest that the Supervisor variable represents // a reference to a "Ola Nordmann" Employee persistent object // and Student represents a reference to a "Richard Doe" // persistent object. // Create a supervisor relation between "Ola Nordmann" // and "Richard Doe". Employee_Student_Supervisor.InsertCouple( *Supervisor, *Student );
The relation can be queried for objects on both of its sides. So we may run queries like "Which students are supervised by Ola Nordmann?", "Who is the supervisor of Richard Doe?" or even more complex ones, but that would be out of the scope of this thesis.
The one-to-many relation can be replaced by a reference to s supervisor in the Student
class definition:
... dbPtr(supervisor); dbString(studcardid); MAP_BEGIN mapPtr(supervisor,"SUPERVISOR"); ...
Usage of the references is closer to the object-oriented approach in which we navigate using pointers or references to gain access to the related objects. The drawback is, that the navigation is usually one-way and in this case, the retrieval of all supervised students of an employee is not trivial.
Persistent objects can enter one of the following states (see the state diagram in Figure 3.2, “POLiTe persistent object states”):
Transient - Each new instance of persistent class enters this state. The instance data are stored only in the application memory and are not persisted.
Local copy - A persistent image of the transient instance can be created by calling the
BePersistent()
method. The method inserts attribute values of the instance to the database. The memory instance can be deallocated at any time as it is considered as a cached copy of the inserted database data. This state can be entered also at a later time when loading a persistent instance which has no local copy in the application memory.Locked local copy - To prevent the local copy deallocation, the local copy can be locked in the application memory. Local copy is not deallocated until its lock is released. After unlocking, the locked local copy enters the local copy state.
Persistent instance - A persistent object can enter this state if its local copy is removed from the application memory. During the state transition, the changes in the local copy are usually propagated to the database. The object exists now only in the database; it can be loaded later and enter one of the local copy states.
All local copies and local locked copies are managed by the ObjectBuffer
which acts as a trivial object cache. The buffer is implemented as an associative container between object identities and local object copies. If the buffer is full, all non-locked local copies are freed and dirty instances updated in the database.
Because a persistent object can exist in one of those states, library uses indirect references to access the object's attributes. Users do not have to know whether the object is loaded into the object cache or if it exists only in the database. Users can just access it via the Ref<T>
reference type using the overloaded ->
operator. The library looks for the requested instance in the object cache and if not found, it loads it from the database. C++ chains the operator ->
calls until it gets to a type that does not overload the ->
operator and there it accesses the requested attribute or calls the requested function. So if the variable e
is of the Ref<Employee>
type, the following expression:
e->salary(65000);
does not invoke the setter method on the Ref<Employee>
instance, but it looks for the object in the object cache, loads it eventually, and invokes the setter method on it. The process is illustrated by the Figure 3.3, “POLiTe references”.
POLiTe allows the users to specify several concurrent data access strategies the ObjectBuffer
will use. These strategies are used to influence the safety or speed of concurrent access and cached data coherence.
Updating strategy - determines whether changes done to local copies are propagated to the database immediately or they can be deferred.
Locking strategy - determines how the rows in the database are locked when they are loaded into local copies. Shared, exclusive or no locking can be requested.
Waiting strategy - if the application tries to access a locked database resource (by another session), this strategy specifies whether the application waits until the resource gets unlocked or an exception is thrown.
Reading strategy - determines behaviour of the persistence layer if a local copy is accessed using the indirect reference
Ref<T>
. Local copy can be either used right away or it can be refreshed with the data stored in the database. The refresh option can be speeded up by comparing timestamps of the local copy and of the stored image.
Object manipulation is illustrated by the Example 3.3, “Persistent object manipulation”. Two objects - an employee and a student which is supervised by that employee are created as transient instances and inserted into the database. The BePersistent()
call returns references to unlocked local copies of created objects. Then the salary of the employee is modified and the change propagated to the database. In the end, the student object is deleted from the database and also from the memory.
Example 3.3. Persistent object manipulation
// Create a new employee Employee* e = new Employee(); e->name("Ola Nordmann"); e->age(45); e->salary(60000); Ref<Employee> employee = e->BePersistent(dbConnection); // Create a new student supervised by the employee created Student* s = new Student(); s->name("Richard Doe"); s->age(22); s->studcardid("WCD-3223"); s->supervisor(empl); Ref<Student> student = s->BePersistent(dbConnection); // Update the employee's salary e->salary(65000); e->Update(); // Propagates the change to the database // The change could be propagated immediately if the // updating strategy was set to the "immediate" setting. // Delete the new student from the database s->Delete(); );
Queries in the POLiTe library search for objects of a specified class. Search criteria restricting the result set can be specified. Queries are represented as instances of the Query
class which contains only two data fields: The search criteria (in fact the WHERE clause of the final SELECT statement together with the ORDER BY clause specification) determines what object will be returned and how the result will be ordered. The search criteria can be written using SQL (referencing physical table and column names) or using a C++-like syntax. The C++-like syntax hides the O/R mapping complexity and allows the users to use more convenient class and attribute names. The query objects can be then combined using the C++ !, && and || logical operators. Results of the query execution are accessed using instances of the Result<T>
template class. The template is used similarly to the Ref<T>
template. Example 3.4, “Queries in the POLiTe library” illustrates how the queries are created, combined and executed.
Example 3.4. Queries in the POLiTe library
// All employees with salary > 40000 Query q1("Employee::salary > 40000") // All employees with the first name Ola Query q2("Person::name LIKE 'Ola %'); // All employees with salary > 40000 having the first name Ola Query q3 = q1 && q2; // Order the result by the salary descending. q3.OrderBy("Employee::salary DESC"); // Execute q3 and iterate through the result Result<Employee>* result = Employee_class(q3, dbConnection); while (++(*result)!=DBNULL) { // members of the current object are accessible // using (*result)-> }; result->Close(); delete result;
The library provides solid and rich-featured ORM solution. However, there are several areas in which the library can be improved:
Transparency - persistent classes have to be precisely described by macros. Typos in this description may lead to unclear compile time or runtime errors. Attributes must be accessed via the getter and setter methods.
Library design - library is one monolithic block and compiles into one shared library. There is no other way to add additional database drivers or features than changing the makefile and recompiling the library. Same applies to the library configuration - many parameters are configured as preprocessor macros. Changing them implies library recompilation.
Database dependency - without modifications, the library supports only the Oracle platform. Adding new database support supposes to derive new descendants of
Database
,Connection
andCursor
classes, implement their code and recompile the library. The library also contains several SQL fragments that are not separated into the database driver layer.
These disadvantages are addressed mostly by the IOPC library [04] and its descendant described further in this thesis. But first, we will look at the performance enhancement provided by the succeeding library POLiTe 2.