Al is a contributing editor for DDJ and the author of C++ Database Development (Henry Holt, 1993). He can be contacted through the DDJ offices at 411 Borel Ave., San Mateo, CA 94402.
Object-oriented database management systems (OODBMS) are coming of age. The trouble is, no one knows exactly what that means. More precisely, many people claim to know, but few agree. There is no standard definition because the technology is too new. If you want to call a DBMS "relational," there are rules against which you can measure it: Codd's 12 rules. A CODASYL database manager--network by definition--has a published standard to shoot for, although not many DBMSs still try to comply with CODASYL. But the practitioners of object-oriented technology have not yet hammered out a standard definition of just what constitutes an object-oriented database. Add to that the total absence of support for persistent objects in the C++ language, and the overwhelming acceptance of C++ as the object-oriented language of choice, and you have a fertile field for new C++ OODBMS products, no two of which are alike.
But with no standard, how are you to know if a DBMS is object-oriented, and, if not, what does it matter anyway? You might wait several years for the industry to define it and a standard definition to emerge, but you might also need to write a program now. Why wait? Latch onto something that works and use it. But which of today's so-called OODBMSs should you use? Without trying to answer that question with complete authority, this article looks at three contemporary OODBMS packages and compares them. Each one takes a different approach to implementing persistent objects, and each has its strengths and weaknesses. By using all of them to solve a simple problem, I hope to shed some light on how they work, how easy they are to learn and work with, and what kinds of problems each of them is particularly suited to solve.
Two of the OODBMS packages support development of Windows as well as DOS programs. Not wanting to mess with the SDK or a Windows applications framework, I decided to build a simple DOS command-line program. I wanted at least two classes, each one supporting object retrieval by a key data value, with one class related some how to the other in typical database fashion. At least one class should have a variable-length data field, perhaps a text string. If you can do these things with a database manager, you can usually do anything you want. I designed a simple school-administration system, one that records teachers, subjects, and teacher/subject assignments. To make it simple, I decided that a teacher could teach only one subject and only one teacher could teach any given subject. This is not the kind of problem that cries out for an object-oriented design (unless you are an OOPS zealot who believes that every solution should be object oriented), but it is one that all programmers can relate to, one that any DBMS--object oriented or not--worth its salt should be able to handle, and one where three different solutions will fit into an article of this size.
By no means did I wring out any of the three. I did no stress or performance tests, and I did not exercise all the features. Each product has many features that the others do not have. My simple application leans toward the features they share and reflects the database problems I work on. I do not mean for you to use this article as the only measure for selecting an OODBMS. Instead, I hope to show three different approaches to what is commonly lumped under the single vague category of "persistent objects," and encourage you to try each of them in your own environment against your own requirements.
I found all three products lacking in documentation, although one is clearly better than the others. All three limit the types of data members that you can put into a persistent class. Two of the products are difficult to learn and use, and those are the ones with the poorest documentation. Those were also the ones for which I had to get technical support from the vendor, not only to use but to install. You'll read about some of these experiences later on. But before you get the idea that these criticisms imply fatal flaws, let me add that I had a compiled, working program in less than one day with each of the products, and what is more, I felt like I understood how they worked and how to use them. One product, Object Manager, takes a traditional view of the database. Another, Code Farms Libraries, takes a revolutionary view, unlike anything I've seen. The third, POET, is somewhere in the middle. I would willingly use any of the three to develop an object-oriented database application.
POET is an object-oriented database-management system that works with C++ and runs on several platforms, among them DOS and Windows. It implements a persistent-object database manager by adding the persistent attribute to the C++ class. POET implements the attribute by extending the C++ language syntax. POET processes the extensions by passing the class-definition code through a preprocessing translator, which BKS calls a "pre-compiler."
POET supports DOS, Windows, UNIX, and Next. The edition I used is Version 1.2 for DOS and Windows. It includes support for the Borland, Microsoft, and Zortech C++ compilers. You can specify during SETUP which compiler you want to use and whether the target programs are for Windows, DOS, or both. Oddly, the POET SETUP program runs only under Windows, so you'll need Windows to install POET even though you might want to use only the DOS version.
SETUP has an annoying practice. It modified my AUTOEXEC.BAT without asking my permission. If I had not been watching closely, I might not have seen it do so, because a message flashes by only while the modification is going on. I am using DOS 6.0, which allows you to select from a menu of configurations in CONFIG.SYS. AUTOEXEC.BAT reacts to the selection by testing the CONFIG environment variable. I have several possible configurations, each with its own PATH statement. I was sure that POET's SETUP couldn't possibly get it right. It didn't. I had to manually correct the error.
I had some difficulty after the installation getting the package running properly. I installed both the DOS and Windows versions to compile with Borland C++ 3.1 and started with the Windows version. POET includes a Borland-like IDE in Windows. There is an example program already compiled and installed in a Windows group. I decided to recompile it.
A POET application, like any other C++ application, consists of class definitions and executable C++ code that uses objects of the classes. POET persistent-class definitions contain keyword extensions to the language that describe the persistent properties. The PTXX Precompiler compiles these definitions into C++ code for input to the C++ compiler. By convention, the POET class definitions are in files with the .HCD extension. PTXX compiles them into source modules with the .HXX file extension. The .CXX extension denotes the C++ source files that contain the persistent class methods.
When I tried to precompile the example application's class definitions, the POET IDE reported errors on the first file it processed. The errors made no sense. I ran it again and watched. Even though I had installed the Borland version, it was precompiling the Microsoft header files. I had failed to change the default directories in the IDE's options process, and POET was using the INCLUDE environment variable, which is normally set for the Microsoft compiler and which does not usually get in the way of the Borland compiler. The POET documentation makes no mention of the INCLUDE environment variable.
The default directories out of the box assume that you have a C:\TMP subdirectory for temporary files and that the compiler is in C:\BC31, neither of which is conventional. You'll have to change these. I did and tried to compile again, and it failed. The changes were posted but had not taken effect. As it turns out, you have to save the project before the option changes will associate themselves with it. Poking around in the examples, I found a Borland-compatible, PRJ file that built the program with Turbo C++ for Windows. It would save a lot of time if the documentation included a cookbook tutorial to get a programmer past this first hurdle.
Next I set out to build the School application program. I began with a simple .HCD file that described the Teacher and Subject classes, both with the persistent attribute and with one class containing a reference to the other. Using the command-line example in the manual, I tried to precompile the .HCD file with the DOS version of PTXX. The program locked up my computer. The sample DOS programs that accompany the package use a subdirectory structure for the include, source, database, and binary files. The documentation is vague on these matters, but I emulated that structure, and still PTXX locked up. Then, to be sure that it wasn't something in the code I wrote, I tried it with the sample application. PTXX locked up there, too. I called tech support, and we agreed that the culprit was probably DOS 6.0, which is still in beta. I switched to a machine with DOS 5.0, and the problem went away. I changed my HIMEM.SYS and EMM386.SYS in the DOS 6.0 computer to those from the Windows 3.1 distribution, and that cured the problem. BKS will need to address this problem because most users will not want to relinquish the superior memory management that the DOS 6.0 memory managers provide.
Now underway, I precompiled my first .HCD file. Both classes had character pointers to represent the names of the objects. POET does not allow persistent classes to have pointers to nonpersistent objects. I tried references and got the same restriction. I substituted instances of Borland's String class, and these, too, were unacceptable to POET because the String class contains a character pointer. Next, I subclassed a persistent class from the Borland String class, only to find that a persistent class may not be derived from a nonpersistent class. These restrictions are documented in the reference guide, but of course I hadn't read it yet. POET includes the persistent PTString class, which solved that particular problem, but the underlying concept points to a larger problem. Your persistent classes may contain instances of nonpersistent classes if they obey certain rules, but they may not contain pointers or references to nonpersistent objects, and they may not be derived from a nonpersistent class. This approach impairs your ability to use existing class libraries at the foundation of your object-oriented design. POET has a few compatible classes such as the PTString class and date and time classes, but a programmer needs a full set of container classes that can be persistent.
I could not put an instance of the Borland String class into my persistent class because it has what POET calls "hidden semantics," which, in this case, means that the class has a character pointer, and POET cannot tell whether the pointer points to a single character or an array of unknown dimension. (For a detailed discussion of this type of problem, see my article, "Persistent Objects in C++" in the December 1992 issue of DDJ.) POET includes a process called the Type Manager, with which you specify how an object's representation is to be processed in the persistent-object database. Assuming you know the semantics of a class that you want to include, you would use the Type Manager to specify them, and then, somehow, POET would accept the object as a member of a persistent class. The Programmer's and Reference Guide does not tell you how to use the Type Manager, presuming that most users will not need it, a presumption with which I flatly disagree. If you are into serious object-oriented design with persistent objects, and if you are using class libraries from earlier work--reusability being one of the tenets of object-oriented design--then you will certainly need the Type Manager. BKS provides the Type Manager documentation on request.
If you do not know the semantics of a nonpersistent object and POET rejects it, then you are out of luck because you cannot tell the Type Manager what you do not know. Another tenet of object oriented design is that encapsulation hides implementation details from the user. You might find yourself rummaging around in the class library's source code just to discover its semantics, the very kind of detail that you never wanted to know.
Listing One (page 13) shows the class design. Except for the persistent specifier and the two template-like typedefs, which support object queries, the header file in Listing One is typical C++ code.
The subdirectory conventions allow you to specify a name for the database with a command-line option. Their convention uses the word "base," which creates a subdirectory named BASE and puts the database files there. Then it creates a source file named BASE.HXX and puts it in the subdirectory specified by another command-line option. It also creates a file named, in this case, SCHOOL.HXX, and puts it in a subdirectory named in yet another command-line option. The "base" convention is arbitrary, and I renamed it to SCHOOL to coincide with the name of my database. That procedure caused PTXX to put a second file named SCHOOL.HXX (instead of BASE.HXX) in the SCHOOL subdirectory, and the SCHOOL.HXX file includes this statement: #include <school.hxx>.
Of course, I did not notice this at the time. I tried to compile the SCHOOL .CXX source file (in another subdirectory named by command-line option), and the source file's include of itself put the Borland C++ compiler into a memory eating loop that continued until the program expired. I changed the name back to BASE. The documentation should warn you about that one. Better still, the PTXX program should issue a warning. The subdirectory structures and the source files generated are confusing enough as it is, without that kind of trap. Once again, a better user's guide could clear up the confusion.
With the database name changed, the C++ compilation ran to completion. My first attempt had the teacher object in the Subject class as a reference to type Teacher. The C++ compiler reported that the reference to the Teacher object in the Subject class was not initialized by two constructors that POET had generated. I hadn't even written constructors yet, and already they had compile errors. A call to tech support revealed that they had not tried using references in persistent objects and did not know if it would work. They thought that if I provided a default constructor and what they call a "class factory constructor" to override the ones that PTXX creates, it might work. I decided not to experiment and switched instead to a pointer.
I wrote the member functions and built a simple program that opens the database and instantiates one of the objects, nothing more. After several false starts, I got the program compiled and linked. The documentation says nothing about which libraries you must link with. You have to poke around in POET's installation directories and guess. Remember that a POET application consists of a lot of source code that PTXX generates for you. I was curious to see how well Turbo Debugger would work, what with all that computer-generated source code to step through. I imagined a horrendous maze of constructors and destructors. Well, that's not something you should worry about. My little program, when compiled with debugging information added, is over 500K, which is way too big for Turbo Debugger to load into memory. I'm not sure how you are supposed to debug a POET application. The documentation does not address the problem. Listing Two (page 13) is the program that builds persistent objects, retrieves them, and relates them to one another.
POET has a full set of object-oriented database-management features, and the program in Listings One and Two uses only a small subset of them. POET keeps track of object copies so that only one copy of an object is in memory regard less of how many times the object is declared. Retrievals are an interesting part of POET because the database is not bound to the primary-key paradigm of the relational database. You retrieve records by building a set and specifying Boolean query criteria in an object of a class especially built by POET for each data member in a persistent class. If you do not need to use a particular member in any queries, no objects of its query class are instantiated, and there is no overhead. If a class member is used for frequent queries against a large database of objects, you can tell the precompiler to build an index for that data member, and the query process will automatically use the index for searches.
POET's strengths are its implementation of persistent objects with Boolean queries, its support for multiple platforms, and its plans for future versions, which include a client/server version. POET's weaknesses are its incomplete documentation and the constrained subset of class definitions that you can make persistent--no pointers to nonpersistent objects, no references, no nonpersistent base classes, no instances of classes that have any of the above.
The Programmer's and Reference Guide does a reasonable job of explaining POET's underlying concepts and how the classes work, but there is no real user's guide to explain how to set up and run the precompiler and how to design your first class. There is a tutorial and an example application, but neither of them has enough information to get you going. The Windows version is even weaker when it comes to documentation. That could be overcome with comprehensive, context-sensitive Help screens, but none of the IDE's dialog boxes have Help command buttons or respond to F1. The IDE itself has a Help menu, and it provides some information, but the overall documentation for both the DOS and Windows versions is too weak to be considered even marginally acceptable. The company is aware of these shortcomings, and they are committed to doing a better job in future versions. In the mean time, they spend a lot of time hand-holding their customers through the first stages of getting up and running, after which, they tell me, most programmers are comfortable with the procedures and can proceed without needing much further help. You should consider this when you decide to use POET. Most programmers will not get POET running without at least one call to tech support. As the package gets wider distribution, the company's ability to hold the hands of every user will wane. In the future you'll spend more and more time waiting for them to call you back. Too bad, because the problems and the support calls could be significantly reduced by better documentation.
POET is a good product that needs good documentation. It is difficult to learn because the documentation is less than what is required for a package like this. Once past the learning barrier, you will be able to develop POET applications effectively. The first hurdle is the highest, and BKS needs to do something to lower it. POET also compiles huge executable files, which, as I learned, was an impediment to source-level debugging.
The Code Farms Libraries (CFL) are C and C++ libraries that implement persistent objects under DOS, UNIX, and the Macintosh. This article is about OODBMS solutions, so I will discuss only the C++ side of CFL.
CFL implements persistent objects by preprocessing class definitions, which include macro statements that identify the persistence of the classes. CFL's view of persistence is unlike that of other OODBMS products. Classes are defined as belonging to meta-classes, not through inheritance, but in the form of organizations. Objects exist within hyper-organizations--rings, collections, aggregations, trees, graphs, links, names, stacks, and entity-relationship models. Once stored in the database, an object may belong to many organizations. This architecture underpins the retrieval processes and interobject relationships supported by CFL.
The organizations themselves resemble basic data structures that most programmers will recognize. A RING is a singly or doubly linked list, except that it is circular instead of having a listhead and two ends. A COLLECTION is a RING with a listhead-like parent object of another type to manage its entry point, which can vary. An AGGREGATION is a COLLECTION where the RING objects point to their listhead/parent object. A TREE is a hierarchy of objects of the same class where the objects at each level form a RING and point to a parent object of the same class at the next higher level in the hierarchy. A GRAPH is a network of nodes and paths between nodes, called "edges." A LINK relates two objects. A NAME is a form of LINK that relates an object to a character string. STACKs are LIFO or FIFO lists. The ENTITY-RELATIONSHIP MODEL relates objects in one-to-many relationships between classes, where the nature of the relationships are different. Some of the organizations have variants, such as the SINGLE_LINK and the DOUBLE_TREE.
When you design an object-oriented database, you will apply your understanding of these organizations to organize the objects. The CFL application must be designed so the data model fits into one or a combination of the organizations. The architecture of a database might very well reflect the background of the designers who find that the data models they prefer must fit into some combination of the organizations.
The traditional view of a database is not apparent in these organizations. In many ways, CFL forces you to think differently about data structures. Whether or not this new perspective is the new, true object-oriented view or simply the view of the CFL developers remains to be seen. Nonetheless, CFL has the potential to support many different data architectures in ways that can surpass traditional database models.
A fundamental difference between CFL and more traditional approaches is that the application loads the complete CFL database into memory when the program begins, and, if the program changes the data in any way, the application must save the complete database to memory when the program is done. Therefore, a CFL database must by definition fit into memory. There are virtual paging operations that use disk or extended memory, and these are mostly transparent to the programmer, but the performance penalties can be significant. CFL does not retrieve individual objects on the basis of data-dependent key values after the fashion of a relational model. Instead, you arrange objects into organizations and navigate those organizations by using entry classes and iterator functions.
Many diverse applications' database requirements will fit the CFL model, and CFL is a powerful tool for addressing those requirements, but you will not use it for very large databases. The need to fit all the objects into memory and the absence of individual object retrieval would constrain a large, transaction-based database application.
CFL persistent objects may not contain references. They may contain pointers to other types, but with exceptions. Character pointers are treated like null-terminated character strings. Pointers to other types are treated as pointers to single instances of the class rather than as pointers to arrays of the object.
Installing CFL on a DOS system is straightforward, although not without a few glitches. There is an INSTALL program that builds subdirectories into which it decompresses files. Then it tells you how to compile the libraries with your compiler. The package supports several compilers and operating platforms, and when you select a compiler from its menu, it simply tells you which batch files to run. When I ran the MAKE batch file to build the Borland C++ version, the system built several programs and then attempted to run one of them, the ZZCOMB program, which aborted with a SHARE violation on drive C:. Since no other programs were running, I assumed that ZZCOMB locks a file and then somehow attempts to open it elsewhere. I removed SHARE.EXE from my AUTOEXEC.BAT, rebooted, and the ZZCOMB program ran OK.
Another batch file specified by the INSTALL program builds the CFL library. You select the batch file depending on which compiler and memory model you are using.
The User's Guide spells out a series of two tests to make sure that the software is correctly installed. The first one tests the class generator, which is CFL's preprocessor that converts class definitions into acceptable C++. The second test compiles a test program. Both tests worked correctly, although the User's Guide incorrectly names one of the batch files, and you must modify it if you are using other than the medium memory model.
You can run tests to exercise every feature in the CFL package, and they promise to run for several hours. I decided not to try them unless I had other problems later.
The last step of the installation builds the Reference Manual, which does not come in printed form. The ZZDOCUM program builds the manual by extracting the information directly from the source code, which assures that the manual is current. You have to compile the ZZDOCUM program, and my copy of ZZDOCUM.C had some jibberish in it that looked like the insert command lines from some text-management utility. I took those out, and the program compiled and ran. It puts garbage characters in the document under the Date Printed heading, but the document is otherwise usable. I sent a report of these problems to Code Farms, and they assured me that they would correct them in the next release.
Code Farms' policy of having you compile its library and print its Reference Manual is an excellent way to stay on top of their version control and that of the compiler manufacturers. You always get the most recent Reference Manual, and if your favorite compiler's new version has object formats incompatible with earlier versions, CFL automatically adjusts when you recompile.
Documentation consists of the printed and bound User's Guide and the Reference Manual, which you build as a printable disk file. The User's Guide is at times a pleasure to read and at other times a deep and confusing document. The confusion is at its worse when the manual describes some of the more arcane parts of the CFL procedures. Many of the code examples contain errors or confusing syntax. The document often breaks lines of code by hyphenating identifiers, which makes the expression appear to contain two identifiers separated by the binary minus operator. In other places, the code will declare uninitialized pointers and then proceed to dereference the pointers, apparently assuming that the reader will mentally fill in the missing details. Other parts of the manual's code refer to class members that do not exist. Apparently the code in the manual has never been compiled. Once you get the hang of how to organize and design a CFL program, you are better off looking at the example programs in the installation's TEST subdirectory rather than those in the manual. I would often use grep to find a sample program that contained a feature that I wanted to understand, read the example with my editor, and compile it to see what it did.
The User's Guide is poorly organized and has an incomplete index. I found many references in the text to items not listed in the index, usually when I needed more information. There are concepts and rules for which there are no examples, and the descriptions are less then comprehensive.
You build an application by including in your source program two files: a header file at the front and a source file at the end, neither of which exists when you begin. In between the two includes, you declare your classes and the program's functions, which contain macros that define the persistent nature of the classes and the organizations into which you will arrange them. Next, you run a preprocessor named ZZPREP, which reads your code and generates the two include files. Then you compile your program with the standard C++ compiler. The include files fill in the necessary information to allow the compiler to compile the program. Listing Three (page 14) is the School application rewritten to work with CFL.
CFL deals with runtime errors by writing error messages on the screen. For example, if you try to open a database that has not been created yet, you get an error message on the screen. If you were to try to associate more than one teacher with the same subject in the program in Listing Three, you would get an error message on the screen. Such situations could be the result of a user-input error, and the program should be able to deal with them without cryptic, intrusive error messages that display wherever the cursor happens to be. The error conditions themselves are posted in a flag that the program can query. I would like to be able to suppress the error displays to stdout.
CFL's strengths are found in its unique approach to an object-oriented database, one that you should at least consider before you opt for one of the more traditional methods. Its weaknesses are in the quality of the documentation, especially in view of its nonconventional approach. Some programmers will avoid CFL because it requires them to learn a new way of thinking about their object-design tasks. That is a shame, because many kinds of applications could benefit from the data organizations of CFL.
Like POET, Code Farms Libraries is a good product that suffers from the lack of good documentation. The restrictions on what you can put into a persistent object could mean that you need a different solution as well. Unlike POET, the CFL executable files are of a manageable size. The example program compiles to 147K, with debugging information included. CFL is worth a look when you consider your object-database requirements. If you can fit all the objects into memory at once, if you can describe your persistent objects without the use of pointers and references, and if you can describe the relationships between classes and the retrieval requirements by using CFL organizations, then CFL is a good choice.
Raima Object Manager is a C++ wrapper around the mature and respected Raima Data Manager, a C library formerly known as db_Vista. Data Manager implements a data model that supports relational, network, and direct access of objects. Object Manager encapsulates that model into an object-oriented database manager. Object Manager and Data Manager are available in single- and multiuser versions for DOS, Windows, and OS/2, with support for Borland, Microsoft, and Zortech C++ compilers. There are also UNIX versions. I worked exclusively with the single-user DOS version.
A C++ programmer comfortable with relational technology or the network set organizations of CODASYL databases will have no trouble using and understanding Object Manager. You design a database by building a traditional schema definition in a Data Definition Language (DDL) text file. A DDL compiler reads the DDL and generates a C header file with structure definitions for the records and #define statements to identify the files, records, and fields. You include the header file in your source code and process the database by inheriting classes and using macros and member functions to manage object persistence, retrieval, and relationships.
You can retrieve objects by key values, from one-to-many sets, or by object identity. You can relate classes by using sets, by duplicating key values in related objects, or directly with database pointers. There is a data model among these to satisfy almost any system architecture.
An Object Manager database consists of collections of fixed-length objects with only primitive data types--int, char, float, arrays, and so on--as members. There are no provisions for variable-length records other than those for the BLOB (binary large object) data type. You may not use pointers, references, or instances of other classes as data members. You must be mindful of the physical file organization when you design your database. A file that holds multiple class types uses fixed-length object slots, with each slot being large enough to hold an object of the biggest class. This behavior would incline a designer to organize objects in files according to object size rather than based on functional relationships.
You install Object Manager as an optional feature of or an addition to the Data Manager installation. The installation adds some libraries and example programs to what already comes with Data Manager. If you already have Data Manager, you can upgrade. If not, you can purchase the entire package in one bundle.
Installation goes without a hitch. Specify a source drive and destination path and insert diskettes when the program asks for them. All installations should be this easy.
Compared to the other products addressed in this article, Raima's documentation rates a literary award. The chapters on database concepts and object and database design are readable and will be particularly helpful to programmers not well versed in those design concepts. The documentation is far from perfect, however. It's often difficult to find the information even when you're sure it's in there somewhere. The index could be better. Several times I came across a macro, function, or data type in an example program that was not in the index, but which I eventually found described somewhere in the documentation. A few subjects are ignored in the printed documentation. I searched high and low for information on how to compile and link an Object Manager application only to find it tucked away in a README file. Such knowledge belongs in a User's Guide.
The DDL is straightforward, using a C-like syntax to describe the database, its files, the records in the files, the fields in the records, the keys, and the sets. Listing Four (page 15) is the DDL for the School application and Listing Five (page 15) is the school application program rewritten for Object Manager.
Persistent classes are defined with classes that derive from the structure defined in the Database Definition Language Processor (DDLP) generated header file and from Object Manager's StoreObj class. The Teacher and Subject classes in Listing Five show how this is done.
The SCHOOL.H file that Listing Five includes is generated by the Object Manager's DDLP from the DDL statements in Listing Four. The header file defines the teacher and subject structures as well as a number of #defines to identify records and fields. The program references some of these, such as TEACHER and SUBJECT_NAME. Other parts of the program use Object Manager macros to provide some additional required definition for the classes. For example, both classes for database objects include the DIRECTREF macro, which tells the compiler to add member functions to support specific direct references between classes. In this implementation, I decided to represent the one-to-one relationships between teachers and the subjects they teach with direct-reference database-object pointers--not the best database design, but sufficient to illustrate the technique.
An Object Manager application works from within a task object that you build by deriving a class from the StoreTask class. This approach allows the multiuser version to discern between tasks and users. Since I needed a task object, I encapsulated the menu processes into it. You must also define a class for the database itself, and I put an instance of it into the task class. The constructor launches the application, so all the main function must do is declare an instance of the task class. If the task object was global, we wouldn't need a main function at all except that, for some reason, the C++ language specification--as it exists today--insists on one.
Object Manager supports object navigation with overloaded operators. The ++ and -- operators navigate forward and backward in the object's default accessing sequence, which is defined by the constructor. You can retrieve objects in the sequence of a key value. The SchoolTask: List function uses the keys, which, in this simple example, are the teachers' and subjects' names. The overloaded [] operator fetches objects, too. You can put FIRST, LAST, and so on inside the brackets, or you can use a key value. (An aside: FIRST and LAST are Object Manager keywords. They are not in the index of either manual. This is an example of the quality of the index.) The overloaded >> and << operators implement retrieval of objects with direct references to other objects. Many pundits warn against the use of overloaded operators, and I must agree that, although I used them in this example, I do not find them to be intuitive. For those who dislike overloaded operators, Object Manager offers member functions to perform the same functions.
Because Object Manager uses Data Manager as its database engine, all the Data Manager utility programs work with the object database. The db_Query package, based on SQL, supports ad hoc and program--generated queries. The db_Revise package assists with database conversion. There are utility programs to unlock the database after a crash; view and change the contents of database fields and check their consistency; export and import the database to and from ASCII files; and pack, inspect, and rebuild indexes.
Object Manager's strengths are in its use of the rugged Data Manager, its intuitive use of the C++ language to encapsulate database operations, and its support for the three data models within which a designer will find a solution to most problems. Its weaknesses are in the restrictions as to what you can put into classes, a weakness shared by the other two packages discussed in this article.
If you are concerned with the quality of documentation, Raima is by far the best product of the three discussed here. If you depend heavily on documentation that is easy to read, has good tutorials and introductions to concepts, and covers the product comprehensively, then Raima outshines the others. But don't let documentation be a litmus test. The suitability of the data models supported should be just as important. It's difficult for an old relational-database analyst like me to imagine a data model that you could not fit into one of Object Manager's relational, network, and direct-reference accesses.
Nonetheless, the data model will determine what tool works best for you. POET is the only one of the three that offers a way to describe the hidden semantics of other classes to the database manager, thus allowing you to incorporate embedded objects into your persistent classes. The techniques for doing this are not easy, and they won't work in every case, but it's the closest any of the products comes to this kind of support. Code Farms Libraries offers a rich repertoire of object organizations, and you could probably implement any traditional data structure by using one or more of them, but you must be able to fit the entire database into memory at once, not to mention bearing the overhead of reading it and writing it every time you run a program. Object Manager builds traditional database files and fixed-length records, wrapping a C library in a C++ wrapper.
There are models of simulation, imagery, and multimedia data out there waiting for solutions, and more object-oriented approaches could be more appropriate. It's just a matter of determining what those really are.
__OBJECT-ORIENTED DATABASE MANAGEMENT SYSTEMS_
by Al Stevens
[LISTING ONE]
// ----------- school.hcd
// School Database Design for POET
#include <poet.hxx>
#include <iostream.h>
persistent class Teacher {
PtString name;
public:
Teacher(char *nm) { name = nm; }
~Teacher() {}
void Display() { cout << (char *) name; }
};
persistent class Subject {
PtString name;
Teacher *teacher;
public:
Subject(char *nm)
{ name = nm; teacher = NULL; }
~Subject() {}
void AddTeacher( Teacher &tch ) { teacher = &tch; }
void Display() { cout << (char *) name; }
Teacher *Tchr() { return teacher; }
};
typedef cset<Teacher*> TeacherSet;
typedef cset<Subject*> SubjectSet;
[LISTING TWO]
// -------- schoolp.cpp
// School Application for POET
#include <iostream.h>
#include <poet.hxx>
#include "school.hxx"
PtBase objbase;
// ------- build a new Subject object
void NewSubject()
{
char nm[50];
cout << "\n--- New Subject --- ";
cout << "\nEnter Subject name: ";
cin >> nm;
Subject subj(nm);
subj.Assign(&objbase); // assign object to database
subj.Store(); // store object in database
}
// ------- build a new Teacher object
void NewTeacher()
{
char nm[50];
cout << "\n--- New Teacher --- ";
cout << "\nEnter Teacher name: ";
cin >> nm;
Teacher tch(nm);
tch.Assign(&objbase); // assign object to database
tch.Store(); // store object in database
}
// ----------- assign a Teacher object to a Subject object
void Assignment()
{
char nm[50];
cout << "\n--- Assignment --- ";
cout << "\nEnter Subject name: ";
cin >> nm;
// ----- build sets to query for Subjects
SubjectAllSet *allSubjects = new SubjectAllSet(&objbase);
SubjectSet *SubjectResult = new SubjectSet;
SubjectQuery SubjQuery;
// ----- build the query
SubjQuery.Setname(nm, PtEQ);
// ----- run the query
allSubjects->Query( &SubjQuery, SubjectResult );
// ---- get the first SubjectResult
int n = SubjectResult->GetNum();
if (n == 0)
cout << "\nNo such subject";
else if (n > 1) {
// ----- more than one result, query used wild cards?
cout << "\nEnter specific subject";
cout << '\n';
cout << "n = " << n;
}
else {
// ------- found a Subject object
Subject *subj;
SubjectResult->Seek(0, PtSTART);
SubjectResult->Get(subj);
cout << "\nFound ";
subj->Display();
// ------- get a Teacher object to assign to Subject
char tnm[50];
cout << "\nEnter Teacher name: ";
cin >> tnm;
// ----- build sets to query for Teachers
TeacherAllSet *allTeachers = new TeacherAllSet(&objbase);
TeacherSet *TeacherResult = new TeacherSet;
TeacherQuery TchQuery;
// ----- build the query
TchQuery.Setname(tnm, PtEQ);
// ----- run the query
allTeachers->Query( &TchQuery, TeacherResult );
// ----- get the first TeacherResult
int n = TeacherResult->GetNum();
if (n == 0)
cout << "\nNo such teacher";
else if (n > 1)
cout << "\nEnter specific teacher";
else {
// ------- found a Teacher object
Teacher *tch;
TeacherResult->Seek(0, PtSTART);
TeacherResult->Get(tch);
cout << "\nFound ";
tch->Display();
// ---------- add the Teacher to the Subject
subj->AddTeacher(*tch);
// ------- store the Subject object in the database
subj->Store();
TeacherResult->Unget(tch);
}
delete allTeachers;
delete TeacherResult;
SubjectResult->Unget(subj);
}
delete allSubjects;
delete SubjectResult;
}
// ---------- list the Subjects (with assigned Teachers)
// and the Teachers (regardless of assignment)
void List()
{
cout << "\nSubjects";
cout << "\n--------";
// --------- build set to get all Subjects
SubjectAllSet *allSubjects = new SubjectAllSet(&objbase);
Subject *thisSubject;
allSubjects->Seek(0, PtSTART);
while (allSubjects->Seek(1, PtCURRENT) == 0) {
// ----- get each Subject in turn
allSubjects->Get(thisSubject);
// ------ display the Subject
cout << '\n';
thisSubject->Display();
// ----- if there is a Teacher assigned, display it
Teacher *t = thisSubject->Tchr();
if (t != NULL) {
cout << " taught by ";
t->Display();
}
allSubjects->Unget(thisSubject);
}
cout << "\n\nTeachers";
cout << "\n--------";
// --------- build set to get all Teachers
TeacherAllSet *allTeachers = new TeacherAllSet(&objbase);
Teacher *thisTeacher;
allTeachers->Seek(0, PtSTART);
while (allTeachers->Seek(1, PtCURRENT) == 0) {
// ----- get each Teacher in turn
allTeachers->Get(thisTeacher);
cout << '\n';
thisTeacher->Display();
allTeachers->Unget(thisTeacher);
}
}
// -------- menu to select processes
void SchoolMenu()
{
int sel = 0;
while (sel != 5) {
cout << '\n';
cout << '\t' << "1. New Subject" << '\n';
cout << '\t' << "2. New Teacher" << '\n';
cout << '\t' << "3. Assignment" << '\n';
cout << '\t' << "4. List" << '\n';
cout << '\t' << "5. Quit" << '\n';
cout << '\t' << " Select: ";
cin >> sel;
switch (sel) {
case 1:
NewSubject();
break;
case 2:
NewTeacher();
break;
case 3:
Assignment();
break;
case 4:
List();
break;
default:
break;
}
}
}
void main()
{
// ------- connect to server
if (objbase.Connect("LOCAL") != 0)
cout << "Cannot connect";
else {
// ----- open database
if (objbase.Open("..\\base") != 0)
cout << "Cannot open database";
else {
// --------- run the application
SchoolMenu();
// ------- close the database
objbase.Close();
}
// ------ disconnect from the server
objbase.DisConnect();
}
}
[LISTING THREE]
// -------- schoolc.cpp
// School application for Code Farms Libraries
#include <io.h>
#include <iostream.h>
#include <string.h>
#define ZZmain
#include "zzincl.h" // generated by ZZPREP
// ------ Root class to control entry to others
class Root {
ZZ_EXT_Root
};
// ---------- persistent Teacher class
class Teacher {
ZZ_EXT_Teacher
public:
Teacher(char *name);
Teacher() {}
void Display();
};
// ---------- persistent Subject class
class Subject {
ZZ_EXT_Subject
public:
Subject(char *name);
Subject() {}
void Display();
};
// -------- the Organizations
ZZ_HYPER_SINGLE_COLLECT(subject,Root,Subject);
ZZ_HYPER_NAME(subjname, Subject);
ZZ_HYPER_SINGLE_COLLECT(teacher,Root,Teacher);
ZZ_HYPER_NAME(tchname, Teacher);
ZZ_HYPER_DOUBLE_LINK(assignment,Subject,Teacher);
ZZ_HYPER_UTILITIES(util);
// ---------- Teacher constructor
Teacher::Teacher(char *name)
{
char *nm = util.strAlloc(name);
tchname.add(this, nm);
}
// -------- display a Teacher object
void Teacher::Display()
{
// --- get the NAME linked with this Teacher
char *nm = tchname.fwd(this);
cout << nm;
}
// ---------- Subject constructor
Subject::Subject(char *name)
{
char *nm = util.strAlloc(name);
subjname.add(this, nm);
}
// -------- display a Subject object
void Subject::Display()
{
// --- get the NAME linked with this Subject
char *nm = subjname.fwd(this);
cout << nm;
}
static Root *rt;
// ------- build a new Subject object
void NewSubject()
{
char name[50];
cout << "\n--- New Subject --- ";
cout << "\nEnter Subject name: ";
cin >> name;
Subject *sb = new Subject(name);
subject.add(rt, sb);
}
// ------- build a new Teacher object
void NewTeacher()
{
char name[50];
cout << "\n--- New Teacher --- ";
cout << "\nEnter Teacher name: ";
cin >> name;
Teacher *tc = new Teacher(name);
teacher.add(rt, tc);
}
// ----------- assign a Teacher object to a Subject object
void Assignment()
{
char nm[50];
cout << "\n--- Assignment --- ";
cout << "\nEnter Subject name: ";
cin >> nm;
// ---- iterate through the Subject objects
subject_iterator sIter(rt);
Subject *sb;
while ((sb = sIter++) != NULL) {
char *snm = subjname.fwd(sb);
if (strcmp(nm, snm) == 0) {
// ---- get a Teacher object to assign to Subject
cout << "\nEnter Teacher name: ";
cin >> nm;
// ---- iterate through the Teacher objects
teacher_iterator tIter(rt);
Teacher *tc;
while ((tc = tIter++) != NULL) {
char *tnm = tchname.fwd(tc);
if (strcmp(nm, tnm) == 0) {
// --- associate the two
assignment.add(sb, tc);
return;
}
}
cout << "\nNo such teacher";
return;
}
}
cout << "\nNo such subject";
}
// ---------- list the Subjects and the Teachers
void List()
{
static char *ln = "\n----------------------";
cout << ln;
cout << "\nSubjects";
cout << ln;
// ---- iterate through the Subject objects
subject_iterator sIter(rt);
Subject *sb;
while ((sb = sIter++) != NULL) {
cout << '\n';
sb->Display();
// -- get Teacher object associated with this Subject
Teacher *tch = assignment.fwd(sb);
if (tch != NULL) {
cout << " taught by ";
tch->Display();
}
}
cout << ln;
cout << "\nTeachers";
cout << ln;
// ---- iterate through the Teacher objects
teacher_iterator tIter(rt);
Teacher *tc;
while ((tc = tIter++) != NULL) {
cout << '\n';
tc->Display();
// -- get Subject object associated with this Teacher
Subject *sbj = assignment.bwd(tc);
if (sbj != NULL) {
cout << " teaches ";
sbj->Display();
}
}
cout << ln;
}
// -------- menu to select processes
void SchoolMenu(void)
{
int sel = 0;
while (sel != 5) {
cout << '\n';
cout << '\t' << "1. New Subject" << '\n';
cout << '\t' << "2. New Teacher" << '\n';
cout << '\t' << "3. Assignment" << '\n';
cout << '\t' << "4. List" << '\n';
cout << '\t' << "5. Quit" << '\n';
cout << '\t' << " Select: ";
cin >> sel;
switch (sel) {
case 1:
NewSubject();
break;
case 2:
NewTeacher();
break;
case 3:
Assignment();
break;
case 4:
List();
break;
default:
break;
}
}
}
static char dbname[] = "school";
void main()
{
char *v, *t;
// --------- open the database and load the organizations
if (access(dbname, 0) == 0) {
util.open(dbname, 1, &v, &t);
rt = (Root *) v;
}
else {
// ----- the database has never been built
rt = new Root;
v = (char *) rt;
t = "Root";
}
// --------- run the application
SchoolMenu();
// --------- save the objects to the database
util.save(dbname, 1, &v, &t);
}
#include "zzfunc.c" // generated by ZZPREP
[LISTING FOUR]
/* -----------------------------------------------------------------------
school.ddl -- the Raima Object Manager schema for the School database
---------------------------------------------------------------------- */
database school[512] {
data file "school.dat" contains teacher, subject;
key file "school.k01" contains teacher.name;
key file "school.k02" contains subject.name;
record teacher {
key char name[30];
db_addr subj;
}
record subject {
key char name[30];
db_addr tch;
}
}
[LISTING FIVE]
// -------- schoolr.cpp
// School Application for Raima Object Manager
#include <iostream.h>
#include <string.h>
#include <storedb.hpp>
#include <storeobj.hpp>
#include <keyobj.hpp>
#include "school.h"
// ------ define the database
class School : public StoreDb {
public:
School();
DEFINE_DB_LOCATOR;
};
class Subject;
// ------ Teacher class
class Teacher : public StoreObj, public teacher {
int RecType() { return TEACHER; }
public:
Teacher() : StoreObj(KeyObj(TEACHER_NAME))
{ subj = 0; }
Teacher(char *nm) : StoreObj(KeyObj(TEACHER_NAME))
{ subj = 0; strncpy(name, nm, 30); }
STOREDIN(School);
DIRECTREF(Subject, subj);
void Display() { cout << name; }
};
// ------ Subject class
class Subject : public StoreObj, public subject {
int RecType() { return SUBJECT; }
public:
Subject() : StoreObj(KeyObj(SUBJECT_NAME))
{ tch = 0; }
Subject(char *nm) : StoreObj(KeyObj(SUBJECT_NAME))
{ tch = 0; strncpy(name, nm, 30); }
STOREDIN(School);
DIRECTREF(Teacher, tch);
void Display() { cout << name; }
};
// ------- define the task
class SchoolTask : public StoreTask {
School SchoolDB; // this is the database
int sel; // menu selection
void NewSubject();
void NewTeacher();
void Assignment();
void List();
public:
SchoolTask();
};
DB_INIT(School); // Initialize DB_LOCATOR
// ------ constructor for the database
School::School() : StoreDb("School", PDB_LOCATOR)
{
if (Open() != True)
// --- database probably has not been initialized
cout << "\nCannot open database";
}
// ------- build a new Subject object
void SchoolTask::NewSubject()
{
char nm[50];
cout << "\n--- New Subject --- ";
cout << "\nEnter Subject name: ";
cin >> nm;
Subject sbj(nm); // construct the Subject
sbj.NewObj(); // add it to the database
}
// ------- build a new Teacher object
void SchoolTask::NewTeacher()
{
char nm[50];
cout << "\n--- New Teacher --- ";
cout << "\nEnter Teacher name: ";
cin >> nm;
Teacher tchr(nm); // construct the Teacher
tchr.NewObj(); // add it to the database
}
// ----------- assign a Teacher object to a Subject object
void SchoolTask::Assignment()
{
char nm[50];
cout << "\n--- Assignment --- ";
cout << "\nEnter Subject name: ";
cin >> nm;
Subject sbj;
KeyObj sky(SUBJECT_NAME, nm); // build a Subject key
sbj[sky]; // retrieve Subject
if (sbj.Okay()) {
// ------- get a Teacher object to assign to Subject
char tnm[50];
cout << "\nEnter Teacher name: ";
cin >> tnm;
Teacher tchr;
KeyObj tky(TEACHER_NAME, tnm); // build a Teacher key
tchr[tky]; // retrieve Teacher
if (tchr.Okay()) {
tchr.Ref(sbj); // direct reference to Subject
sbj.Ref(tchr); // direct reference to Teacher
}
else
cout << "\nNo such teacher";
}
else
cout << "\nNo such subject";
}
// ---------- list the Subjects (with assigned Teachers)
// and the Teachers (with assigned Subjects)
void SchoolTask::List()
{
Subject sbj;
Teacher tchr;
cout << "\nSubjects";
cout << "\n--------";
// ------- step through Subjects
for (sbj[FIRST]; sbj.Okay(); sbj++) {
cout << '\n';
sbj.Display();
sbj >> tchr; // direct reference link
if (tchr.Okay()) {
cout << " taught by ";
tchr.Display();
}
}
cout << "\n\nTeachers";
cout << "\n--------";
// ------- step through Teachers
for (tchr[FIRST]; tchr.Okay(); tchr++) {
cout << '\n';
tchr.Display();
tchr >> sbj; // direct reference link
if (sbj.Okay()) {
cout << " teaches ";
sbj.Display();
}
}
}
// -------- task constructor has menu to select processes
SchoolTask::SchoolTask()
{
sel = 0;
while (sel != 5) {
cout << '\n';
cout << '\t' << "1. New Subject" << '\n';
cout << '\t' << "2. New Teacher" << '\n';
cout << '\t' << "3. Assignment" << '\n';
cout << '\t' << "4. List" << '\n';
cout << '\t' << "5. Quit" << '\n';
cout << '\t' << " Select: ";
cin >> sel;
switch (sel) {
case 1:
NewSubject();
break;
case 2:
NewTeacher();
break;
case 3:
Assignment();
break;
case 4:
List();
break;
default:
break;
}
}
}
void main()
{
SchoolTask st;
}
Copyright © 1993, Dr. Dobb's Journal