| The Database Access Library for C++ |
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
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:
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.
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: ""
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:
For more information see the readme file of the MS-Access driver.
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: ""
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: