The Database Access Library for C++

USER GUIDE


1. INTRODUCTION
1.1 Database Drivers
1.2 Platforms &Compilers


2. BASE LIBRARY
2.1 Extended type classes
2.2 Collection classes
2.2.1 Hashtables
2.2.2 Iterators
2.2.3 When to use which collection
2.3 Programming support classes
2.3.1 Exception Framework


3. DEFINE A PERSISTENT MODEL
3.1 Define the Instance Classes
3.2 Define your Model
3.3 Using the ModelGenerator


4. MANIPULATING INSTANCES
4.1 How to Open and close databases
4.2 Transactions
4.3 How to add, change or remove instances
4.4 Pointers and Garbage Collection
4.5 Multi-user issues


5. SELECTIONS
5.1 Polymorphic selections
5.2 Conditions
5.2.1 Attribute condition
5.2.2 Associated Instance Condition
5.2.3 Associated Selection Condition
5.2.4 Combining Conditions
5.3 Sorting Instances


6. EVENTS


7. THE DRIVERS
7.1 The files driver
7.2 The MS-Access Driver


8. COMPILATION INSTRUCTIONS
8.1 How to compile an application.


1. INTRODUCTION

Since the 1960s when object-oriented technology was first introduced its influence on programming has steadily increased to a point where any serious application is built in objects. But although the programming languages have evolved to object-orientation, the databases lag behind in the adoption of this paradigm. Even though many object-oriented databases have been developed in the past years and many have become successful in some part of the professional market, they have still not been able to conquer to vast territory of the mainstream where relational technology is still the norm. This creates the necessity for each application developer who wants to deliver professional programs with database functionality to bridge the gap between his object-oriented program and the relational structure of his database.

There are many libraries that have tried to connect the object-oriented programming languages to the relational databases but their approach has mainly been to introduce the relational concepts of tables, columns and rows to the object-oriented program with the only goal to offer an database independent API to the programmer but leaving the challenge of the paradigm shift to the programmer.

The Database Access Library uses a different approach. It also offers a database independent API but this API does only use object-oriented concepts thus encapsulating the paradigm shift. This approach releaves the application programmer from the burden of storing its objects in relations but also offers some specific features that are typically found in object-oriented environments such as automatic generation of object-identifiers and a client side object-cache. Other features of the library not typically found in other libraries are:

1.1 Database Drivers

The library uses a driver mechanism to specific databases. In the main distribution of the library two drivers are included. One single user driver that uses binary files to store its data. And a full features MS-Access driver. Be aware that the unregistered version of the MS-Access driver does cannot be redistributed and will not work when the IDE of MS Visual C++ or Borland C++Builder is not running.

1.2 Platforms &Compilers

At the moment the Database Access Library supports MS Visual C++ 6.0 and Borland Builder 5.0 on a Win32 platform.


2. BASE LIBRARY

The base library offers functionality that is not directly involved in supporting database functionality. It offers standard classes that are used throughout the library. They are build to introduce a consist API to all classes. They can be divided into three groups. Extended type classes, Collection Classes and Programming support classes.

2.1 Extended type classes

The library offers String, Timestamp and Blob classes. The string class contains functionality to manipulate character data. The Timestamp implement functionality to manipulate and store date and time data. The Blob implements a binary large object.

2.2 Collection classes

The collection classes are built as templates. Therefore they allow full type checking by the compiler. There are four basic types. The List that implements a single linked list. The Vector that implements an array based collection. The Dictionary that is a single linked list of key-value pairs and the Hashtable that implements a hashed collection of key-value pairs.
For each template variable the collection uses a prefix "Val" to indicate that the elements will be used by value or "Ptr" to indicate that the pointer to the type is used. the supported variants are at the moment:

Each Class that is used for a "Val"-template variable should define the following methods:
There are no requirements for classes used in "Ptr"-template variables.

2.2.1 Hashtables

he hashtable is a structure that is based on key-value pairs that are stored in buckets. Which bucket is determined by a hashfunction. This hashfunction should be given as parameter to the constructor. This hashfunction has the following signature:

long hashfunction(const KeyType& aKey);
When you write such a function it is important to spread the possible outcome of the function as evenly as possible over the full range of possible long values.
The classes in the base library have a static hash method for this purpose.
Hashtables can be customized by indicating the number of buckets. The default amount is 25. This basically is enough for 150 key values. The hashtable will increase its number of buckets automatically when it crosses this limit. This is an expensive operation thus should preferably be avoided.

2.2.2 Iterators

Each class has an accompanying iterator. This iterator always has a constructor that takes the collection as parameter to initiate the iterator and a next method to move to the next position in the collection and the value method returns the value in that position. In case of a dictionary or hashtable the key method returns the key in that position. This will result in the following code:

(Val)PtrCollection<(K,)V> theCollection;
...
CollectionIterator<(K,)V> theIterator(theCollection);
while (theIterator.next())
{
  (K* theKey = theIterator.key();)
  V theValue = theIterator.value();
  ...
}

2.2.3 When to use which collection

List should be used when you expect that the collections will have many inserts and deletes in its lifetime. The drawback is that access to its elements via the index is slow. Both features become more important when the list become large.
The Vector uses less memory than the List. It is very fast in accessing its elements but can be slow when inserting or deleting elements especially when it becomes large. You can influence memory use and performance by indicating the initial capacity and increment size.
The Dictionary is used when you want small collections that support key value lookups it uses far less memory than the hashtable but is not as efficient when containing large amounts of elements.
The hashtable is meant for large amounts of key-value pairs where access to values on basis of the key should be fast.

2.3 Programming support classes

The library implements a Exception Framework to indicate exceptional situations.

2.3.1 Exception Framework

he library offers a general Exception Class which is used as exception througout the library. This exception holds a message, an error code, a severity and context for reference in the exception handling code. The exceptions are thrown via the macros FATAL, ERROR, WARNING.
Via these macros the exception is not thrown directly but delivered to an exception handler function. The framework defines a default handler that will throw all exceptions with severity fatal and error and write a warning to stderr. It is also possible to write your own handler. This handler should have the following signature:

void handler(const Exception& e);
via the static method Exception::setExceptionHandler(handler) this handler can be activated.


3. DEFINE A PERSISTENT MODEL

To start working with the Database Access Library you have to define a persistent model. This model defines the classes that should be persistent. These classes can use certain features of the library. These features are:

When you want to make an application it is important first to make a class model of the application domain that define the classes, the associations and the inheritance relations.
Besides this information you have to have an idea of the attributes of your classes. The library supports the following types for attributes:

3.1 Define the Instance Classes

The first thing you do is to define the Instance classes. Instances of these classes will be the actual persistent objects. Do the following steps to define such a class:

Here follows an example of such a Class:
Class MyClass : public Instance
{
private:
  // String attribute 
  String myName;
  // 1- rol van een associatie.
  OneAssociated<OtherClass> myOtherClass;
  // n- rol van een associatie.
  AssociatedList<OtherClass> myOtherAssociation; 
public:
  // constructor methods.
  MyClass(Class* aClass) : Instance(aClass) { 
    ...
  } 

  static Instance* getInstance(Class* aClass) { 
    return new MyClass(aClass);
  } 
  // setter en getter voor Name Attribute
  void setName(const String& aName) { 
    change();
    myName = aName;
  }

  const String& getName() const { 
    return myName; 
  } 
  ... 
  // methods for to one association:
  void setOtherClass(OtherClass* oc) {
    set(myOtherClass,oc);
  }
  OtherClass* getOtherClass() {
    get(myOtherClass);
    return myOtherClass);
  }
  // methods for to many association: 
  void addOtherAssociation(OtherClass* oc) { 
    add(myOtherAssociation,oc); 
  } 
  void removeOtherAssociation(OtherClass* oc) { 
    remove(myOtherAssociation,oc); 
  } 
  const AssociatedList& getOtherAssociation() { 
    get(myOtherAssociation); 
    return myOtherAssociation; 
  } 

  // handle methods implements visitor design pattern 
  virtual void handle(Handler* h) { 
    Instance::handle(h); 
    h->handleString("Name",myName,30); 
    h->handleOneAssociated( 
          "FirstAssociation", "FirstRole", 
          &myOtherClass, Association::Cascading); 
    h->handleManyAssociated(
          "SecondAssociation", "AnotherRole", 
          &myOtherAssociation); 
  }
};
      

3.2 Define your Model

In the context of the Database Access Library the model is the C++-version of your class model. All classes in your class model should be defined in a specific Model class to define your schema. You do this in the following steps.

This will result in a model class that looks for instance like this:
Class MyModel : public Model 
{ 
private: 
  Class* myClass; 
  Class* myOtherClass; 
public: 
  MyModel : Model("MyModel") { };
  ... 
  void defineModel() { 
    setVersion(4); 
    myClass = addClass("MyClass",2, MyClass::getInstance); 
    myOtherClass = addClass("OtherClass",1, OtherClass::getInstance);
    myOtherClass.setAbstract(); 
    MyClass->subclassOf(myOtherClass); 
  }
};
      

3.3 Using the ModelGenerator

The previous paragraphs explain how to code a model by hand. But it is also possible to generate the code with a tool called the ModelGenerator that accompanies the library. This tool offers a user interface to define the classes, attributes and associations for a model. When you are done, you can generate headerfiles that define the Instance classes and the Model.
This tool is not only a tool for use with the library. It is also a test application built with the Database Access Library itself. Therefore you can store your model information in all databases for which you have a driver and see it work for yourself. The source of the Tool can be downloaded as well.


4. MANIPULATING INSTANCES

This chapter will tell you how you should open and close databases, start, commit or rollback transactions and add, change or remove instances. It will also tell something about garbage collection.

4.1 How to Open and close databases

In the context of the Database Access Library the model and the database are the same. To open the database the model has defined a method open that takes five attributes. The first attribute specifies the driver that you want to use. The other four specify the username, password, database and host. How to use these is driver dependent so check out the information about that specific driver in chapter 7 or the readme-file you can find in the driver distribution.
Before you can open a registered driver you have to supply your registration code. You do this with the registerDriver method of the model object. This methods takes the driver identification label, the registration name and codes as parameters.
Once you have registered a driver you do not have to do this again when you want to open the same model again.
Here is an example that opens a MS-Access-file. For the MS-Access driver you should fill in the username and password and the MS-Access database file. Your code should look something like this:

const char* regName = "Your Name"; // This registration code is just 
const unsigned long regCode[4] =             // a ficticious example
    { 0x01234567, 0x01234567, 0x01234567, 0x01234567 };
...
try {
  MyModel theModel;
  theModel.registerDriver(
   "dalmxsm12",regName,regCode[0],regCode[1],regCode[2],regCode[3]);
  theModel.open("dalmxsm12","Me","MyPassword",
             "d:\\storage\\MyDb.mdb","");
  ...
  ...
  theModel.close();
} catch (const Exception& e) {
  cerr << e.asString() << endl;
}
...
      
As you can see you should always use try-catch blocks to catch any thrown exceptions. Via the close-method of Model you can close the database.

4.2 Transactions

Before you can add, change or remove instances from the database you should start a transaction. Only one transaction at a time can be started. If you try to open another one an exception while be thrown. You start a transaction by calling the startTransaction method of Model.
You stop a transaction by calling either the commit of rollback method of Model. By commiting you validate the changes you have made. At that time the changes will be visible for all other users in the database. When you use rollback all changes will be reverted. This means your C++ instances will be brought to the state they had before you started the transaction. None of the other users of the database will have seen any changes.
When you commit or rollback while no transaction was started an exception will be thrown. Be sure to use try-catch blocks around any of these transaction methods. When something goes wrong during a commit an exception will be thrown. In that case you don't have to rollback the transaction manually this is already done by the library.
You should use a transaction to group together changes to instances that should succeed together or not at all. The transaction is not meant to be open for a long time. During the transaction a selection is not guaranteed to deliver all the right instances.

4.3 How to add, change or remove instances

During the execution of your program you will add, change and remove many instances the code described in chapter 3 will make sure you can do this almost in the same way as you would do this with any C++ object. There is one notable difference. To create an object you have to use the newInstance method of the class you want to get the instance from. To remove an instance from the database use the remove method of instance. You should not delete any of the instance-pointer. This is done automatically by the library. For changing objects you can use the add, remove and set methods you have defined in chapter 3. Code will look like this:

try {
  Pointer<OtherClass> theOther = ...;
  theModel.startTransaction();
  Pointer<MyClass> theInstance = 
        theModel.getClass("MyClass")->newInstance();
  theInstance->setName("TheInstance");
  theInstance->setOtherClass(theOther);
  theInstance->addOtherAssociation(theOther);
  theInstance->remove();
  theModel.commit();
} catch (const Exception& e) {
  cerr << e.message() << endl;
}
      

4.4 Pointers and Garbage Collection

You may have wondered why the fourth line of the last example uses a Pointer<MyClass> instead of MyClass*. The Pointer template is a smart pointer used in the garbage collection feature of the library. It is necessary for you to always use this smart pointer instead of the C++ pointer, when you declare variables. Otherwise it is possible that your instance is garbage collected while you still want to use it.You can for the rest use such a pointer in the same way as a C++ pointer with the exception that you can not delete it.
It is not necessary to use the smart pointers for parameters and returnvalues.

4.5 Multi-user issues

The library supports the optimistic locking strategy to make sure the database stays consistent. For this no extra code in required. If someone else has changed or removed an object that you want to change, an exception is thrown and the transaction is rollbacked. If you want to make sure that nobody can alter an object because you want to change it you can explicitly lock it with the lock method of Instance. This lock method can be called outside a transaction, but be aware that all locks will be removed at a commit or rollback.


5. SELECTIONS

The library supports queries on instances of classes with the selection class. You can specify conditions to restrict the instances that will be retrieved. With the iteration methods next and value you can retrieve the instances in the selection. A selection can be constructed giving the class of the instances you are interested in as parameter to the constructor. When you specify no conditions all instances of that class will be selected. Below you see an example of such a selection:

try {
  Selection sel(theModel.getClass("MyClass"));
  while (sel.next()) {
    Pointer<MyClass> theMyClass = (MyClass*)sel.value();
    ...
  }
} catch(Exception& e) {
  cout << e.message() << endl;
}     
At any time it is possible to abort the iteration. Only those objects that you iterate to are actually retrieved from the database server.

5.1 Polymorphic selections

The selection mechanism of the Database Access Library is polymorphic this means that when you select instances of class A and class A has subclasses B and C, you will receive instances of classes A, B, C.

5.2 Conditions

You can add conditions to a selection with the where-method of selection. The parameter of this method is a condition. This condition can be an attibute condition, an associated instance condition, an associated selection condition and a combination of conditions.

5.2.1 Attribute condition

The attribute condition is used to restrict the instances selected with a condition on an attribute. You use the operator[] on the selection to indicate which attribute you want. An example that selects all MyClass instances with the name "Hello" will look like this.

try {
  Selection sel(theModel.getClass("MyClass"));
  sel.where(sel["Name"] == "Hello");
  while (sel.next()) {
    Pointer<MyClass> theMyClass = (MyClass*)sel.value();
    ...
  }
} catch(Exception& e) {
  cout << e.message() << endl;
}     
Note that the parameter used in the operator[] should be the same as used in the handle-method of the instance-class.
At this moment the operators you can use in the attribute condition are:

5.2.2 Associated Instance Condition

The associated instance condition makes it possible to select instances that are related to a specific other instance via a specified association. For this the association is indicated using the operator() on the selection. An example of this is as below:

Pointer<OtherClass> oc = ...
...
try {
  Selection sel(theModel.getClass("MyClass"));
  sel.where(sel("FirstAssociation") == oc);
  while (sel.next()) {
    Pointer<MyClass> theMyClass = (MyClass*)sel.value();
    ...
  }
} catch(Exception& e) {
  cout << e.message() << endl;
}     
The identification string used for the association refers directly to the string used in the handle function for that assocation side.
In case of a recursive association naming the association is not enough to indicate a role or one side of the association, when necessary you can always use the role-name to explicitely name it. In the example above you could also write:
  sel.where(sel("FirstAssociation","FirstRole") == oc);

5.2.3 Associated Selection Condition

The associated selection condition makes it possible to create joins in your query. For instance you want to select instances of one class that are associated to instances of another class that are restricted by some condition. An example of this is shown below:

try {
  Selection otherSel(theModel.getClass("OtherClass"));
  Selection sel(theModel.getClass("MyClass"));
  sel.where(sel("FirstAssociation") == 
        otherSel.where(otherSel["Name"] == "Hello"));
  while (sel.next()) {
    Pointer<MyClass> theMyClass = (MyClass*)sel.value();
    ...
  }
} catch(Exception& e) {
 cout << e.message() << endl;
}     
In this case you can also use the operator() with explicit role parameter.
By creating two selections of the same class you can specify associated selection conditions along recursive associations

5.2.4 Combining Conditions

It is also possible to combine conditions. There are two variants, one based on the operator|| and one on the operator&&.These refer to the logical OR or AND. You can use them as follows:

Pointer<OtherClass> oc = ...
...
try {
  Selection sel(theModel.getClass("MyClass"));
  sel.where(sel["Name"] == 
      "Hello" && sel("FirstAssociation") == oc);
  while (sel.next()) {
    Pointer<MyClass> theMyClass = (MyClass*)sel.value();
    ...
  }
} catch(Exception& e) {
 cout << e.message() << endl;
}     
You can use brackets to indicate the order of evaluation of the conditions.

5.3 Sorting Instances

You can sort the instances on basis of attribute values. These sort orders can be descending or ascending where ascending is the default. For this you can use the method orderBy(...) with an attribute as parameter and optional a bool to indicate that it should be descending or ascending. The method can be called multiple times to add sort attributes. The second sort attribute will be used when values of the first sort attribute are equal. An example follows below:

try {
  Selection sel(theModel.getClass("MyClass"));
  sel.orderBy(sel["Name"]);
  sel.orderBy(sel["OtherField"],false); // sort descending
  while (sel.next()) {
    Pointer<MyClass> theMyClass = (MyClass*)sel.value();
    ...
  }
} catch(Exception& e) {
  cout << e.message() << endl;
}     
The sort functionality is also available in polymorphic selections.


6. EVENTS

The database access library uses events to notify listeners of changes to instances. It is strongly advised to use these events when you are building an interactive application. In this way you are automatically notified of not only your changes but also changes to objects because of rollbacks etc. To subscribe to an event you have to create an instance of a subclass of the Listener class. This listener can be notified of five different events. The instanceAdded event when an instance is created and stored. the instanceChanged when attribute values of an instance are updated, the instanceRemoved when an instance is removed from the database, the associationAdded when two instances are associated and the associationRemoved when the association between two instances is removed.
Which events the listener is notified of depends on which object(s) it subscribes to. You have the choice between a Class, an Association and an Instance. When you subscribe the listener to a Class you are notified of all instance events on instances of that class. When you subscribe to an Association you are notified of all association events of that type of association. When you subscribe to an Instance you receive notifications about instance and association events related to that instance. Be aware that you will never be notified of a instanceAdded event when you subscribe to an Instance because the instance has to be around to subscribe to first.
To subscribe to an object use the addListener method. To unsubscribe use the removeListener method.
To handle events you have to override the methods of the Listener class in your subclass. Let me give an example.

//define your event object
class MyListener : public Listener
{
private:
  MyWindow* myAdaptee;
public:
  MyListener(MyWindow* adaptee) { myAdaptee = adaptee; }
  void instanceChanged(const InstanceEvent& e) {
    myAdaptee->myObjectChanged((MyObject*)e.getInstance());
  }
  ...
  void associationAdded(const AssociationEvent& e) {
    if (e.getOneInstance() == myAdaptee->getMyObject()) {
      myAdaptee->myObjectAssociated((OtherClass*)e.getOtherInstance());
    } else {
      myAdaptee->myObjectAssociated((OtherClass*)e.getOneInstance());
    }
  }
};      
Now subscribe to the Instance somewhere in MyWindow:
...
MyListener l(this);
getMyObject()->addListener(l);
...     


7. THE DRIVERS

This chapter will describe each individual driver in detail

7.1 The files driver

This driver stores its data in binary files and is there only there for demonstration and testing purposes. It has severe limitations compared to drivers to relational databases.
It is NOT a multi-user driver. When you try to access the binary files with more than one client application at a time it can lead to data corruption. It does not implement a locking strategy. It does fully implement the transaction functionality however.
This driver does not fully support selections. It does however support selections of all instances of a class (and its subclasses) and instances related to another instance via a association. Navigational style selections are also supported.
This driver can become slow and inefficient when the database contains a lot of data.
To connect to a database the following parameters for the Model::open method:
Driver "dalflsmXX for MS Visual C++ or dalflsbXX for Borland C++Builder"
Username The correct username. For instance "Peter"
Password The correct password. For instance "Secret"
Database a full or relative path to a file called database.dal.
For instance "d:\storage\database.dal"
Host This is ignored so: ""
When the file database.dal does not exist it will create it. It will also create files for each non abstract class in the model that has instances with the name <ClassName>.cls. When the database.dal file already exists it will check if you used the right username and password before it will give you access to the data.

7.2 The MS-Access Driver

This driver connects to MS-Access database recognized by its extension .mdb. The driver uses ODBC and does not require you to install MS-Access itself. It fully supports all features of the Database Access Library.
To connect to a database use the following parameters for the Model::open method:
Driver "dalmxsmXX for MS Visual C++ or dalmxsbXX for Borland C++Builder"
Username The correct username. For instance "admin"
Password The correct password. For instance "Secret"
Database a full or relative path to a ms-access database.
For instance "d:\storage\test.mdb"
Host This is ignored so: ""
For more information see the readme file of the MS-Access driver.


8. COMPILATION INSTRUCTIONS

This chapter gives instructions on how to compile the library itself or an application itself.

8.1 How to compile an application.

To compile an application with the Database Access Library do the following: