The most complex configuration of the IOPC 2 library we can use is iopclib. It contains all features described to this point, plus it offers object persistence using four kinds of database mapping - vertical, horizontal, filtered and ADT. Objects that can be persisted must derive from the DatabaseObject
or OidObject
classes. We will start with the OID objects as they are easier to handle and offer more functionality. Let's take the classes defined in the section called “Inspecting objects with reflection” and prepare them for the persistence layer.
// Use the iopclib headers #include "iopclib/iopcHeaders.h" using namespace iopc; class Person : public iopc::OidObject { public: EShort age; EString name; }; class Employee : public Person { public: EInt salary; }; class Student : public Person { public: EString studcardid; DbPtr<Employee> supervisor; static void iopcInit(iopc::Type& t) { t["db.mapping.type"].setStringValue("horizontal"); } }; class PhdStudent : public Student { public: EShort scholarship; static void iopcInit(iopc::Type& t) { t["db.mapping.type"].setStringValue("filtered"); t["db.mapping.insertto"].setStringValue("::Student"); } };
We changed the predecessor of the Person
class to OidObject
as we intend to use OID objects. We also specified some metadata that modify the default mapping type from vertical to horizontal for the Student
class and filtered for the PhdStudent
class. If filtered mapping is used, the db.mapping.insertto
metadata must also be specified. The ::Student
value tells the library that attributes of the PhdStudent
class will be stored into the table associated with the Student
class.
Before we can start using these classes, we must prepare required database schema. ScriptsGenerator
can be used for this task as described in the section called “Database mapping”. Once more, the code snippet that creates the database schema for persistent classes in the application:
vector<string> script; script = ScriptsGenerator::getDbCreateScript(conn->getDriver()); for(vector<string>::const_iterator it = script.begin(); it != script.end(); it++) { conn->sqlNonQuery(*it); }
When manipulating persistent objects, we need to use CachedConnection
decorator for the standard connection usually obtained. There is also the CachedDatabase
class which decorates the "basic" database and creates already decorated CachedConnections
. The reason why we are using these decorators is that the caching layer is tightly integrated with the object persistence layer and object persistence cannot be used without it. The CachedConnection
provides us with additional methods for cache manipulation. It can be created in the following way:
CachedDatabase* db = new CachedDatabase( DriverManager::getDriver("IopcOracle10g").getDatabase("XE"), new VoidCache()); CachedConnection* conn = (CachedConnection*)db->getConnection("username=iopc;password=iopc"); conn->open();
CachedConnection
needs to have an associated cache. In this scenario we use the basic cache implementation - the VoidCache
- see the section called “The cache layer”.
Now, we have all the prerequisites we need to be able to manipulate persistent objects. First, we create some persistent objects and store them in the database.
DbPtr<Person> person; person->name = "Mary Major"; person->age = 60; person.bePersistent(conn); DbPtr<Employee> employee; employee->name = "Ola Nordmann"; employee->age = 45; employee->salary = 60000; employee.bePersistent(conn); DbPtr<Student> student; student->name = "Richard Doe"; student->age = 22; student->studcardid = "WCD-3223"; student->supervisor = employee; student.bePersistent(conn);; DbPtr<PhdStudent> phd; phd->name = "Joe Bloggs"; phd->age = 27; phd->studcardid = "PHD-1234"; phd->scholarship = 12000; phd->supervisor = employee; phd.bePersistent(conn); conn->commit();
Transient instance of a persistent class is automatically created by declaring a variable of a database pointer type as shown in the example. Its attributes are accessible using the ->
. The DbPtr::bePersistent()
method then transfers the ownership of the transient instance to the cache associated with the connection conn
. In this case, the VoidCache
immediately inserts the object into corresponding database table.
If we used another cache, the objects might be inserted into the database later. However, the CachedConnection::commit()
method call forces the associated cache to store all changes (including new objects) into the database before the actual commit is issued to the database. This update can be invoked manually by calling CachedConnection::updateDirty()
. Actually, commit calls CachedConnection::updateDirty()
to perform the update. Analogously, CachedConnection::removeAll()
is called before CachedConnection::rollback()
to discard all changes or new objects by emptying the associated cache. (The objects will be loaded from database into the cache again as soon as they are accessed).
Note that when we assign a database pointer to the supervisor
reference attribute, the database pointer must already point to an instance which is managed by a cache. That means that DbPtr::bePersistent()
must have already been called on the database pointer before.
Every time we access attributes of a persistent object managed by a cache using the ->
operator, the object is looked up in the cache and returned as a cache pointer - CachePtr
. Then the attribute is accessed and the cache pointer instance is destroyed. If we use the VoidCache
, the object is even loaded from the database every time when the CachePtr
is created and stored back when destroyed. If the object is transient, just the CachePtr
is created and destroyed repeatedly, cache is not used.
If we want to prevent such behaviour to speed up repeated access to the persistent object attributes, we need to store the CachePtr
instance into a variable. This can be done in two ways:
// Using the getCachePtr() method CachePtr<PhdStudent> phdPtr = phd.getCachePtr(); // or by dereferencing the DbPtr using the * operator: CachePtr<PhdStudent> phdPtr = *phd;
We may now modify the attributes, the persistent object is locked in the cache. If we want the cache to take back the control of the persistent object, we must release the cache pointer. Cache pointer can be released either explicitly by calling the CachePtr::release()
or implicitly by destroying its instance:
// explicitly CachePtr<PhdStudent> phdPtr = *phd; phdPtr->scholarship = 13000; phdPtr->supervisor = employee; phdPtr.release(); // implicitly: { CachePtr<PhdStudent> phdPtr = *phd; phdPtr->scholarship = 13000; phdPtr->supervisor = employee; }
Persistent objects can be locked for read-only access using the DbPtr::getConstCachePtr()
call. We cannot modify the object attributes, but the advantage is that the cache lock used in this case is not exclusive and the object can be locked in this way by more threads at a time.
Cache pointers may be copied. The lock is released as soon as last instance of the cache pointers is released or destroyed. Database pointers may also be copied, the behaviour of the copies depends on whether we are copying a pointer to a transient object or to an object managed by a cache. Copies of the transient objects are reference counted. We are not allowed to call the DbPtr::bePersistent()
method if more than one reference pointing exists. In such case an exception would be thrown.
By creating a copy of a database pointer pointing to a cache managed object, only its identity (PersistentIdentification
) and connection information are copied. If we delete the object using one of the pointers from cache and from the database and then access its attributes using different pointer, we get an exception saying that such object could not be found in the database.
// Copying cache pointers CachePtr<PhdStudent> phdPtr2 = phdPtr; // Copying database pointers DbPtr<PhdStudent> phd2 = phd;
DbPtr
provides two additional noteworthy methods - DbPtr::dbDelete()
and DbPtr::cacheDelete()
. The first method deletes the persistent object from the database as well as from the cache, the second one hints the cache to drop its cached local copy (the hint may be ignored).