Rob Kremer, Knowledge Sciences Institute, University of Calgary, February, 1992. 
Persistent Object Systems
Rob Kremer, University of Calgary
Abstract
This paper surveys the wide range of possible persistent object systems, from relatively simple object I/O to sophisticated OODBMS systems. The fundamental requirements of persistent object systems include rich persistent data models, seamlessness, and reasonably high performance. However, more sophisticated systems will support evolution of instances and classes, object sharing, distributed objects, platform independent objects, concurrency control, flexible transactions, and access control. Persistent systems can be classified over several dimensions such as object model used, navigation vs queries, storage type, transaction support, etc. A detailed look at the simplest persistence implementation (object I/O) is presented, along with suggestions on how this simple framework could be extended to support some of the more advanced features.

Introduction

Object persistence is a rather large topic, mostly because it is not extremely well defined. This is not very surprising because the need for object persistence springs from the object-oriented programming paradigm, and there exists no firm, agreed-upon definition of object-oriented programming either. There is some consensus that object-oriented model means abstraction, encapsulation, modularity and inheritance hierarchy [Booch 91], but there is much disagreement about whether it also means typing, concurrency, co-routines and persistence. Similarly, there is some consensus that persistence means rich data modeling and seamlessness, but there is much disagreement about whether it also means queries, object sharing, complex transactions, version control, distributed objects, platform independent objects, and security support. Based on this last statement alone, there is obviously a tremendous diversity of possible persistent systems ranging from humble class library support for object file I/O to all-encompassing multi-language, distributed, platform-independent OODBMSs with version control, security features, and support for long and cooperative transactions.

The objective of this paper is to present an overview of persistent object systems. Some of the persistence requirements for application programmers are enumerated in section 2. A rough taxonomy of persistent systems is presented in section 3. Finally, a detailed view of simple object I/O systems, the lowest level of persistent systems, is given in section 4. Borland's object I/O class library is presented as a concrete example in this last section.

In this paper, the word "persistence" will be taken to mean the very general concept of objects somehow living between invocations of a program or programs. This is taken to mean somewhat more than simple file I/O but not much more. Therefore, "persistent systems" covers anything from simple object I/O systems, to the most complex and extended OODBs. The term "OODB" and "OODBMS" will be taken to mean a persistent system with at least some form of independent storage management (with the object I/O systems don't have).

It is not within the scope of this paper to discuss the object-oriented paradigm in detail, so I will assume the reader has his own opinion about what "object oriented" means. However, a short discussion defining a few base terms and how they apply to persistence is in order.

Classes, Types and Shapes

Many object oriented languages, like C++, make no distinction between a class and its type. However, the distinction is often important to persistent systems. A type is often referred to as an abstract data type (ADT) and defines the interface (operators or methods) to the object. Shape is the internal data representation of the type, exclusive of the methods. A single type may have several possible shapes. A class, on the other hand, has type and shape, but its definition also includes the implementation of the object (actual algorithms for the operators or methods as well as internal data structures and methods) [Wolf 90, Joseph 91]. Therefore, there can be more than one class for a single shape or type.

These distinctions are important to persistent systems. For example, a persistent system might be able to allow both a new version of class definition, as well as the original version (two different classes), to operate on the same objects in persistent store, if both classes are of the same type and shape.

Object Identity

Object identity is intrinsic to object oriented programming, but few programmers give it much thought. It is obvious that each object has a unique identity, independent of its value, if only 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 identity. 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.

Software Engineering Requirements

Computer applications are becoming increasingly complex, as computers are being relied on for tasks that would not have been deemed possible just a few years ago. Next-generation applications will need far more data storage support from the underlying systems than before [Joseph 91]. This section describes both the fundamental requirements needed by simple, standalone applications; and the optional, more demanding requirements for complex and corporate-wide applications.

Fundamental Requirements

The minimum needs of persistent systems are listed in this section. These are the attributes that will be needed by the most humble of persistent object systems, but are by no means sufficient for large scale applications.

Rich Data Modeling

A persistent object system must serve the needs of the software it is supporting. The data models in object oriented languages are extremely rich, in that they support hierarchies of objects, objects containing other objects, objects referencing other objects, and non-objects such as vectors (themselves containing arbitrary data types). Traditional databases and file systems do not allow anywhere near this flexibility, but persistent systems should support it, at least to some degree.

Seamlessness

Persistent objects should be treated just like transient (non-persistent) objects. To force the programmer to deal with persistent and transient objects in two different data modeling paradigms is to invite disaster. Besides the obvious cognitive load, the programmer may not even be able to map the complex structures required for sophisticated applications into the simple models offered by traditional databases. This huge gap between the rich data models used in object-oriented programming languages and the simple structures representable in conventional database systems has been called the "impedance mismatch". The translation from the programming language's modeling paradigm to the database's modeling paradigm can account for as much as 30% of the code in an application [Joseph 91]. This, and the obvious large potential for error in the translation is sufficient reason to attempt to resolve the impedance mismatch and strive for better integration of a programming language with the database system.

Performance

Next-generation applications will need sufficient performance from their persistent systems, not only because the demand of the new applications is great, but because they will need to navigate persistent object networks in ways not efficiently supported by conventional databases.

Other Requirements

This section lists attributes that the more ambitious and robust persistent systems must support. The system that supports only the attributes listed in the above section is humble indeed.

Support for Evolving Instances and Classes

Object structure is not static, but can be very dynamic as the system evolves. An object store should be able to store objects that are under development without loosing reference to them when some programmer alters their schema. The object store needs some form of version control.

Sharing of Objects among Applications

It is desirable to share objects among various applications, perhaps even applications written in different languages. This introduces problems of concurrency control and platform independence.

Distributed Objects

The concept of the distributed object store, like the distributed database, is clearly of importance. However, a distributed object store is more complex than a distributed database because objects have operators as well as attributes. How is one to carry operators (code) among dissimilar systems in a heterogeneous environment? Furthermore, how is object identity to be maintained when they migrate from system to system?

Platform Independent Objects

Ideally, objects could be accessible to a variety of languages, and possibly within a variety of operating systems and machine architectures. This is no trivial matter. Sharing objects among languages is not easy since languages tend to differ considerably in data models, inheritance capabilities, message handling, etc. The situation can be even worse when sharing objects among different systems since it posses further problems such as code compatibility, memory models, and system interaction.

Concurrency Control

An object store in any multi-tasking environment must have some sort of concurrency control to prevent multiple tasks from inadvertently corrupting the objects by concurrent access. The integrity of the object's identity is also at issue here. Thus, a multi-access object store must support some concept of transaction.

Flexible Transactions

The simple lock-and-commit transactions of current database systems are inadequate for next-generation applications. Various cooperative design applications, such as cooperative CAD, can require very long transactions that may not commit for hours, days or weeks [Joseph 91]. Such transactions may need partial commits or some sort of cooperative locks.

Security

Any sophisticated object store has to contend with the security problem. No one is going to trust their finely crafted objects to a system that is going to let just anybody modify or delete them.Other Database Models

Persistent System Taxonomy

It is obvious from the previous discussion of various demands made on persistent object stores that persistent systems can be categorized along very many (not necessarily orthogonal) dimensions. The major dimensions of comparison include the object model, navigation vs queries, storage, transactions, queries, change management, access control, remote databases, interlanguage sharing, and user interfaces. More detailed discussions of persistent system taxonomy can be found in [Joseph 91] and [Kim 90]. Table 1 is an attempt to summarize the points of comparison, while the rest of this section expands on the taxonomy.
Issue
Sub-issue
Range
object model
programming language and database integration
integrated
not inegrated
value oriented vs OODB
value oriented
OODB
treatment of classes and types
class-based
type-based
object identity support
OIDs directly supported
Side-by-side name spaces
entity relationship
relational queries
active vs passive objects
active
passive
navigation vs queries
persistent language based 
extensions
custom language
query based
supported
not supported
indexing
class hierarchy
single class
class hierarchy
nested attribute
supported
not supported
storage
type
typeless page servers
typeless object servers
class-based object servers
type-based object servers
clustering and prefetching
supported
not supported
transactions
nested transactions
supported
not supported
type specific transactions
supported
not supported
long and cooperative transactions
supported
not supported
queries
queries
supported
not supported
predicates
class-based (relational)
type-based (allows methods)
 response sets
heterogeneous
homogeneous
language conformation
semantics
shallow
deep
change management
versions
not supported
supported
configurations
not supported
supported
transformations
not supported
supported
access control
not supported
object centered
OODB centered
remote databases
not supported
supported
interlanguage sharing
not supported
data layout
semantics
method sharing
user interfaces
not supported
supported
implementation
class library
language extension
custom language
Table 1: A summary of points of comparison of persistent systems

Object Model

Object models used by persistent systems vary along several dimensions including degree of integration of the programming language and database, value-oriented versus object-orientation, treatment of classes and types, method of maintaining object identity, and use of active versus passive object activity.

Integration of Programming Language and Database

Integration of the programming language and database data models is one of the most salient goals of persistent systems, however different implementations achieve this goal to varying degrees and from different points of view. Programs written using conventional databases expend considerable effort (as much as 30% of code) in converting from the programming language's data model to the database's, while systems that have totally integrated the object store with the language have to spend no extra code whatsoever in the storage and retrieval of data. Most programmers have experienced the former with C programs trying to store networks of dissimilar objects on stream files. An example of the latter is the Smalltalk and Lisp environments where objects can be stored within the environment, and become, essentially, a part of the executable code. An intermediate example is the case of C++ class libraries that implement persistence by inheritance; programmers extending persistent classes by inheritance must write custom code to translate memory to persistent store, while users of the classes get a (relatively) free ride [Borland 91, Foster 90].

Value Oriented vs OODBs

Value-oriented databases, like the relational model, store relationships between entities indirectly, by virtue of shared data values. For instance, the concept of Fred's dog having fleas may be expressed as the tuples (Fred, Rover) and (Rover, has-fleas). There is no relationship between Fred's dog and the flea-bitten dog, except by virtue of both having the same name. This obviously does not have the power needed by the object model, but extensions of the conventional data model such as the nested relational model (a relation can have another relation as a tuple member), entity relationship model, and semantic data models can more easily mimic the true objects [Joseph 91].

On the other hand, true object-oriented databases reject the more conventional data models in favour of a true object model. (Although critics note that the object model isn't a lot different from the earliest databases using the hierarchical and network models.) While the object model is more to the point, value-oriented extensions have the advantage of a good deal of technology behind them, and the possibility of smooth migration from current systems.

Classes and Types

Many object oriented languages do not recognize the distinction between class and type, however the distinction is important to object persistence since an implementation can use type or shape information to materialize an object from store, but without class, it cannot interact with it. Even in systems where the object store does not interact with the object, but leaves that totally up to the programming language, it is still an important concept since a single type or shape may have application to more than one class or more than version of the same class.

Object Identity

Object identity must somehow be preserved. The most straightforward way to do this is simply to directly model the programming language and replace memory pointers with object identity codes (OIDs). The OIDs must be large enough to be unique for each element within the object store. Persistent object references must incur the overhead of a run-time check and a fetch if the OID references an object that has not yet been brought from memory. For example, E [Schuh 90] uses indirect pointers though "handles" -- objects that encapsulate the information about the physical location (disk or memory) of the object.

A alternate approach is to use the entity relationship model, where all inter-object references are outside the scope of the object and are separate objects in their own right. This method is slower because references are not directly accessible from inside an object.

Another approach is to go with the relational database approach and force all references to be queries. This does not necessarily rule out object identity because artificial "uniqueness" fields can be created. This approach can be very clean in systems that support efficient queries since there is only one access method, but the expense of calling the query processor for every single reference could be prohibitive.

Side-by-side name spaces is another approach that is (more or less) intermediate to queries and OIDs. These implementations support separate names spaces for transient and persistent objects. Ada PGraphite and C++ Persi [Wolf 90] use the persistent name space to bring objects into working memory, where the transient name space is used. EC++ [Sequeira 91] and Arjuna [Dixon 89] use the persistent name space for all persistent references.

Active vs Passive Objects

Passive object storage most resembles conventional database storage in that objects can only receive and send messages once they have been materialized in the program's working memory, where the object is under sole control of the program. Active object storage allows (or enforces) messages to be sent to the objects while they remain in the object store. Therefore, the object store has complete control of the interaction of the program with the persistent objects. This can be very powerful in enforcing very fine-grained security, and in allowing "object views". Only the higher-end systems (type-based object servers) such as Arjuna [Dixon 89] support active objects, while low-end implantations are stuck with passive objects.

Navigation vs Queries

Navigation is searching on the basis of following pointers through a network of objects. Querying is searching by making queries (boolean expressions) on records in a set (a class in this case). Queries spring from traditional database technology. Programming languages traditionally perform search on their (non-persistent) data structures by navigation. Therefore, persistent language based OODBs naturally perform navigational search (owning to their programming language heritage), while query language based OODBs naturally perform query search (owing to their database heritage).

Persistent language based OODBs (Navigation)

Persistent language based OODBs can be class libraries for existing OO languages, extensions of OO languages, or OO languages designed solely to accommodate persistence, but their overriding goal is to eliminate the "impedance mismatch" between program language and database. They reference persistent objects in the object store transparently in exactly the same way as they reference dynamic objects -- usually by references internal to other objects. All program data structures (including vectors, lists and sets) are generally supported as first class objects to maintain the smooth integration of persistent data with non-persistent. These languages have a hard time supporting common database features such as concurrency control, locking, and version control. It is unclear how persistent language based OODBs could support multiple languages.

An example of an extension language is E [Schuh 90], an extension of C++ which introduces an new storage class persistent, and new db primitive types. [Atwood 90] is a comparison of a persistent C++ extension (OQL) and a C++ class library implementation. OQL is also interesting in that it is a C++ language extension that also supports queries.

Query-language OODBs

Query language OODBs don't worry about the "impedance mismatch" problem, and instead borrow from the mature relational database model. As a result, they support only sets as first class objects. The concept of pointers to persistent objects is lost in favour of confining all persistent references to queries, although these queries can be in the form of "path names" to allow for more specific reference beyond the standard query result sets. Of course, the object model is supported, since objects in the database can (almost) directly reference other objects, and queries are often generalized to include methods invocation. It is clear that query language based OODBs, in contrast to persistent language based OODBs, could support multiple languages.

Indexing

There is more than one type of hierarchy in any object oriented system, the major ones being the inheritance (isa) hierarchy and the composition (part-of) hierarchy. The same distinction occurs in OODBs.

Class Hierarchy

In class (is-a) hierarchy indexing, classes are viewed as sets, just as a domain is a set in the relational database model. A query is a search through all objects (members) in a particular class (set). There arises here a problem: in light of inheritance, what are we to define as members of a class? If class D inherits from class B, then is object d (an instance of D) also a "member" of class B, since it "is-a" B and contains all data members and methods of B? An OODB that considers only direct instances of the target class is said to have single-class indexing; an OODB that considers all instances (direct or instances of all derived classes) is said to have class-hierarchy indexing [Kim 90]. OODBs that support queries must support one of these indexing capabilities.

Nested Attribute

In nested attribute (part-of) indexing, queries can be made on the bases of attributes of objects contained in the target objects. This is obviously useful for a large class of queries, but can be an expensive operation. This capability may or may not be supported by an OODB.

Storage

Typeless Page Servers

Typeless page servers pay no attention to object semantics or object structure; they deal only with pages of virtual memory, mapping persistent memory into the working space of a program. This sounds like a simple and fast scheme, but it has problems. Pointers must be offset to the page boundary instead of real virtual memory pointers. This can cause performance problems because all pointers need an extra level of translation. This is not a problem in languages like Smalltalk where all pointers are OIDs anyway. Further problems arise with multiple access and concurrency control. Examples of this storage model are Exodus [DeWitt 86] and ObjectStore [Atwood 90].

Typeless Object Servers

There is not much difference between typeless page servers and typeless object servers in that they both have no concept of an object's semantics. The typeless object servers, however store the objects according to natural object boundaries, and so can grant finer grained concurrency control. Examples of this storage model are Zeitgeist [Ford 88], Mneme [Moss 88] and ObServer [Reiss 86].

Class-based Object Servers

Class-based object servers can handle queries, since they are "aware" of object attribute structure, but cannot execute methods of their objects. Many class-based object servers are no more than relational databases for objects. Thus, they have all the limitations of the relational model: no direct reference and inability to store non-class objects like arrays and vectors. Class-based object servers can be further divided into horizontal partitioning servers (objects derived from inheritance are flattened to store all attributes in a tuple) and vertical partitioning servers (each class's non-inheritied attributes form a tuple and objects derived from inheritance are spread over several tuples in their lineage). Examples of class-based servers are Postgres [Stonebraker 86] and Iris [Fishman 87].

Type-based Object Servers

Type-based object servers are fully "aware" of object semantics: they comprehend both attributes and methods. Type-based servers can handle both complex queries (involving methods) and inter-object navigation. Type-based object servers are often just extensions of any of the other three types: A software layer is added to act on an object once the more basic server materializes it in memory. Examples of type-based servers are Orion [Banerjee 87] and O2 [Deux 90]. An interesting intermediate form between type based and class based servers is SOS-C++ [Shapiro 89] where objects can carry around "code objects".

Clustering and prefetching

Any of the storage methods can be further divided according to optimization methods (or lack thereof). Clustering is the arranging of objects into larger blocks to optimize access. Prefetching is usually in the form of predictive caching.

Transactions

Shared databases of any kind must allow for some form of concurrency control or transaction management in order to maintain consistency. Since OODBs must support sophisticated next-generation applications such as multi-user CAD, they must pay particular attention to this problem and handle it in a very general manner. The least sophisticated persistent systems will ignore the problem altogether, while the most powerful OODBs will have to account for very long transactions and cooperative transactions.

Nested Transactions

The concept of nested transactions is that parent transactions can have sub-transactions. An OODB that actually handles messages will have to embody this concept since method invocation will have to be handled as a transaction too. Method invocation necessarily implies the possibility of resulting method invocations which are also transactions -- nested transactions. Arjuna [Dixon 89] supports nested transactions.

Type Specific Transactions

In conventional database technologies, concurrency control is limited to knowledge about the read/write semantics on the objects. However, OODBs open up other possibilities. If the semantics of the object are known there is no reason to necessarily restrict concurrent transactions to a single atom object. For example, there is no reason to refuse concurrent enqueueing and dequeueing operations on a queue.

Long and Cooperative Transactions

Cooperative CAD and similar application raise the possibility of very long transactions where the design may not have a commit for very long periods (even months!). This is not very feasible with conventional lock-and-commit transaction mechanisms. Various mechanisms have been proposed such as checkpointing (traditional), nested transactions, piggy-back transactions (a transaction splits and each passes its lock to the next), and cooperative transactions (many readers may observe during a writer's update [Joseph 91].

Queries

Not all persistent systems support queries, but even the ones that don't will allow the programmer to build query support into the class library. We are more interested here in the ones that support query directly. Obviously, the typeless page server and typeless object server storage models cannot support queries, since they know nothing about the semantics of the objects what so ever.

Predicates

Persistent systems vary according to types and power of queries they support. OODBs based on the class-based object server storage model can support relational query types, since they are based on the relational model. Thus they can support queries based on primitive attributes of an object only. On the other hand, OODBs based on the type-based object server model can support much more sophisticated queries. Since they have more semantic knowledge about the objects, they can use methods in their queries as well. Method calling within queries can get complicated, so some of these OODBs may choose to make restrictions to method calls such as "no side effects", etc.

Response Sets

Persistent systems differ in the query response sets they can offer. Since inheritance exists in OO 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). Postgres [Stonebraker 86] offers heterogeneous response sets while others do not.

Semantics

Semantic problems arise in OO queries. For instance, inheritance is often used in two different ways: as an is-a relationship, or simply for code sharing. So, if class Android inherits from class Person, how do you answer a query about all Persons with red hair? One would not expect to see an Android in the reponse set. However, one would expect to see ComputerScientists (inherits from Person) in the response set. Somehow, the OODB should know about such things. Likewise, the part-of hierarchy suffers from similar problems.

Change Management

Change management in OO systems is a very complex issue and is a topic in itself. I know of no persistent system that fully supports it, and most ignore it completely. Change management applies to individual objects and also to classes and types. Change types include versions (trees or lattices of update histories to objects, classes or types), configurations (evolving object composition or class hierarchy structure), and transformations (operations applied to the object, class or type). To illustrate, when a class or type is modified, its version changes; if it changes its inheritance structure, its configuration changes; when any modification is made to the type or shape, it may perform a transformation to change (either overwrite or version) all the objects that are direct or indirect instances of the class.

Access Control

Sophisticated OODBs require some sort of security measures to prevent unauthorized changes or deletions. OODBs have possibilities over conventional DBs because objects have the potential to control their own access.

Remote Databases

Persistent systems differ in their support of remote (possibly heterogeneous) databases. The object orientation of the persistent systems will make it easier for them to transparently support access to remote, heterogeneous systems. Arjuna [Dixon 89] is primarily designed around remote (but not heterogeneous) access.

Interlanguage Sharing

Persistent language-based persistent systems are obviously not good for interlanguage sharing, however there is no reason for query language-based OODBs not to support multiple languages. Issues in interlanguage sharing include data layout differences between the languages (such as word alignment issues), semantic differences between languages (such as statically vs dynamically sized arrays), and the question of how to handle arbitrary methods in multiple languages (do you have to code every method in every language?).

User Interfaces

OODBs may or may not have user interfaces. Interfaces are needed to browse data and class/type hierarchies. The same browsers may be used within the programming language environment as well. Since OODBs will make it easier to store multimedia information (images, video and audio), the OODB may act as a central repository for this information, and as such it will need multimedia editors and other tools.

Implementation

The simplest implementations confine themselves to the syntax and semantics of an existing programming language, and implement persistence through inheritance in a class library. Ada PGraphite, C++ Persi [Wolf 90], ObjectWindows [Borland 91], and TCL Object Input/Output [Foster 90] are all examples of this. More commonly, persistence is implemented as a language extension, such as a new storage class for C++ (E [Schuh 90], SOS [Shapiro 89]) or other extensions (OQL [Atwood 90]). Still other implementations involve completely new languages.

Low-end (class library based) Persistence

The rest of this paper is dedicated to low-end persistence -- persistence at the humble object I/O end of the spectrum. My reasons for doing this are twofold: Low-end persistence is a reasonable way to illustrate the complexity of persistent systems without writing a book. And I feel that low-end persistence is a practical way to go for many applications, and is still a viable area with ample territory for research.

It is my contention that, for practical purposes, a developer using OO technology today should endeavor to aim at low-end, class library-based persistence models. See [Atwood 90] for the opposite opinion, [Wolf 90] for a supporting opinion. The lowest persistent model is little more than object I/O. Object I/O involves persistent objects belonging (by inheritance) to a persistent class which contains constructors, destructors, and methods which handle the object I/O in a reasonably transparent manner. This does not mean that all objects inheriting from a persistent class are necessarily persistent, but all such objects have the potential to be persistent. It is generally accepted that persistence should be orthogonal to type [Wolf 90].

This may sound very conservative, but I believe that the OO paradigm makes even simple object I/O a very powerful tool, since objects can be made to "instantiate themselves".

Why aim so low?

Lack of standards

There are currently no agreed-upon standards for OODBs, furthermore, there is no consensus on what a OODB is. In the absence of such agreement, if the point of using any OODB is to share data with other applications, it would be foolhardy to go charging off investing large amounts of time, money and effort pursuing a complex, all-encompassing OODB platform when it is extremely unlikely that any other application is likely to adopt the same OODB platform. If you are not going to share data with any other applications, or just "known" applications, there isn't much point in pursuing the holy Grail either. Only if one is part of a very expensive research program, directed at just this technology, should one pursue such a high goal.

High cost of DB approach

Even when appropriate third part OODB products are available, they should be used with caution. Will the target sites have licence to use that particular OODB product? Would they be able to afford the licence? Even if licensing costs are low, a site may reject installation of the OODB on the grounds of already having "too many" DBs. These problems have to be considered even for research programs where empirical testing at target sites is required, or where the research is targeted to have practical application in the near future.

Programming "in the small"

There has been much talk of "programming in the large", but let's not ignore "in the small" applications. This is where most applications are, after all. And with server architectures, more and more problems are going to fall to the "small" applications. It undoubtedly makes sense for "small" application to use the low-end persistence technology. Obviously, use of low end persistence is not appropriate for "programming in the large" where we consider large corporate-wide databases that demand high security, sharing, and multi-language access. These "large" applications may be able to spend the effort and risk to take advantage of sophisticated OODBs, but it may be wiser to use OO technology to encapsulate conventional DB technology within the objects themselves -- inheriting the persistence through the class hierarchy -- essentially using the low-end persistent systems approach.

Language version problems

Persistent systems that involve language extensions (beyond that of a simple pre-processor) tend to have problems whenever the base language is upgraded. It can take considerable time for the extension vendor to catch up to the base language vendor. In the meantime, the user either is left with an incompatible system or is forced to hold off using the new base language features. EC++ users weren't happy about being tied to C++ 1.2 when C++ 2.0 was available [Sequeera 91].

Object I/O

Object I/O is one of the simplest forms of persistence. It usually is not supported by the language itself, but by the class library. It does not directly support sharing of an sort, queries, transaction management, change management, security, remote access, or user interfaces. Object I/O is not an OODB implementation because there is no true storage management. Storage management is left solely to the programmer. But, because of the encapsulation afforded by the OO paradigm, most details of storage management are left to the objects themselves, so this is not as big a problem as one would imagine. The simplest models use stream files as a storage medium, however, there is no reason why other DB facilities might not form the underlying storage medium of an object I/O facility. It should be noted that stream I/O, especially the C++ stream I/O style, can also serve as a method to communicate objects over character-based lines, and so form a practical bases for object migration between systems.

In the following discussion, I will use C++ terminology, since the case example (to follow) will use C++.

Object I/O is not quite as simple as the preceding paragraph makes it sound. In these implantations, a PERSISTENT base class contains virtual functions for object input and output to a file. Each persistent class must inherit from PERSISTENT and override the input (reader) and output (writer) functions. The writer function is responsible for writing each data member to a file. The reader function is responsible for exactly the reverse, reading in the object from a file (in exactly the same order).

Writer function

The writer function is responsible to write each data member to the file. We can simplify by just writing all the data members that are unique to this class and calling the output function of the parent class(es). Writing out the data members is simply done by writing each data member of primitive type to the output stream, and calling the output function of each user-defined class member. However, a problem arises with pointers. We could assume that a referenced object is merely written to the file as well. But if the object A references another object B via a pointer, it is possible that another object C also references the same object, B.

If both objects A and C are stored, the B object would be copied into the file twice. Now, when the objects are read back A and C would each reference separate copies of B,

and we have violated the object identity of B, which is not what we want. To get around this problem, a runtime (memory) output database must be kept of all object addresses written to the file. When an object is written to a file, the output database is checked to see if the object has been written out before; if it has, a reference (object identifier) is written to the file instead. That way, when the object is read back the second time, the reader will be able to reference the original object via the input database (see the next section).

Reader function

The reader function is responsible to read each data member from a file in exactly the same order as the writer function wrote it. Again, this is done by reading in all data members introduced in this class, and then calling the reader functions of parent class(es). Primitive types are simply read from the file, and the reader functions of any class members are called. Similar to the output database mentioned above, an input database is kept of all objects read in. This database associates the object identifiers (created when the objects were written) of all the objects read in with the objects' current address. Now, when an object identifier is found in the input stream (put there when a duplicate object reference was detected on output), the object identifier is referenced in the database to obtain its memory address.

Note that the entire writer-and-reader scheme is totally dependent on the order of reading exactly duplicating the order of writing. As long as the writer and reader functions are all coded correctly, the order will always be correct, no matter how complex the object reference graph.

Storage Management

As stated above, in object I/O persistence, all storage management is left up to the application programmer. But this is not a serious matter, since for many classes of programs, there are small numbers of storage units, where a storage unit is a self-contained object graph. For example, a word processor may have units of default settings, a current document, and maybe an address database. The point is that programs generally do not operate on huge numbers of independent objects -- they are usually highly interconnected by virtue of being in sets, lists, arrays, or otherwise referenced by one another. The programmer may choose to store each of these units in separate files, or to store them all in the same file in a specific order (and to retrieve them in the same order). Separate files have certain advantages for sharing, since in the word processor mentioned above, if the address database is a separate file, then that same file may be used (non-concurrently) by other applications. An even better method is to encapsulate the address database in a server program (agent object) who controls it and could enforce safe, concurrent access, authorization, and even change management.

Case Example: Borland's Streamable Objects in C++

Borland C++ offers an object I/O implementation with their ObjectWindows class library [Borland 91].

All persistent classes must have the class TStreamable as one of their direct or indirect base classes. TStreamable defines the pure virtual member functions read(), write(), and streamableName(), which all subclasses must redefine. Furthermore, a streamable class should overload the << and >> (stream I/O operators). Optionally, the streamable class can have a build() member function and a special builder constructor. See table 2 for an explanation of all these functions. All of these (except streamableName()) operate on a special stream library, whose root is class pstream (see fig. 1), that parallels the standard stream library.
Function
Explanation
virtual void* write(opstream) 
(protected) 
Responsible to write the object out to the opstream. This is usually done by first calling the write() member functions of any direct superclasses, then writing out all data members introduced by the class. Writing out data members is done by calling their << operators on the opstream. 
virtual void* read(ipstream) 
(protected) 
Responsible to read the object from the ipstream in exactly the same order as the write() member function wrote it. This is usually done by first calling the read() member functions of any direct superclasses, then reading in all data members introduced by the class. Reading in data members is done by calling their >> operators on the ipstream. 
virtual const char* 
streamableName(void) 
(private)
Returns the name of the class as a character string for the use of the stream manager. 
static TStreamable* build(void) 
(public) 
Note that build() is a static member function; it is global to the class, not an object, and can be called without reference to a particular object: "<class-name>::build()". build() is used to return an "empty" object -- space is allocated, but nothing is initialized and the constructors are not called for any of the contained objects. This is done by simply returning an instance constructed with the builder constructor. Thus, build is always defined as: 
<class-name>* <class-name>::build() 
  {return new <class-name>(streamableInit);} 
builder constructor 
(protected)
This is a special constructor which always has the format: 
 <class-name>(StreamableInit) : 
   <base-class-name>(...) [,...] {}; 
which has the effect of calling all the parent constructors, but does not initialize any of the data members. StreamableInit is a special constant used to select this special constructor. This signals the compiler not to initialize the data members. 
operator >> (ipstream, <class>) 
operator >> (ipstream, <class>*) 
operator << (opstream, <classs>) 
operator << (opstream, <class>*) 
These are the operators that the user of the class actually uses when performing object I/O. They are always defined as: 
ipstream operator>>(ipstream is, <class-name> cl) 
{return is >> (TStreamable)cl; } 
ipstream operator >> (ipstream is, <class-name>* cl) 
{return is >> (void*)cl; } 
opstream operator<<(opstream os, <class-name> cl) 
{return os << (TStreamable)cl; } 
opstream operator<<(opstream os, <class-name>* cl) 
{return os << (TStreamable*)cl; } 

Table 2: Member functions and overloaded operators used in a streamable class definition.

fig. 1: pstream library


It should be obvious, from the access control specifiers (public, protected, and private) in table 2, that the member functions are not normally used by the application programmer. The application programmer only uses the << and >> operators. Thus, the application programmer need only open a stream (usually done in the constructor) and then write and read the objects using << and >>.

Multiple references

This is all clean and simple, but for the problem of multiple references to the same object. As stated in the previous section, if more than one reference to the same object is written to the stream, a naive implementation would end up reading back the references as pointing to distinct objects. To get around this problem, a database (a list in memory) is kept of all objects written to and read from a stream. The information in the database is accumulated during the execution of << ipstream and >> opstream operators. The output database (<<) associates memory address with object identifier (OID). The input database (>>) associates OID with memory address. OIDs are generated during output to the stream.

Reading in unallocated objects: build()

There is also the possibility of reading in a pointer to an object. The object is on the stream, but the object we are materializing is only a pointer to it: there is no memory pre-allocated to the data members or the virtual function table. The job of allocating the memory and initializing the virtual function table is the domain of the class's static build() member function (see table 2). The only problem is how to find the address of the build function, given that we know only the name of the class (which we can get from the stream -- see "object I/O algorithms"). The solution is a database associating class names to their build function addresses. But where is this database to come from? Borland's solution is to ask the class author to declare a static object of type TStreamableClass in the class's .CPP file. TStreamableClass's constructor "registers" the class by putting it's name and build() member function address in the database.
TStreamableClass Reg<class-name>("<class-name>",
                                 <class-name>::build,
                                 __DELTA(<class-name>);
where __DELTA is a macro that computes the offset of TStreamable within the class. There is a further problem in that if this declaration is off in an object library somewhere, it's constructor will never be called. The solution is to have the class user include a macro, __link(Reg<class-name>), that forces the constructor to be called. The macro is not intuitive, but it does the job:
 
#define __link(s) extern TStreamableClass s; \ 
    static struct fLink {fLink *f; TStreamableClass *t;} \ 
    force##s={(fLink*)&force##s, (TStreamableClass*)&s};
I will leave it to the reader to convince himself that this really does force a call on the TStreamableClass constructor.

Object I/O algorithms

The steps to object output (<<) are:
  1. The output data base is checked to see if this address has been written before, if it has then the OID is written to the stream, and we are done.
  2. The classname (from the streamableInit() virtual member function) is written to the stream.
  3. The OID is generated, and written to the stream.
  4. The object is output to the stream by calling the object's virtual write() member function.
  5. Save the object address and OID in the output database.
The steps to object input (>>) are:
 
  1. Look at the next element on the stream. If it is an OID, then the object must already have been read and we need only look up the OID in the input database to determine the memory address. We return a reference to this address and we are done.
  2. Otherwise, read in the class name, and construct the object if necessary (see "Reading in unallocated objects").
  3. Read in the OID from the stream.
  4. The object is read from the stream by calling the object's virtual read() member function.
  5. Save the OID and object address in the input database.

Using object I/O

From the user of the persistent class's point of view, the only requirements for saving an object are:
  1. make the class known to the stream manager: __link(RegMyClass);
  2. open the ofpstream: ofpstream os("myfile.dat");
  3. write the object: os << myobject;
To read the object back, the user need only:
  1. make the class known to the stream manager: __link(RegMyClass);
  2. open the ifpstream: ifpstream is("myfile.dat");
  3. declare the object appropriately:

  4.   myClass myObject(StreamableInit); // an instance
      myClass *myObject; // a pointer
  5. read the object: is >> myObject;
The author of the persistent class needs to do considerable more work:
 
  1. Make the class a subclass of TStreamable
  2. Override the virtual member functions read(), write(), streamableName(), and, optionally, static build().
  3. Create a special constructor: <class-name>(StreamableInit);
  4. Declare a static instance of TStreamableClass: TStreamableClass Reg<class-name>("<class-name>",<class-name>::build,__DELTA(<class-name>));

Possible Extensions to Object I/O

Simple object I/O covers the fundamental requirements for persistence, but perhaps it could be extended to cover some of the other requirements such as change management, sharing, distributed objects, concurrency control, transactions management and access control while still maintaining the basic simplicity and low cost of implementation as a class library. The idea here is to avoid special languages, language extensions or pre-processors.

Performance

On the whole object I/O is very fast, due to its simplicity. However, there is a particularly severe bottleneck: the reader, writer, and class/type database accesses used during the I/O can be as source of inefficiency. One should pay careful attention to the efficiency of these operations. A simple sorted list may not be appropriate! In fact, Borland's version implements the databases with a dynamic hash table.

Auto Read and Write (Transparent Persistence)

It is not hard to imagine implementing a class in which persistence is almost completely transparent to the application programmer. One could simply have the class "know" of the file in which its objects are stored. An object of that class would access an appropriate instance in its constructor, and update the stored copy in its destructor. Problems with this implementation would be:
 In what file would we store the objects?

Loading Partial Objects

One problem with the object I/O gathering networks of objects is: what do you do if the network is too large to fit into available memory? The obvious solution is to more objects into memory in a "lazy" manner (defer until referenced). There are several possible schemes to handle lazy migration, including the use of handles in pointer indirection [Shuh 90]. They all involve much more sophisticated persistent object naming conventions.

Persistent Name Space

If more than one object network is to be stored in a single file, and the objects cannot read in a consistent order, how are we to find the individual objects? Basic object stream I/O doesn't help us much. A persistent object naming convention and an efficient method of scanning for named objects is required. The addition of a "name server" to the basic object I/O scheme is a possibility.

Schema Evolution

A persistent system introduces increased problems with new software releases. If the class (shape) of persistent objects is changed between releases, then the users' persistent data is incompatible with the program. How are we to deal with this? Should all classes also know how to read older versions? (Considerable overhead.) Can we automatically generate a program to convert old object bases? (No run-time overhead, but potentially painful for users.)

Servers

Special purpose programs (processes) could be built to act as persistent object servers in almost any operating system environment -- all using nothing but object I/O capabilities. Even on the lowly IBM PC, these could be build as MSWindows DLLs. Each server could serve all the objects of a particular class with great flexibility. The basic stream object I/O capability for C++'s general streams allow interprocess object transfer even to remote sites over character-based communication lines. The application programmer could be insulated from even being aware of the existence of the server, as all this could be encapsulated within the class library; he/she would only need use a persistent class, the constructors and destructors would take care of the rest. Such a system could, in theory, offer all the benefits of the most sophisticated OODB, without compromising the program's data model. It might even support queries. It could support change management, sharing, distributed objects, concurrency control, transactions management and access control. It would be a sophisticated OODB. But now, does it have all the difficulties and expense of a big OODB? Hmmm...

Conclusion

I have presented the basic concepts for persistent systems, examined the software engineering requirements, classified persistent systems in several dimensions, and, finally, presented one of the simplest forms of persistence (namely object I/O) in a fair bit of detail. It is my hope that this last detailed example will give the reader an appreciation for the relative complexity of the higher forms of persistence. Finally, I have suggested a few lines of further work on gracefully extending the class library based object I/O model into higher forms of persistent systems, without the need for new or extended languages

References

[Atkinson 91] Atkinson, Malcolm. "A Vision of Persistent Systems." Extended Abstract for Invited Paper. Deductive and Object-Oriented Databases, Second International Conference, DOOD '91. Delobel, C; Kifer, M.; Masunaga, Y. (Eds.), Lecture Notes in Computer Science Series #566, Springer-Verlag Berlin Heidelberg 1991.

[Atwood 91] Atwood, Thomas. "Two Approaches to Adding Persistence to C++." In Implementing Persistent Object Bases: Principles and Practice, the Fourth International Workshop on Persistent Object Systems, September 23-27, 1990, Morgan Kaufmann Publishers Inc, 1991. pp. 369-383.

[Banerjee 87] Banerjee, J. et al. "Data model issues for object-oriented application." ACM Transactions on Office Information Systems, vol. 5, pp.3-26, January, 1987.

[Booch 91] Booch, G. Object oriented design with applications. Benjamin/Cummings, Redwood City, Calif., 1991

[Borland 91] Borland International Inc. ObjectWindows for C++: User's Guide. Scotts Valley, CA., 1991.

[Duex 90] Duex, O. et al. "The story of O2." IEEE Transactions on Knowledge Data Engineering, vol,2, pp.91-108, March 1990.

[DeWitt 86] DeWitt, D.; Carey, M. "Object and file management in the EXODUS extensible database system." In Proceedings of the International Conference on Very Large Data Bases, 1986, pp.91-100.

[Dixon 89] Dixon, G.N.; Parrington, G.D.; Shrivastava, S.K.; Wheater, S.M. "The Treatment of Persistent Objects in Arjuna." The Computer Journal, vol 32, no. 4, 1989. pp.323-332.

[Fishman 87] Fishman, D. et al. "Iris: An object-oriented database management system." ACM Transactions on Office Information Systems, vol.5, pp.48-69, January, 1987.

[Ford 88] Ford, S. et al. "Zeitgeist: Database support for object-oriented programming." In Proceedings of the Second International Workshop on Object-Oriented Database Systems., 1988, pp.23-42.

[Foster 90] Foster, R.K. "TCL Object Input/Output." Journal of the Symantec Programming Languages Association, Fall 1990.

[Joseph 91] Joseph, John V., Thatte, Satish M., Thompson, Craig W., Wells, David L. "Object-Oriented Databases: Design and Implementation." Proceedings of the IEEE, vol 79, no. 1, January 1991. pp 42-63.

[Kim 90] Kim, Won. "Object-Oriented Databases: Definition and Research Directions." IEEE Transactions on Knowledge and Data Engineering, vol. 2, no. 3, September 1990. pp 327-341.

[Moss 88] Moss, J.E.; Sinofsky, S. "Managing persistent data with Mneme: Designing a reliable, shared object interface." in Proceedings of the Second International Workshop on Object-Oriented Database Systems, 1988, pp.298-316.

[Reiss 86] Reiss, S.; Skarra, A.; Zdonik, S. "An object server for an object-oriented database systems." In 1986 International Workshop on Object-Oriented Database Systems, 1986, pp.196-205.

[Schuh 90] Schuh, Dan; Carey, Michael; Dewitt, David. "Persistence in E Revisited -- Implementation Experiences." In Implementing Persistent Object Bases: Principles and Practice, the Fourth International Workshop on Persistent Object Systems, September 23-27, 1990, Morgan Kaufmann Publishers Inc, 1991. pp. 345-359.

[Sequeira 91] Sequeira, Manuel; Marques, Jose Alves. "Can C++ be Used for Programming Distributed and Persistent Objects?." In Proceedings of the International Workshop on Object-Orientation in Operating Systems, Ed. Cabrern, L.F.; Ruso, U.; Shapiro, M. Palo Alto, Ca, Oct 17-18, 1991. pp.173-176.

[Shapiro 89] Shapiro, M.; Gautron, P.; Morsseri, L. "Persistence and Migration for C++ Objects." In ECOOP 89: Proceedings of the Third Conference on Object-Oriented Programming, 1989, Shephen Cook, Ed.

[Stonebraker 86] Stonebraker, M.; Rowe, L. "The design of Postgres." In Proceedings of the 1986 ACM SIGMOD International Conference on Management of Data, 1986, pp.340-355.

[Stroustrup 91] Stroustrup B. The C++ Programming Language (second edition). Addison-Westley, Reading, Mass., 1991.

[Wolf 91] Wolf, Alexander L. "An Initial Look at Abstraction Mechanisms and Persistence." In Implementing Persistent Object Bases: Principles and Practice, the Fourth International Workshop on Persistent Object Systems, September 23-27, 1990, Morgan Kaufmann Publishers Inc, 1991. pp. 360-368. 



Rob Kremer, Knowledge Sciences Institute, University of Calgary, February, 1992.