Persistence Smith in Detail
Data store driver
Object model
Attribute mappings
Class and inheritance mappings
Relationship mappings
Object access
Dereference by demand
Object identity
Id factory
Collection adapters
Object queries
Data service
Object input/output
Instance management
Transactions and threads
Concurrency controlPreface
The purpose of this toolset is to translate object models to relational ones and to manage the lifecycle, storage, and concurrent access to persistent business objects. In addition, it provides a multithreaded transactional environment to object-oriented applications which adds isolation and recovery features. Business objects can participate in hierarchical and peer relationships that were previously difficult to represent with pure relational technology by itself.
Persistence Smith consists of a CASE schema designer, a set of runtime frameworks and driver classes that interface Persistence Smith to a number of today's most widely used relational databases. These datastore technologies include heavy engines, such as MS SQLServer, Sybase, SQLAnywhere, Oracle, Gupta SQLBase and lightweight MS Access.
Object-relational layer includes a number of integrated components that collectively form the persistent services. These components is broken down into several distinct systems. All of them run within the client application and utilize a direct connection to one or more database servers.
Data store driver
At the lowest level, the Data Store Driver (DSD layer) provides the application interface for controlling transactions, reading, writing and removing objects to and from the database. Also within this layer is the object query framework, which allows applications to issue queries to their object model. At this level, the concept of tables and columns does not exist and the developer issues queries against the object model and not against the database.
The DSD is responsible for generating and preparing all SQL DML statements and SQL DQL queries. This layer accepts i/o requests from the top level application framework, interfaces with the persistent objects and forms low-level i/o requests to the database engines. In effect, it performs the acts of object model flattening and isolates the details of specific databases from the object oriented application.
The DSD has a standard interface with ODBC.
Object model
Persistence Smith prosecutes its operations based on the data dictionary defined by developer. This schema is specified in the CASE framework of Visual Programming Armoury. This framework produces a set of class tables and interface methods, C++ header files for the defined object model as well as SQL DDL script for the database construction. To read and write the object data, Persistence Smith invokes the get/set attribute methods of the target object instead of attempting to understand it's binary layout. This is called the direct-access protocol.
Developer defines a business class and it's data attributes. Then modifiers are assigned. Project-wide, class and attribute modifiers are used to specify additional mapping information and code generation options. Next, he specifies the relationships between objects. The job of the Persistence Smith is to analyze the schema and generate the appropriate data tables and object model bindings for C++.
Data definition tables are objects of metaclass. Tables are statically built when the application is loaded. Objects contain key information that was specified in the Schema Designer and are used by the persistence engine to read, create and manipulate the instances. C++ object bindings include a get/set method pair for each attribute. Persistence Smith generates most of the C++ class library for the specified business objects and regenerates it when changes are made to the model.
After all, developer encodes the functionality of business classes and check the code with a common C++ compiler. Thus and so the static control of types is ensured.
Attribute mappings
The data types associated with an object might need to be changed when the data is made persistent. Data stores typically support a limited number of data types (for example character string, float, decimal, binary). The data types in the application programming language might not match those supported by the data store. A mapping of data types is required between the in-memory object and its stored form. The persistence service provides tools for defining the stored schema and the mapping to a language-specific object. When the mapping is adequately defined, store and retrieve methods can be generated for accessing the object's data.
The attribute types defined in VPA schema designer can be any of the core literal datatypes, an object type or an extended type defined by Persistence Smith. The extended types include: BCD, Date, Time, Binary, BLOB. The following chart illustrates the Schema Designer modifiers and the appropriate C++ and SQL data type mappings:
Schema Field Class Attribute SQL Column SHORT short SMALLINT USHORT UShort SMALLINT LONG long INTEGER ULONG ULong INTEGER FLOAT float FLOAT DOUBLE double DOUBLE BOOL BOOL INTEGER BYTE BYTE CHAR(1) STRING<n TC_CString VARCHAR(n) CHARARR<n char[n] VARCHAR(n) DATE TC_CDate TIME TIME TC_CTime DATETIME BCD<n,d TC_CBCD(n,d) DOUBLE BINARY<n TC_CBinArray(n) VARBINARY(n) BLOB TC_CBlob IMAGE Class and inheritance mappings
For the most part, a class is mapped to a relational table. There are however many variations that can be introduced when inheritance and aggregation relationships are required. For example, the given class B being derived from A. The additional attributes introduced by class B is grouped into a new table that will be joined to the primary table. This inheritance scheme also requires a class identifier to be added to the primary table.
Another important class mapping is that of aggregate classes and simple data types. Persistence Smith uses one-to-one association to represent aggregation relationship. Aggregate objects can be embedded within other aggregates. The nesting level of aggregate mapping can be arbitrarily deep.
Relationship mappings
There are many ways in which object instances can relate to one another. In object-oriented applications these relationships are maintained with pointers and collection classes. In a database, primary keys, foreign keys and association tables are used to implement relationships. The types of relationships supported by the Persistence Smith include one-to-one, one-to-many, many-to-many and ternary associations.
One-to-one and one-to-many relationships are mapped by allowing the primary and foreign keys to be specified. Understanding the key relationships allows relations to be created by the user and loaded from the database. In C++ these relationships are represented with pointers (one-to-one), collections (one-to-many) and demand references.
One-to-many and many-to-many relationships can be represented in a database with an association table. Tables contain two or more foreign keys to related types. Each row forms a specific bidirectional relationship between the referenced entities. For the object oriented client these associations are mapped to nodes in a collection. Each collection implements a one-to-many relationship, whereas a pair of such collections are used in the many-to-many case.
More complex association tables are used to add link properties and build ternary relationships. At that rate additional foreign keys and data columns are added to the basic association table described above. In these cases the developer must use a new class to represent such relationships.
Object access
Persistence Smith uses an interface-based approach to persistence. Unlike other schemes, it makes no attempt to understand the binary layout of the object or the object model composition. Data is extracted from an object by invoking the get methods. Likewise an object's value is initialized by invoking the set methods for each attribute. For each supported datatype the get/set methods must be coded to a well defined signature.
The get/set methods can be coded by the developer, generated by the schema builder or a combination of both. Custom access methods are sometimes needed when a developer wishes to store an attribute in a form that is different than in the object. For example, custom get/set methods could be used to represent an enumerated value as a string mnemonic in a database.
Dereference by demand
Many storage schemes introduce the concept of deep and shallow operations. Persistence Smith also supports such ideas. To deep restore and object for example, means to load the object from the database, followed in turn by loading all objects that are referenced by the first object and so on. This can be very dangerous with some business models because it can cause a large part (if not all) of the database to be loaded into memory. A shallow read does not cause this problem since only the requested object will be loaded into memory. However it is very common for the application to want the deep variety only on a selected subset of the object graph.
Persistence Smith addresses this requirement by allowing relationships to be managed with demand references, which are specialized pointers that cause the related objects to be loaded from the database when the application accesses it. This behavior is selected by using the "Demand Ref" modifier for the desired relationship attribute. A demand reference is simply a C++ pointer class that keeps track of whether the relationship has been loaded or not. An instance of a demand reference is used in place of a simple pointer in the C++ class definition.
Demand reference classes are defined by the schema builder as subclasses of the TC_CPsDemRef base class. This class is defined as follows:
class TC_CPsDemRef
{protected:
int m_RefStatus;public:
TC_CPsDemRef();
int RefStatus() const;
void UnResolve();
int GetObject(TC_CPsObject *owner, const char *attr_name) const;
void SetRef(void *obj);
BOOL IsResolved() const;
BOOL IsAssigned() const;};
The subclasses defined by the schema builder implement a number of overloaded operators that allow it to be used with the same syntax as a normal C++ pointer.
The demand reference objects are used primarily by the get methods that return related objects. As an example, PsDeal object contains a PsVenture object. In this case, the schema builder generated a method named GetSupplier to return the pointer to the venture. The code below illustrates the implementation of this method:
PsVenture *PsDeal::GetSupplier()
{if(!m_Supplier.IsResolved()) m_Supplier.GetObject((TC_CPsObject *)this, "m_Supplier"); return m_Supplier.GetRef();
}
When the application invokes this method to get the supplier for a deal, the implementation first checks to see if the objects have been read from the database. If not, the GetObject method call is used to load the one-to-one relation from the database. Once loaded, a pointer to the referenced object is returned. Persistent collections may be dereferenced by demand the same way.
Demand reference allows an application to utilize shallow reads for safety and high performance. Relations from the root objects are then subsequently loaded as the application needs them. This approach is generally transparent to the application.
Object identity
Objects are typically distinguished from one another by unique identifiers. The identifiers are used to refer to an object either by the application or by one object referring to another. Object identity is intrinsic to object oriented programming. Each object has a unique identity, independent of its value, due to the fact that it has an address. But the concept of identity goes deeper than just an address - one can envision an object changing type without changing its address. Object identity must be maintained by persistent systems to maintain compatibility with the languages that they support. If this seems trivial, consider relational databases: a relation is a set of tuples, and since it is a set, if two tuples have the same values, they are not distinct entities, so object identity is not supported.
For an object to be persistent it must be identifiable by something other than a pointer. This is of course because the address of an object has no meaning in a database. Each time a particular object is restored, it will have a new physical address. To identify objects Persistence Smith uses two primary data elements: class handle and the primary key value of the object in the database. Each persistent class contains an m_hClass and m_OID attributes.
OIDs are used as input parameters to Persistence Smith methods such as RestoreObject and GetObject(). For example, to use the above object id to restore deal 1075, the following code would be used:
...
TC_TPsRef<PsDeal deal;
TC_PSMITH-RestoreObject(deal, PsDeal_OID(1075));
...Id factory
The general approach to object identity is to utilize the concept of primary keys. Thus Persistence Smith generally avoids getting into the business of generating object identifiers, as opposed to object databases that make extensive use of internally generated object id's. Although shifting the responsibility of key creation to the application simplifies Persistence Smith, it imposes a burden on the developer. After all, generating keys that are guaranteed to be unique in a client-server environment is not easy, and code of this type should not be littered through an application. As a compromise, Persistence Smith supports a simple object id creation by means of GenerateOID(char *db_tbl, PsInstID *inst_id) method in the module that binds Data Store Driver to object-oriented application. One common implementation of such a factory uses an SQL sequence table that always returns a number greater than the last one returned when accessed.
Collection adapters
Collection adapters are used to interface custom collection types to Persistence Smith. By default, the standard collection classes are used. These include the double-link list class, arrays, sets, and maps. If a different kind of collection were needed to manage a relationship or the user wanted to use a different container class library, an adapter would have to be defined and implemented.
To start with, the adapter class must be derived from the PsCollection base class. This class is defined as follows:
class TC_CPsCollection
{public:
virtual ~TC_CPsCollection();
virtual TC_CPsObject *GetObject(int idx) = 0;
virtual int AddObject(PsObject *obj) = 0;
virtual void RemoveObject(PsObject *obj) = 0;
virtual void RemoveObject(int idx) = 0;
virtual void Release();
virtual void SetImpl(void *obj) = 0;
virtual void *GetImpl() = 0;
virtual void *CreateImpl() = 0;
virtual void CreatePrivateImpl() = 0;
virtual TC_CPCollection *Copy() = 0;
virtual int Count() = 0;
virtual int ContainsInstance(TC_CPsInstID inst_id);
virtual int ContainsObject(TC_CPsObject *obj);};
Note operations that are defined to create collection instances, add and remove items to/from the collection. To iterate over the collection use operator[int idx] or first/next functions. This interface is used by Persistence Smith to create and manipulate one-to-many relationships.
Object queries
A query examines a group of objects and returns those that satisfy a search condition. Persistent systems differ in the query response sets they can offer. Since inheritance exists in obect-oriented systems, a query could legitimately return a heterogeneous response set. For example, a query requesting all pet animals in a certain household could return a set containing both a cat class instance and a dog class instance (both inheriting from class animal).
Just as there are various forms of queries, there are various ways to bring an object into memory. You can use an explicit routine, such as a restore object or collection read routine or you
can dereference an object using its link. Application developers must have the ability to execute both simple and complex queries to find their objects. In addition, queries must be database-independent and object-centric. Because all database vendors have variations in their SQL syntax, and few support object extensions, writing queries using the native query language of a given database will not achieve these goals. To address this, Persistence Smith presents an object query framework which translates object queries to the native SQL and apply the appropriate object to SQL mapping information.One of the main strengths of a relational database resides in its powerful querying capabilities. Object models can take advantage of sophisticated relational query engines without leaving the object model. Properly designed and indexed relational models can provide fast ad-hoc access to objects. The Query Services of Persistence Smith allow objects to be found and returned from a particular data source based on search criteria. The criteria used to examine object values are called predicate terms. A predicate term specifies:
An evaluation attribute whose value is to be examined. Evaluation attributes must be specified in a precise, non-ambiguous way that is understandable to a database. A comparison operator determines how an attribute value is compared to a key value. A comparison operator can be a relational operator for comparing scalar values, a pattern matching operator for comparing strings, or an is-a operator for determining class membership. A key value is the value to which a specified attribute is compared. A key value can be a link or a value for any type of attribute allowed in a query. When multiple predicate terms are used in a search query, the combination of predicate terms is collectively called the predicate. To express how predicate terms are to be collectively applied, they are concatenated into a predicate using logical boolean operators. Allowed boolean operators are logical AND, OR and NOT.
Suppose that you want to find cash operations double-entried in the financial period of September 1998 with the author not from the user group of accountant-general. In addition, the result must be sorted by date, debit, credit-side in series and include accepted business operations only. In this case, the source code will consist of the following elements:
// predicate terms is concatenated with logical operators
TC_CPsPredicate search
(TC_CPsPredTerm(TC_CPsMember("m_OperDate"), op_GE, TC_CDate("1998-09-01")) &&
TC_CPsPredTerm(TC_CPsMember("m_OperDate"), op_LE, TC_CDate("1998-09-30")) &&
(TC_CPsPredTerm(TC_CPsMember("m_Debit.m_Code"), op_MATCHES, "Cash%") ||
TC_CPsPredTerm(TC_CPsMember("m_Credit.m_Code"), op_MATCHES, "Cash%")) &&
TC_CPsPredTerm(TC_CPsMember("m_Author.m_UserGroup.m_Code"),op_NOT_MATCHES, "Accountants/Seniors%") &&
TC_CPsPredTerm(TC_CPsMember("m_IsAccepted"), op_NOT_NULL));
// build clause for ordering
TC_CPsSort sort
{TC_CPsSort(TC_CPsMember("m_OperDate")) +
TC_CPsSort(TC_CPsMember("m_Debit.m_Code")) +
TC_CPsSort(TC_CPsMember("m_Credit.m_Code"));}
// define the query and the result type
// starting point objects are the instances of PsEconomicOperation
TC_CPsQuery query(PsEconomicOperation_Class, search, sort);// cursor allows to control the fetching of objects during iteration
TC_CPsCursor<PsEconomicOperation cursor;try
{// determine border of client workspace
TC_PSMITH-TxBegin("TwoTier", Tx_Exclusive);// pass TC_CPsQuery object to an overloaded ExecQuery function
// which executes the query and prepares the result
TC_PSMITH-ExecQuery(query, cursor);
// note that given TC_CPsQuery object can be executed as many times as needed// define intermediate object
TC_TPsRef<PsEconomicOperation obj;// fetch the matching objects and perform useful work
while(cursor.FetchNext(obj))
{cout << obj-Get_OperDate() << "\t";
cout << obj-Get_Debit ()-Get_Code() << "\t";
cout << obj-Get_Credit()-Get_Code() << "\t";
cout << obj-Get_Sum() << endl;}
// provide cleanup
TC_PSMITH-TxCommit(TxTerm_Restart);}
catch(TC_CPsException ex)
{TC_PSMITH-TxRollback(TxTerm_Restart);
ExceptionMessage(ex);}
The query result can take the form of a simple data type (such as long, double value and string), a collection of objects or a cursor that allows the application to control the fetching of objects. This example demonstrates the ability to iterate over the returned results. If a real collection object were passed to the ExecQuery function, the collection would have been populated immediately, but the iteration method remains the same.
To improve the performance of queries, you can do any or all of the following:
cluster instances of a class tuple specify raw devices or files for database volumes set indexes tune database operating parameters turn locking and logging on and off reduce context switching by running an application with a single process The principle advantage of the query framework is to present a common object query system to all types of supported datastores. Not all drivers may support the full syntax, but the query semantics is common. In addition, query framework understands object members, subclasses and aggregate objects. This allows object based queries to be formed without knowing the underlying database schema.
Data service
In a number of places throughout this document, the notion of an object location is mentioned. A location in the context of Persistence Smith specifies two important parameters:
a connection to a specific datastore or database server a protocol for manipulating the objects that live in this database Defining a location to Persistence Smith requires creating a pair of objects with an associated name, and then registering these objects. One object, instantiated PsDataDriver class, defines the target database connection. The second object, derived from PsDataService, defines the protocol manager to use. In the current version, there exists only one data service implementation. This class implements the interface-based object access method that was described earlier.
The code fragment below illustrates an example of defining an object location for Microsoft SQL Server:
// create the data service and media driver
TC_CPsDataDriver *dd = new TC_CPsDataDriver("TcPsDrv.DLL");
TC_CPsDataService *ds = new TC_CPsDataService("TwoTier", db);try
{// register the data service with the Persistence Smith
TC_PSMITH-DSRegister(ds);
dd-Connect("Server", "Database", "User", "Password");}
catch(PsException ex)
{delete ds;
delete dd;
ExceptionMessage(ex);}
The first DS that is registered becomes the default one. Since most applications use single database, the default feature eliminates the need for applications to specify the location name for each object that it creates.
Object input/output
Persistence Smith supplies a number of methods for reading, writing, and deleting objects to/from the database. These operations are:
TCPsRetCode CreateObject(TC_CPsObject **obj,
TC_CPsInstID &inst_id);
TCPsRetCode CreateObject(TC_CPsObjRef &obj,
TC_CPsInstID &inst_id);
TCPsRetCode StoreObject (TC_CPsObject *obj,TCPsDepthMode depth = Depth_Default);
TCPsRetCode RestoreObject(TC_CPsObject **obj,TC_CPsInstID &inst_id, TCPsDepthMode depth = Depth_Default);
TCPsRetCode RestoreObject(TC_CPsObjRef &obj,
TC_CPsInstID &inst_id, TCPsDepthMode depth = Depth_Default);
TCPsRetCode ReleaseObject(TC_CPsObject *obj);
TCPsRetCode RemoveObject (TC_CPsObject *obj);TCPsRetCode StoreAssoc (TC_CPsObject *obj,
const char *attr_name, TCPsDepthMode depth = Depth_Default);
TCPsRetCode RestoreAssoc(TC_CPsObject *obj,
const char *attr_name, TCPsDepthMode depth = Depth_Default);
TCPsRetCode ReleaseAssoc(TC_CPsObject *obj,const char *attr_name);
TCPsRetCode RemoveAssoc (TC_CPsObject *obj,
const char *attr_name);
TCPsRetCode AssociateObject (TC_CPsObject *owner,TC_CPsObject *obj, const char *attr_name);
TCPsRetCode DisassociateObject(TC_CPsObject *owner,
TC_CPsObject *obj, const char *attr_name);
The short code fragment below demonstrates operations on persistent objects. This function call is already bounded with try block.BOOL FrmProducts::StoreImage(TC_CPsInstID &inst_id_prod)
{// image is untouched or not captured
if(!m_IsPictModified)return TRUE;
// though PsProduct object is already restored we
// construct an auto smart pointer ref_prod for local usage
TC_TPsRef<PsProduct ref_prod;
if(TC_PSMITH-RestoreObject(ref_prod, inst_id_prod) != rcOk)throw TC_CPsException(rcObjNotFound);
// dereference by demand of the PsProdPicture object
TC_TPsRef<PsProdPicture ref_pict = ref_prod-Get_Picture();if(!ref_pict)
{// product isn't associated with the picture,
// so create its placeholder
if(TC_PSMITH-CreateObject(ref_pict,PsProdPicture_OID(TC_PS_NULL_OID)) != rcOk)
throw TC_CPsException(rcCreateObjFail);
}
TC_BlobStruct bs;
memset(&bs, 0, sizeof(TC_BlobStruct));// provide JPEG compression if picture is captured
if(m_Picture)
{AJpegCompDestMem dest;
AJpegCompressor jc(&dest);if(!jc.FromMem(m_Picture, InFmt_DIB))
return FALSE;
bs.buf = dest.GetBuffer();
bs.size = dest.GetCompSize();}
// store picture instance
ref_pict-Set_Picture(bs);
ref_pict-SetModified();
ref_pict-Store();// one-to-one association of product and its optional photo
TC_PSMITH-AssociateObject(ref_prod, ref_pict, "m_Picture");
return TRUE;}
Note the use of exception handling. When exceptions are enabled, Persistence Smith will throw a PsException object when it encounters a severe error.
Instance management
In the above example code, you may have noticed a TC_CPSmith method called CreateObject that is used to create new persistent instances. Also, a smart pointer was used to implement the PsProdPicture pointer so that the referenced object is deleted from memory when it goes out of scope. These features help the application manage the creation and destruction of objects in memory.
it is necessary to use a CreateObject method instead of the normal C++ constructor because complications set in when inheritance is involved. In order for Persistence Smith to store an object, it must know the object's persistent id. This information is given to the object manager by registering the object. Thus the process of creating a new object that can be stored comprises generating the object instance, initializing the key attribute and registering the object.
Another argument for the use of the CreateObject method is that object factories should be used to create instances. When a specific constructor is used, the application is mandating that the object must be created in the local process. Using a factory though (such as the CreateObject method), provides location independence and allows applications to migrate to distributed applications more easily.
Persistent objects are registered when they are created and read from the database. They are unregistered when freed from memory. All registrations occur within the scope of a transaction. In addition to tracking the object pointer to object id value, Persistence Smith also tracks the relationships between persistent objects. Consider for example two Student objects, A and B, that point to the same Teacher object, C. If object A is destroyed, does it destroy C? This question has plagued C++ developers since the language was born. Some have tried to use reference counting schemes to solve this problem, but that approach breaks down when circular references occur (which happen all the time in modern business models).
For the example, Persistence Smith would keep track of the fact that both A and B point to C. In the destructor of Student, instead of destroying C, it releases it. The operation of release means that the object should be destroyed from memory unless another dependent object is pointing to it. The task of identifying dependent objects is a science in itself, which Persistence Smith handles automatically. Continuing with the example, when object A releases C, C remains in memory because B is pointing to it. When the destructor of B then releases C, it can be destroyed without side effects.
In general, instance management is not seen by applications and is handled under the covers. However, for special cases, Persistence Smith offers the following interface to the instance manager:
// instance management
TC_CPsObject *TC_CPsSmith::GetObject(TC_CPsInstID &inst_id);// reference management
void TC_CPsObject::AddRef();
void TC_CPsObject::RemoveRef();
int TC_CPsObject::RefCount();// instance properties
BOOL TC_CPsObject::IsModified();
BOOL TC_CPsObject::IsNew();
TCPsRetCode TC_CPsObject::SetModified(TCPsSetOpt is_set = opt_Set);
String TC_CPsObject::InfoStr();
void TC_CPsObject::Trace(char *title = "");One important aspect of tracking objects id's is preventing Persistence Smith from loading a second copy of an object when that instance is already in memory. Using the above example where two Students point to one Teacher, consider what happens when these objects are read from the database. Object A is read from the database which causes object C, the Teacher, to be read and associated with A. Now when B is read, a second copy of the Teacher object would be loaded if Persistence Smith did not provide a way to track instances. Eliminating duplicate objects does improve performance, but having two copies of the same instance within the same transaction is a blatant error. This problem is source for a large number of errors encountered when developing object oriented client-server software.
The last aspect of instance management to discuss is that of keeping status flags for each instance. In general, status flags are used by Persistence Smith to keep track of whether an instance is new (that is, it does not exist yet in the database yet), and whether an instance is clean or dirty. Dirty objects have been modified and must be stored if the transaction is to complete properly. Persistence Smith also ignores requests to store existing objects that have not been changed. To mark an object as modified, the programmer must use the SetModified method listed above.
Transactions and threads
Not considering persistence, transactions is the another important feature missing in object oriented languages. This is unfortunate since it is a fundamental concept that is of key importance to most business programs and processes today.
Persistence Smith provides the concept of a unit of work and recovery. Before database activity can begin, the user must start a transaction by invoking the Begin method. This technique is illustrated in the above example of object i/o. The start of a new transaction creates a logical scope for the objects that are involved in it. Each object created or restored is registered and managed within this transaction scope. The cancel and undo work is supported through the ability to rollback objects as well as database changes. So it becomes very easy to back out of changes made by the user.
The thread can be implicitly associated with a transaction context. When the application creates a transaction, the thread that created the transaction is assigned the new transaction as current. Each subsequent operation on a persistent object or other resource will be executed within the scope of this current transaction. The following methods are provided by Persistence Smith to manage transaction contexts:
class TC_CPsWorkContext
{private:
static TC_CLock m_Lock;public:
TC_CPsWorkContext();TC_CPsTxContext *Begin(const char *data_service, long tx_flag);
TC_CPsTxContext *Current();
TC_CPsTxContext *Suspend();TCPsRetCode Resume(TC_CPsTxContext *tx_cxt);
TCPsRetCode Commit(TCPsTxTerm tx_term);
TCPsRetCode Rollback(TCPsTxTerm tx_term);
TCPsRetCode Destroy(TC_CPsTxContext *tx_cxt = 0);
TCPsRetCode Restart(TC_CPsTxContext *tx_cxt);};
As an example of how transaction scopes work, recall the example above of two Student objects, A and B, that point to a single Teacher object, C. If both Student objects are restored within the same transaction then they will share a single instance of C. However, if object A was restored in one transaction, and B is restored in another, then each will gets its own copy of the Teacher object. If one of the transactions intend on changing the Teacher object, it must use the lock manager to lock the object for writing. If both transactions want to update the teacher, and they are running concurrently, then only one will be granted the lock. The other transaction would have to wait for the lock to be freed, or take another course of action. This scoping feature implements a fundamental transaction property called isolation. Database servers do this naturally, but its rarely available for clients.
Concurrency control
To manage the concurrent access of objects by two more concurrent transactions, Persistence Smith provides a Locking Service that allows objects to be locked by their identity. The locking operations provided by this service are as follows:
BOOL TC_CPSmith::LockObject (TC_CPsObject *obj, TCPsLockMode lock_mode, int timeout = 0);
BOOL TC_CPSmith::UnlockObject(TC_CPsObject *obj);Any object can posess one or more locks of any mode. However, two more transactions cannot create locks that conflict with one another. The chart below illustrates the lock modes and conflicting combinations:
Requested Read Requested Write Granted Read Ok Conflict Granted Write Conflict Conflict When a conflicting lock is requested and the caller can specify a wait time, then the invoking thread will be blocked until the lock releases or the wait time expires. The lock service works along with the transaction services. Each lock granted is stamped with the transaction scope in which it was created. All locks created by any type of transaction are destroyed if the transaction commits or rolls back.