EVENT-DRIVEN DATABASE PROGRAMMING IN C++

Can OODBMSs replace relational DBMSs?

This article contains the following executables: EVENT.EXE

Dirk Bartels

Dirk is founder and president of BKS Software. He has a degree in computer science from the Technical University in Berlin. Dirk is the chief designer of the object-oriented database system POET, and he can be contacted at One Kendall Square, Cambridge, MA 02139 or Fobetaredder 12, 2000 Hamburg 67, Germany.


Concurrent processes and event-driven environments like those used with Windows, Macintosh, or X/Motif require a database system that's an active part of the entire environment. What happens, though, with information stored in databases on a network? Do the old database management systems (DBMSs) based on records and relations fit into these systems? I believe they don't.

Conventional database management systems do not contain event-driven concepts. Moreover, their data model--based on records, tables, and a fixed set of data types--is very simple. The result is that database designs are often abstract and don't fit in with complex information models. This comes as no surprise since the design of conventional database systems dates back to the '70s when PCs, workstations, and local area networks didn't even exist. The objective of these earlier DBMS designs was to support mid-range and host computers with relatively simple database applications such as online transaction processing (OLTP). The paradigm shift to object-oriented GUIs in the '80s will usher in a shift to object-oriented database systems in the '90s.

Object-oriented database systems (OODBMSs) fulfill the new demands of modern software systems. They have a rich data (class) model based on the same design approach as 00 languages such as C++ and Smalltalk. Most of these systems are smoothly integrated into an OOP language and provide relatively easy database programming. Some of the tools available today also offer special features for concurrency control based on event-driven mechanisms. Let us take a look at a sample application that uses the features of the rich data model and event-driven concurrency control.

The Requirements of the Sample Application

A typical workgroup computing application is a personal-information management (PIM) system like Lotus Notes, where a group of people who share information among themselves are connected to a network. A simplified data-base model that meets the needs of a workgroup might include a calendar, to-do list, and database for people, companies, and notes.

For the purposes of our example, I'll implement an object-oriented design for the application and the database model because it helps implement a system that's extendible, suitable for GUIs, and embodies an elegant and realistic database schema that supports a 1:1 mapping from the programming model into the database. While this database model is simplified, it nonetheless typifies an OODBMS and illustrates its benefits. Due to space constraints, the complete system is only available electronically; see "Availability," page 3.

The Class Hierarchy

Figure 1 shows a simple class hierarchy with classes for people and companies, and several tasks, notes, and address types. If you adopted the relational model for a hierarchy like this, you'd have to use a table for every class and subclass and join them with keys and relations because the relational model has no mechanism for expressing class hierarchies. This isn't efficient and destroys the idea of objects in the database. An OODBMS, on the other hand, will support these class hierarchies.

Object Identity, Containers, and Polymorphism

In addition to the relationships between base and derived classes (Task and Mail in Figure 1, for instance), it's also important to consider how classes use one another --that is, how they are embedded inside each other.

I do this in C++ by simply using a pointer or a list of pointers to express the relationships between objects; see Figure 2. A C++ pointer provides a kind of object identity (that is unique during the program's execution). It also provides polymorphism, which in this case means that a pointer to a particular object type may also point to other types of objects. For example, a pointer to a Task object (a base class) could also reference (point to) either a Meeting object or a Mail object. Lists of objects or lists of references (pointers) to objects are quite standard in C++, and are more or less generally available in foundation-class libraries. How does this influence our database model?

The Task schema (see Figure 3) completes our database class model and highlights many of the benefits of OODBMS design discussed above:

The simple class model presented up to this point leads to a complex object framework; see Figure 4. If you're wondering why calendar and to-do list classes haven't been designed in the database, keep in mind that there's no real reason for these classes because a calendar (or to-do list) is only a specific view (based on navigation and queries) on top of the database model.

The Event-driven Database Approach

The object-oriented class model provides many advantages for the application (and application developer). For example, you can navigate from one object to another without time-consuming queries; the OODBMS reconstructs the pointer via the object identifier. Besides the navigation, there's still the opportunity to form a query such as searching for a person named "Miller." The simplified class definitions are in the header files (.hcd) in Listing One (page 47).

What are the requirements for such a system within an interconnected workgroup? Imagine a small work group of four people: Peter, the boss; Mary, his secretary; Kim, a developer; John, a developer; and Cheryl, a salesperson.

Peter and Mary are working with Macintosh computers, while the programmers and salesperson work in Windows. The team has different tasks that overlap:

The workgroup database requires permanent access and updates, and the database system must fulfill specific requirements to guarantee the integrity of information distributed between the clients. This includes: object distribution; concurrency control on objects; keeping track on updates of objects; informing clients if a state of an object has changed; informing clients if new objects are available; and delivering new object states.

Object Distribution

OODBMSs understand object structure and behavior. The object structure consists of the class hierarchy, attributes, embedded and referenced objects, and containers. The object behavior is expressed by the methods of the class. All true OODBMSs support structural object orientation, and--depending on the language binding and the implementation language--they more or less support object-oriented behavior. The approach with languages that use static binding (like C++) is to copy the object to the client (without its methods) and construct it with its normal constructor or a similar factory mechanism that combines the data part and the methods. Languages with dynamic binding (like Smalltalk or CLOS) can also process object methods on the server side, as shown in Figure 5 and Figure 6.

Object copying (Figure 6) has an advantage over object sharing (Figure 5) because methods are processed in the local environment--the overhead for a method call is far smaller. On the other hand, the OODBMS server has more tasks because it has to synchronize the different copies of an object within the network.

Concurrency Control on Objects

As with conventional database systems, it's necessary that an object be updated in a controlled way. This is referred to as "object locking." OODBMSs can perform lock requests on an object because OODBMSs know the structure. Lock requests can be performed on a deep (that is, with all subobjects) or a shallow (the class hierarchy only) level. Deadlock detection must be performed, and other clients should be informed that the state of the object has changed (it's locked).

OODBMS Events

As soon as the server confirms a lock request, it should inform other clients that have a copy of this object about this new state. The server is sending a message to the client and the application program can react, by changing the color or the object, for instance, or simply avoiding an update. Other events that should be propagated by the OODBMS server are object updates, object deletions, and the creation of objects that belong to a set (container) of a related object. Finally, there should be two-way communication between the database server and the client. For instance, the server informs the client about the state of a query and the client displays the state in a dialog box. Another example would be the client interrupting a query with the Cancel button in the dialog box.

Optimizing OODBMS Events

OODBMS event handling can be optimized in many ways. For instance, it might not be always necessary or desirable to get numerous lock and update messages about all the objects a client has loaded. The solution is a configurable messaging concept based on several levels of information:

The Programming Environment

To implement the sample program presented here, I've chosen a standard C++ environment under Microsoft Windows. I'll use Borland C++ 3.1 and Object Windows Library (OWL), the POET/Windows OODBMS 2.0 on the client side, and the POET/Novell OODBMS server on the server side.

I've simplified the sample program by developing only the persistent class Task, a dialog box to insert or update objects of this class, and a browser to show all instances of this class. Event handling takes place between the Task dialog box and the Task browser. The application class is called DrDobbs. It constructs a MDI desktop class Desktop. This class drives the entire application. Three different kinds of events will be handled by the application: insert, lock, and update events.

POET's event-handling mechanism is based on callbacks that occur when a client method is called after the POET system encounters a specific insert, lock, or update event. When working with callbacks, keep in mind that the class that contains the callback method must be derived from the base class PtCallback, and that the callback method must bind to the event. Every possible POET event has a kind of predefined slot with a default callback method that does nothing. This slot can be reset with the user's callback method by calling a set method or overloading the PtCallback:: Notify() method.

Handling Insert Events

In this example, we'll assume that two processes (Windows clients) are working with the same database. Process 1 inserts task objects, and process 2 displays the task browser. Every time a new object is stored in the database, process 2 should redraw or update the Task browser. Process 1 creates the Task object, and the user fills it in the Task dialog box. The Task class is derived from PtObject, a base class that is a tool class from the POET environment and inherits the methods PtObject::Assign() and PtObject::Store() to make the object persistent.

Process 2 constructs an object of the class TaskAllSet. Every persistent class in the POET programming environment is associated with a template class <class> AllSet that contains all stored objects of this <class>. The base class PtAllSet inherits methods like PtAllSet::Seek(), PtAllSet::Get(), and PtAllSet::Query() to the derived class to access the persistent objects. The task objects should be displayed in a browser window called AllsetTaskBrowser. The task-browser object should be notified if the state of the TaskAllSet changed; that is, a new task object is inserted into the database.

We implement a method CallbackClass::NewObject() that should be called in case of this event. To simplify the implementation, call the method BWindow::Redraw(), which paints the whole browser again. (Note that BWindow is the base class for AllsetTaskBrowser and provides basic browsing capabilities.)

The next step is to create the watchpoint object. A database object, a callback method, a watch mode, and a depth mode must be specified. In this case, the database object is the TaskAllset and the callback method is TaskBrowser::NewObject(). The watch mode tells the database system which events should be cached: store, update, delete, lock, and unlock. These events are represented as bits that can be shared. Finally, the depth mode specifies the way the object is treated. The possible modes are shallow, deep, and flat. If the watchpoint object is created, then the Watch() method of the database object is called and everything is set. The handling of update and delete events is identical and we haven't included a specific treatment in our example.

Handling Queries

Queries normally take more time, and the application has to provide some kind of response--an hourglass or a message box, for instance. POET offers a three-step mechanism to give the application the control over the state of the request. After the request is sent, the server sends back a confirmation event to the client. During the process, the server periodically sends a pending event to the client, say every two seconds. Finally, the server sends a completion event to the client when the request terminates. You can define callbacks for these events. Our sample application has specific callbacks for each event. We define a simple query asking for a specific deadline of a task. The query is implemented in the method QueryAbortDialog::Query(), displaying a dialog box with an input field. The callback methods are part of the desktop. When the request is started, the first callback CallbackHdl::StartQuery() displays a message box with a Cancel button. The Cancel button can be pressed by the user and the pending event callback CallbackHdl::PendingQuery() handles it and aborts the request. Finally, the dialog box is closed and destroyed by the terminate callback CallbackHdl::TerminateQuery().

Unlike the watch-and-notify mechanism (see Figure 7), these events are easier to implement because they do not have to deal with specific objects, watch modes, and depth modes. Three methods are available in the class PtBase, which represents an open database: PtBase::SetActionStarting(), PtBase::SetActionPending(), and PtBase::SetActionTerminated() are responsible for setting the callback functions in the manner just discussed. The callbacks are set in the QueryAbortDialog::Query() method. The result of the query is a set of task objects that we display in a browser.

Handling Lock Events

Receiving events that other processes in the network are using or modifying objects that are actually used could have a major impact on the user interface. For instance, a sophisticated application could gray all lines of a browser that are in edit mode on other workstations.

For example, every object we read in and display in the browser should be assigned a callback function to handle this event. Our simplified solution to these kinds of events is to display a message box that mentions the name of the task object and the event state (lock or unlock). The mechanism to define this callback is identical to that of the insert event. You can see the implementation in the method CallbackHdl::LockEvent().

Conclusion

What is database event-handling all about? First, and most important, I think it is a necessary extension to databases because it helps the user get the most correct and most up-to-date information. Second, it helps the programmer implement systems for delivering this information. It would be extremely time consuming to develop customized event handling on the application side. It might be possible on a local machine, but it seems to be impossible in multiplatform networks. Until now, we always had to take a step back and live with restrictions like simplified data models or snapshots of databases. With OODBMSs, we can overcome these restrictions.

The workstation model is based on event-driven applications that should work transparently between concurrent processes. Conventional database systems are not designed to meet these demands. They don't support event handling and their data model is too simple to manage the complex and dynamic information of new power applications. OODBMSs can deliver the required functionality. Eventually, OODBMSs will likely become the next integral extension of modern operating systems and be common as today's GUIs and network operating systems.



_EVENT-DRIVEN DATABASE PROGRAMMING IN C++_
by Dirk Bartels


[LISTING ONE]



/////// INFO.HCD

#include <bks.hxx>
#include <ptcomp.hxx>
#include <ptstring.hxx>

#include <_defs.h>

_CLASSDEF(TWindowsObject);

//  ZWInfo
////////////////////////////////////////////////////////////////////////////

enum InfoType { ZWTASK, ZWMAIL, ZWPHONECALL, ZWMEETING, ZWNOTE, ZWREPORT,
                                                              ZWPHONEREPORT };
// forward declaration
persistent class ZWPerson;
persistent class ZWInfo
{
protected:
    PtString        subject;    // optional
//  cset<ZWPerson*> relations;  // optional
    PtDate          date;       // last change
    PtTime          time;       // last change
    PtDate          expiration; // optional, if Info recycled
public:
    ZWInfo(void) {;}
    void    SetSubject      (PtString t)    { subject = t; }
    void    SetDate         (PtDate &Date, PtTime &Time) {date=Date;time=Time;}
    PtDate  &GetDate        (void)            { return date; }
    PtTime  &GetTime        (void)            { return time; }
    PtString &GetSubject    (void)            { return subject;        }
    char     *GetSubject    (PtString &t)     { t = subject; return (char*)t; }

    virtual InfoType GetInfoType(void) { return ZWTASK; }
//  virtual int DisplayYourself(PTWindowsObject) {;}
};


/////// NOTE.HCD

#include <bks.hxx>
#include <ptcomp.hxx>
#include <ptstring.hxx>
#include <_defs.h>

_CLASSDEF(TWindowsObject);

#ifdef PTXX
#include "info.hcd"
#endif

//  ZWMemo
////////////////////////////////////////////////////////////////////////////

class ZWBasePerson;
class ZWNote : public ZWInfo
{
protected:
            PtString    text;
transient   int         dirty;
public:
    ZWNote(void);
    void SetText                   (char *Text)    { text  = Text;  }
    void SetDirty                  (int Dirty)     { dirty = Dirty; }
    PtString &GetText              (void)          { return text;  }
    int       IsDirty              (void)          { return dirty; }
    virtual InfoType GetInfoType   (void)          { return ZWNOTE; }
//  virtual int         DisplayYourself(PTWindowsObject);
};
typedef cset<ZWNote*> ZWNoteResult;

//  ZWReport
////////////////////////////////////////////////////////////////////////////
persistent class ZWBasePerson;
class ZWReport : public ZWNote
{
protected:
    ZWBasePerson*   person;     // person dedicated with this report
public:
    ZWReport(void);
    void SetPerson                (ZWBasePerson *Person)  { person = Person; }
    ZWBasePerson *GetPerson       (void)                  { return person;    }
    virtual InfoType  GetInfoType (void)                  { return ZWREPORT; }
//  virtual int         DisplayYourself(PTWindowsObject);
};
typedef cset<ZWReport*> ZWReportResult;

//  ZWPhoneReport
////////////////////////////////////////////////////////////////////////////
class ZWPhoneReport : public ZWNote
{
protected:
    ZWPerson*   person;         // Called person
    PtDate      date_called;
    PtTime      time_called;
public:
    ZWPhoneReport(void);
    void SetPerson               (ZWPerson *Person)   { person = Person; }
    ZWPerson *GetPerson          (void)               { return person;   }
    virtual InfoType  GetInfoType(void)               { return ZWPHONEREPORT; }
//  virtual int         DisplayYourself(PTWindowsObject);
};
typedef cset<ZWPhoneReport*> ZWPhoneReportResult;


/////// PERSON.HCD

#include <bks.hxx>
#include <ptcomp.hxx>
#include <ptstring.hxx>

#include <_defs.h>

_CLASSDEF(TWindowsObject);

//  Address
////////////////////////////////////////////////////////////////////////////

persistent class ZWAddress
{
protected:
    PtString        street;
    PtString        countrycode;
    PtString        plz;
    PtString        city;
    PtString        postbox;
    cset<PtString>  phone;
    PtString        fax;
    cset<PtString>  communication;
public:
    ZWAddress(void){;}

    void SetStreet  (char *szStreet)    { street = szStreet;  }
    void SetCountry (char *szCountry)   { countrycode = szCountry; }
    void SetPlz     (char *szPlz)       { plz = szPlz; }
    void SetCity    (char *szCity)      { city = szCity; }
    void SetPostBox (char *szPostBox)   { postbox = szPostBox; }

    PtString &GetStreet (void) { return street; }
    PtString &GetCountry(void) { return countrycode;}
    PtString &GetPlz    (void) { return plz; }
    PtString &GetCity   (void) { return city; }
    PtString &GetPostBox(void) { return postbox; }
    PtString &GetFax    (void) { return fax; }
    cset<PtString> &GetPhone  (void)    { return phone; }
    cset<PtString> &GetCommunication(void) { return communication; }

//  int DisplayYourself(PTWindowsObject);
};

// Forward declaration
persistent class ZWNote;

//  ZWBasePerson
////////////////////////////////////////////////////////////////////////////

persistent class ZWBasePerson
{
/*?
friend class _CLASSTYPE BasePersonDialog;
?*/
protected:
    cset<ZWNote*>   information;    // list of all available information
    PtString        name;           // Must be completed
    cset<PtString>  keywords;       // list of search items

public:
    ZWBasePerson(void);

    void SetName(char *szName)          { name = szName; }

    PtString        &GetName(void)      { return name;  }
    cset<PtString>  &GetKeywords(void)  { return keywords;  }
    cset<ZWNote*>   &GetInfos(void)     { return information;   }

//  virtual int DisplayYourself(PTWindowsObject);
};
typedef cset<ZWBasePerson*> ZWBasePersonResult;

//  ZWCompany
////////////////////////////////////////////////////////////////////////////
persistent class ZWPerson;
class ZWCompany : public ZWBasePerson
{
/*?
friend class _CLASSTYPE CompanyDialog;
?*/
protected:
    ZWAddress       adr;
    cset<ZWPerson*> employees;
public:
    ZWCompany(void);

    cset<ZWPerson*>  &GetEmployees(void) { return employees; }
    ZWAddress        &GetAddress  (void) { return adr;       }

//  virtual int DisplayYourself(PTWindowsObject);
};
typedef cset<ZWCompany*> ZWCompanyResult;

//  ZWPerson
////////////////////////////////////////////////////////////////////////////
class ZWPerson: public ZWBasePerson
{
/*?
friend class _CLASSTYPE PersonDialog;
?*/
protected:
    PtString        title;
    cset<PtString>      firstnames; // name is in BasePerson
    short           gender;         // Geschlecht
    cset<ZWAddress*>    homeadrs;
    PtDate          birthday;
    short           check_birthday;
    ZWCompany       *employer;
    PtString        department;
    PtString        function;
    PtString        phone;
    PtString        fax;
public:
    ZWPerson(void);
    ZWPerson(char* nm)  { name = nm; employer = 0; }
    PtString        &GetTitle       (void)  { return title; }
    cset<PtString>  &GetFirstNames  (void)  { return firstnames; }
    short           &GetGender      (void)  { return gender; }
    cset<ZWAddress*>&GetAddresses   (void)  { return homeadrs; }
    PtDate          &GetBirthday    (void)  { return birthday; }
    short           GetCheckDay     (void)  { return check_birthday; }
    ZWCompany       *GetEmployer    (void)  { return employer; }
    PtString        &GetDepartment  (void)  { return department; }
    PtString        &GetFunction    (void)  { return function; }
    PtString        &GetPhone       (void)  { return phone; }
    PtString        &GetFax         (void)  { return fax; }

    void    SetTitle        (char *szTitle)      { title = szTitle; }
    void    SetGender       (short Gender)       { gender = Gender; }
    void    SetDepartment   (char *szDepartment) {department=szDepartment;}
    void    SetFunction     (char *szFunction)   { function=szFunction; }
    void    SetEmployer     (ZWCompany *pCompany){ employer = pCompany; }
    void    SetBirthDay     (PtDate &Date)       { birthday = Date; }
    void    SetCheckDay     (short check)        {check_birthday = check; }
    void    SetPhone        (char *szPhone)      { phone = szPhone; }
    void    SetFax          (char *szFax)        { fax = szFax; }

//  virtual int DisplayYourself(PTWindowsObject);
};

///////// TASK.HCD

#include <bks.hxx>
#include <ptcomp.hxx>
#include <ptstring.hxx>

#include <_defs.h>

_CLASSDEF(TWindowsObject);

#ifdef PTXX
#include "info.hcd"
#endif

//  ZWTask
////////////////////////////////////////////////////////////////////////////
class ZWTask: public ZWInfo
{
/*?
friend class _CLASSTYPE TaskDialog;
?*/
protected:
    PtString        description;
    short           done;           // TRUE = done
    ZWPerson*       delegated_to;   // Person responsible for task
    PtDate          deadline;       // optional
    PtTime          minTime;        // Time of task start
    PtTime          maxTime;        // Time of task end
    PtDate          minDate;        // Date of task start
    PtDate          maxDate;        // Date of task end

    useindex ZWTaskIndex;           // query optimization, see below
public:
    ZWTask();
    void SetDescription      (char *Desc)    { description = Desc;   }
    void SetDone             (short Done)    { done        = Done;   }
    void SetDelegatedTo      (ZWPerson *Person)  { delegated_to = Person; }

    PtString &GetDescription  (void)      { return description;   }
    short     GetDone         (void)      { return done;          }
    ZWPerson *GetDelegatedTo  (void)      { return delegated_to;  }
    PtDate   &GetMinDate      (void)      { return minDate;   }
    PtDate   &GetMaxDate      (void)      { return maxDate;   }
    PtTime   &GetMinTime      (void)      { return minTime;   }
    PtTime   &GetMaxTime      (void)      { return maxTime;   }
    PtDate   &GetDeadline     (void)      { return deadline;  }

    virtual InfoType GetInfoType  (void)  { return ZWTASK;    }
    virtual int     DisplayYourself(PTWindowsObject);
};
typedef cset<ZWTask*> ZWTaskResult;

//  ZWTaskIndex
////////////////////////////////////////////////////////////////////////////
indexdef ZWTaskIndex : ZWTask
{
    minDate;
    maxDate;
};

//  ZWReport
////////////////////////////////////////////////////////////////////////////
enum ZWMailType  { LETTER, MEMO, CONCEPT };
class ZWMail: public ZWTask
{
/*?
friend class _CLASSTYPE ReportDialog;
?*/
protected:
    cset<ZWPerson*> cc_list;        // carbon copy list
    ZWMailType  type;
public:
    ZWMail();
    void            SetMailType(ZWMailType Type)    { type = Type; }
    ZWMailType      GetMailType(void)               { return type; }
    cset<ZWPerson*> &GetCCList(void)                { return cc_list; }

    virtual InfoType GetInfoType(void)              { return ZWMAIL;    }
//  virtual int     DisplayYourself(PTWindowsObject);
};
typedef cset<ZWMail*> ZWMailResult;

//  ZWPhonecall
////////////////////////////////////////////////////////////////////////////
enum ZWCallState  { HAVENT_CALLED, TRIED, LEFT_MESSAGE, DONE };
class ZWPhonecall: public ZWTask
{
/*?
friend class _CLASSTYPE PhonecallDialog;
?*/
protected:
    ZWPerson*   person;     // Pointer to person to call
    ZWCallState status;     // e.g. LEFT_MESSAGE
    short       attempts;   // unsuccessful attemps to call the person
    PtString    notes;
public:
    ZWPhonecall();

    void SetContact(ZWPerson  *Contact) { person = Contact; }
    void SetStatus (ZWCallState Status) { status  = Status;  }

    ZWPerson *  GetContact  (void) { return person; }
    ZWCallState GetStatus   (void) { return status;  }

    virtual InfoType GetInfoType(void)  { return ZWPHONECALL;   }
//  virtual int      DisplayYourself(PTWindowsObject);
};
typedef cset<ZWPhonecall*> ZWPhonecallResult;

//  ZWMeeting
////////////////////////////////////////////////////////////////////////////
persistent class ZWAddress;             // Forward reference
persistent class ZWBasePerson;          // Forward reference
class ZWMeeting : public ZWTask         // Termine
{
/*?
friend class _CLASSTYPE MeetingDialog;
?*/
protected:
    cset<ZWPerson*> participants;   // e.g. Miller, Smith, Bartels, Witt
    ZWAddress       *location;      // optional
    ZWBasePerson    *host;          // optional
public:
    ZWMeeting();
    void SetLocation(ZWAddress *szLocation) { location = szLocation; }
    void SetHost    (ZWBasePerson *pPerson) { host     = pPerson; }

    cset<ZWPerson*> &GetParticipants(void)  { return participants; }
    ZWAddress       *GetLocation(void)      { return location; }
    ZWBasePerson    *GetHost(void)          { return host; }

    virtual InfoType GetInfoType(void)  { return ZWMEETING; }
//  virtual int     DisplayYourself(PTWindowsObject);
};
typedef cset<ZWMeeting*> ZWMeetingResult;






Copyright © 1993, Dr. Dobb's Journal