|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Announcements
Chapters
Services
Feature Zones
|
Note: This is an unedited contribution. If this article is inappropriate,
needs attention or copies someone else's work without reference then please
Report This Article
Contents
IntroductionIn this article (actually my first article in The Code Project), I will introduce you to db4o, one of today's most popular Object-oriented Database Management System (ODBMS). But first, why should we need a ODBMS? Why can't we just use Relational Database Management System (RDBMS) such as Oracle and MS SQL Server? If you cannot answer these questions, I would suggest you to have a look at my two blog entries (see the Resources section) which attempt to explain the object-relational mismatch, the impact it has on the object model, and provide an overview of ODBMS as well as its advantages and disadvantages in comparison with RDBMS. Okay, assume that you already understand the context in which ODBMS is useful and are interesting in learning about how to make use of one of them, let's get start. db4o is an open-source native ODBMS available for both .NET and Java platforms. As a native ODBMS, the database model and application object model are exactly the same, hence, no mapping or transformation is required to persist and query objects with db4o. Regarding usage mode, db4o can be deployed either as a standalone database or a network database. Finally, db4o has support for the schema evolution, indexing, transaction and concurrency, database encryption, and replication service (among db4o databases and certain relational databases). The latest version of db4o is 6.1 and available under two licenses: GPL and a commercial runtime license. The domain modelWhile db4o's strengths are more obvious in applications with highly complex object model, the purpose of this article is more to offer an introduction to db4o, instead of exploring it in every level of dept. As a result, I will use an object model which is very simple but still comprehensive enough to demonstrate features of db4o. What we have is an object model for a painting application. There are two
concrete shape types,
The codeI will write code using C# 2.0. If you are from the Java space, you should still easily understand the code because the db4o libraries for the two platforms are virtually identical. Some notes about coding styles
The first step we need to do is to download the binary of db4o from its website, and then in VS.NET 2005, add a
reference to the Below is what the solution explorer looks like after we've added the references as well as created the source files needed. The source code of these model objects can be found in the source download of this article.
1. Storing ObjectsOkay, let's first create a new shape and store it into the database. // Open a local database located at DB_PATH
// Think of IObjectContainer as an ADO.NET connection
using (IObjectContainer container = Db4oFactory.OpenFile(DB_PATH))
{
// Let's create a circle
IShape circle = new Circle(new CPoint(1, 2), 5f);
// ...and then store it using the container
container.Set(circle);
// Get all objects of type Circle in the database
IObjectSet result = container.Get(typeof(Circle));
// Check if we have one circle
Assert.AreEqual(1, result.Count);
// ...and it has equivalent attributes
Assert.AreEqual(circle, result[0]);
// ...actually, it is the same object
Assert.AreSame(circle, result[0]); // ***Line 19***
}
What needs to be explained a bit is line 19 (see the ***Line 19*** comment), the retrieved object does not
only have the same attributes as the original object, but it actually is the
same object in memory ( Deeply nested objectsNow, let's create a more complex nested object using (IObjectContainer container = Db4oFactory.OpenFile(DB_PATH))
{
Circle circle = new Circle(new CPoint(50, 20), Color.Pink, 20f);
ShapeList list = new ShapeList();
list.Add(circle);
list.Add(new Line(null, null));
container.Set(list);
}
using (IObjectContainer container = Db4oFactory.OpenFile(DB_PATH))
{
IObjectSet result = container.Get(typeof(ShapeList));
// One shape list
Assert.AreEqual(1, result.Count);
ShapeList list = (ShapeList)result[0];
// ...has 2 children
Assert.AreEqual(2, list.Count);
// ...has the right circle
Circle circle = new Circle(new CPoint(50, 20), Color.Pink, 20f);
Assert.AreEqual(circle, list[0]);
}
Notice that I use two separate sessions, by opening and closing the object
container twice, that is to avoid the problem in which the object retrieved is
actually the one already existing in memory (if we happen to perform all calls
in one session). The object stored, 2. Updating ObjectsNow, let's see how we can update objects already existing in the database. As already mentioned in the discussion about weak references, you need to either have "live" objects (still in the db4o's references cache), by fetching them from the database or storing them in the same session, before being able to update them. We also use the IObjectContainer#Set() method to update objects. using (IObjectContainer container = Db4oFactory.OpenFile(DB_PATH))
{
// Create and store a new object
Circle circle = new Circle(null, Color.Red, 0f);
container.Set(circle);
// Retrieve and update the color
IObjectSet result = container.Get(typeof(Circle));
Circle storedCircle = (Circle)result[0];
storedCircle.Color = Color.Blue;
container.Set(storedCircle);
}
// Start a new session to avoid using in-memory references
using (IObjectContainer container = Db4oFactory.OpenFile(DB_PATH))
{
IObjectSet result = container.Get(typeof(Circle));
Assert.AreEqual(1, result.Count);
Assert.AreEqual(Color.Blue, ((Circle)result[0]).Color);
}
Update DepthNow, try the same update, but this time we will update the using (IObjectContainer container = Db4oFactory.OpenFile(DB_PATH))
{
CPoint point = new CPoint(10, 20);
Circle circle = new Circle(point, 0f);
container.Set(circle);
IObjectSet result = container.Get(typeof(Circle));
Circle storedCircle = (Circle)result[0];
storedCircle.Center.X = 30; // ***Line 9***
container.Set(storedCircle); // ***Line 10***
}
using (IObjectContainer container = Db4oFactory.OpenFile(DB_PATH))
{
IObjectSet result = container.Get(typeof(Circle));
Assert.AreEqual(1, result.Count);
Circle storedCircle = (Circle)result[0];
// This will fail
Assert.AreEqual(30, storedCircle.Center.X); // ***Line 19***
}
The last assertion will fail, However, note that if line 9 is replaced by the below statement, then the
assertion will pass since storedCircle.Center = new CPoint(30, 20); To make the code
works as expected we can either increase the Update Depth or turn on cascading
update for a specific type or for the whole database, as shown in the code
segment below. // 1: Turn on cascading update
container.Ext().Configure().CascadeOnUpdate(true);
// 2: Turn on cascading update for type Circle
container.Ext().Configure().ObjectClass(typeof(Circle)).CascadeOnUpdate(true);
// 3: Increase update depth
container.Ext().Configure().UpdateDepth(2);
// 4: Increase update depth for type Circle
container.Ext().Configure().ObjectClass(typeof(Circle)).UpdateDepth(2);
The above code only modifies the setting of a specific container, we can also
makes the settings applied for all containers by calling the same methods in the
3. Deleting ObjectsAs like update, you need to either have "live" objects before being able to
delete them. To delete an object, we simple call the using (IObjectContainer container = Db4oFactory.OpenFile(DB_PATH))
{
Circle circle = new Circle(new CPoint(1, 2), 0f);
container.Set(circle); // ***Line 4***
// Remove circle from db4o's referencing system
container.Ext().Purge(circle);
IObjectSet result = container.Get(typeof(Circle)); // ***Line 9***
Assert.AreEqual(1, result.Count);
container.Delete(result[0]);
Assert.AreEqual(0, container.Get(typeof(Circle)).Count);// ***Line 12***
}In the above example, I use a different technique to make sure the object
retrieved in line 9 is not the same in-memory object as the one stored in line
4, i.e. instead of closing the current session and opening another one for the
retrieval, I use a call the IObjectContainer.Ext()#Purge() method which will
remove the object passed as parameter from the db4o's internal reference
management system. Now, the code above does show that the Circle object is
deleted, but how about the CPoint object? Is it also deleted? Obviously, you
and I would expect the CPoint object to be deleted when the Circle object is
deleted since they have a composition relationship. However, let's think about
the association and aggregation relationships, in which the referenced objects
still make sense regardless of the existence of the referencing objects (e.g.
Album -> Song, Song -> Next Song etc.), will we still expect the
referenced objects to be deleted? The answer would be 'no' in most cases.
Since db4o does not know about the relationship semantics of our object model,
it cannot choose to delete every referenced objects from the root object. In
fact, the following assertion if added after line 12 would pass, since the
CPoint object is not deleted although its container (Circle) is. Assert.AreEqual(1, container.Get(typeof(CPoint)).Count);In
order to have CPoint deleted, we need to turn on the cascade delete option for a
specific type or for the whole database with the following calls // 1: For the whole DB
container.Ext().Configure().CascadeOnDelete(true);
//2: For a specific type
container.Ext().Configure().ObjectClass(typeof(Circle)).CascadeOnDelete(true);The
last point of interest regarding object deletion is that if there is an object
in the database referenced to by more than one references (in one or more
objects) and that object is deleted, then all the references will be null when
they are retrieved from the database. This maybe an unexpected behavior when we
delete a parent object with cascade delete enabled just to find out later that
the children objects are also deleted while they are referenced to by some other
parent objects. The code segment below shows this unexpected behavior in action
using (IObjectContainer container = Db4oFactory.OpenFile(DB_PATH))
{
// Enable cascade delete for type Line
container.Ext().Configure().ObjectClass(typeof(Line)).CascadeOnDelete(true);
// A point shared by both lines
CPoint sharedPoint = new CPoint(99, 100);
Line line1 = new Line(sharedPoint, null);
container.Set(line1);
Line line2 = new Line(sharedPoint, null);
container.Set(line2);
// The shared point is also deleted with this call
container.Delete(line1);
}
using (IObjectContainer container = Db4oFactory.OpenFile(DB_PATH))
{
IObjectSet result = container.Get(typeof(Line));
// line2 unexpectedly "lost" its point
Assert.IsNull(((Line)result[0]).Start);
}
4. Querying Objectsdb4o exposes three different APIs for application developers to retrieved stored objects, they are Query-By-Example (QBE), Simple Object Data Access (SODA), and Native Query (NQ). These APIs differ in their ease of use and flexibility, as we will see shortly. Before going to details about each API, let's first talk about an important concept of db4o which applies to all query APIs: Activation Depth. Activation DepthThink of the Person example mentioned in the discussion about cascade update, when we retrieve a person from the database, since this person may contain a reference to a list of friends, each of whom may contain another list of friends and so on, it is possible that we will pull out the entire database with one single call while what we really need is just one person. In order to avoid this problem, db4o controls the level of objects to be "activated" as part of a query via the Activation Depth setting. If the container is configured with an Activation Depth of 5 for a certain type, then when objects of that type are loaded from the database, only 5 level of object references are "activated" (e.g. instantiated and populated with stored values) and the reference at the 6th level will not be "activated" (all attributes are set to their default values). Let's go through an example to demonstrate this point. using (IObjectContainer container = Db4oFactory.OpenFile(DB_PATH))
{
ShapeList list = new ShapeList();
list.Add(new Circle(new CPoint(50, 50), Color.Pink, 20f)); // ***Line 4***
list.Add(new Line(new CPoint(10, 5), new CPoint(5, 10), Color.Purple));
container.Set(list);
}
using (IObjectContainer container = Db4oFactory.OpenFile(DB_PATH))
{
//Default is 5, set it to 2 for all types
container.Ext().Configure().ActivationDepth(2); // ***Line 11***
IObjectSet result = container.Get(typeof(ShapeList));
// We have a ShapeList with two children
Assert.AreEqual(1, result.Count);
Assert.AreEqual(2, ((ShapeList)result[0]).Count);
// Circle is an unactivated object
Circle circle = (Circle)((ShapeList)result[0])[0];
Assert.IsNull(circle.Center); // ***Line 21***
Assert.AreEqual(Color.Empty, circle.Color); // ***Line 22***
Assert.AreEqual(0f, circle.Radius); // ***Line 23***
// Let's activate it
container.Activate(circle, 1); // ***Line 26***
Assert.IsNotNull(circle.Center);
Assert.AreEqual(Color.Pink, circle.Color);
Assert.AreEqual(20f, circle.Radius);
// Center is still not activated
Assert.AreEqual(0, circle.Center.X);
// Ok, activate the Center object
container.Activate(circle.Center, 1); // ***Line 33***
Assert.AreEqual(50, circle.Center.X);
}
Since the default Activation Depth is 5, in line 11 we need to configure it
to 2 in order to demonstrate the problem. With Activation Depth as 2, only the
returned The next few paragraphs will discuss about each of the query API supported by db4o. But first, let's populate the database with the following objects and write the query using (IObjectContainer container = Db4oFactory.OpenFile(DB_PATH))
{
container.Set(new Circle(new CPoint(-5, 15), Color.Red, 10f)); //#1
container.Set(new Circle(new CPoint(20, 30), 0f)); //#2
container.Set(new Circle(new CPoint(100, 6), Color.Blue, 1f)); //#3
container.Set(new Circle(new CPoint(-10, -20), Color.Pink, 10f)); //#4
container.Set(new Line(new CPoint(5, 10), new CPoint(20, 30))); //#5
container.Set(new Line(new CPoint(15, 0), new CPoint(-10, 5), Color.Pink)); //#6
container.Set(new Line(new CPoint(15, 0), new CPoint(5, 10), Color.White)); //#7
container.Set(new Line(new CPoint(0, 5), new CPoint(5, 10), Color.Green)); //#8
}
Query-By-ExampleQBE is the most basic mechanism to query objects from db4o's databases. To
look up objects, we would need to create a example (or template) of the kind of
objects we want to retrieved by specifying the attributes, which are part of the
search criteria, with specific values while leaving the remaining attributes
with their default values (null for reference type, using (IObjectContainer container = Db4oFactory.OpenFile(DB_PATH))
{
// Find all circles
IShape prototype = new Circle(null, new Color(), 0);
IObjectSet result = container.Get(prototype);
Assert.AreEqual(4, result.Count);
// Find circles with radius of 10, #1 and #4 will match
prototype = new Circle(null, new Color(), 10f);
result = container.Get(prototype);
Assert.AreEqual(2, result.Count);
// Find lines with the start point of (15, 0), and the color of pink
// Only #6 will match
prototype = new Line(new CPoint(15, 0), null, Color.Pink);
result = container.Get(prototype);
Assert.AreEqual(1, result.Count);
}In the first query, we create an Circle template filled with default
values and the matches would obviously include all Circle objects in the
database. This is actually the long version of the short-hand one we saw
earlier (IObjectContainer#Get(typeof(Circle)). The second and third queries do
populate some values to the templates' attributes, thus only object matching the
specified criteria are returned. So simple, right? Unfortunately, the
simplicity of QBE comes as an expense to its flexibility. In fact, QBE is not
sufficient to be used for certain querying tasks since it has the following
problems:
Simple Object Database AccessThe basic idea behind SODA is that each query is represented as a graph which includes nodes (each represents a class, multiple classes or an attribute) and constraints (criteria applied for each node). The querying engine will traverse these nodes and base on their constraints to select the objects to be returned as part of the result set. The SODA API allows application developers to build up query graphs and execute them. Let's say we are searching for "all circles whose center's X-coordinator is smaller than 100 and Y-coordination is greater than 6, and radius is not 10", the SODA code will be written as follows: using (IObjectContainer container = Db4oFactory.OpenFile(DB_PATH))
{
// Create the root node
IQuery rootNodeQuery = container.Query();
// Constraint to objects of type Circle
// Can use a template object since QBE is used internally
rootNodeQuery.Constrain(typeof(Circle));
// Create the node for circle.center.x
IQuery pointXNode = rootNodeQuery.Descend("center").Descend("x");
// Add a smaller-than-100 constraint
pointXNode.Constrain(100).Smaller();
// Create a node for circle.center.y
IQuery pointYNode = rootNodeQuery.Descend("center").Descend("y");
// Add a greater-than-6 constraint
pointYNode.Constrain(6).Greater();
// Create a node for circle.radius
IQuery radiusNode = rootNodeQuery.Descend("radius");
// Add a not-10 constraint
radiusNode.Constrain(10f).Not();
// Execute the query and assert the result
IObjectSet result = rootNodeQuery.Execute();
Assert.AreEqual(1, result.Count);
// #2 matches
Assert.AreEqual(new Circle(new CPoint(20, 30), 0f), (Circle)result[0]);
}Note that in order to create the nodes, we need to specify the
attributes' names ('center', 'y' etc.) and we need to modify the query code
should we decide to change objects' attributes names. By default, all the
constraints are ANDed, if we need to write OR queries, we need to explicitly
perform a call to the method Or() of the constraints. For example, if we want
to modify our search as "Find all circles whose center's X-coordinator is
(smaller than 100 and Y-coordination is greater than 6) or (radius is not
10)", we would rewrite the code as follow (I removed the comments existed
in the previous example for brevity) using (IObjectContainer container = Db4oFactory.OpenFile(DB_PATH))
{
IQuery rootNodeQuery = container.Query();
rootNodeQuery.Constrain(typeof(Circle));
IQuery pointXNode = rootNodeQuery.Descend("center").Descend("x");
IConstraint const1 = pointXNode.Constrain(100).Smaller();
IQuery pointYNode = rootNodeQuery.Descend("center").Descend("y");
// Add a greater-than-6 constraint
// "AND" it with const1
IConstraint const2 = pointYNode.Constrain(6).Greater().And(const1); // ***Line 11***
// Create a node for circle.radius
// "OR" it with const2
IQuery radiusNode = rootNodeQuery.Descend("radius");
radiusNode.Constrain(10f).Not().Or(const2);
IObjectSet result = rootNodeQuery.Execute();
// #1, #2, and #3 will match
Assert.AreEqual(3, result.Count);
}
The call to Native QueryNQ API is an attempt by db4o creators to make queries as close and natural to
the host programming languages as possible (as using (IObjectContainer container = Db4oFactory.OpenFile(DB_PATH))
{
IList<Circle> circles = container.Query<Circle>(delegate(Circle circle)
{
return (circle.Color != Color.Blue) && (circle.Radius <= 10);
});
// #1, #2, and #4 match
Assert.AreEqual(3, circles.Count);
}
Very simple, right? We can also sort the result set by creating a
Some of you may notice that this API looks like all of the objects of a specific type must be loaded from the database, instantiated, and passed to the Predicate so that it can be checked against the search criteria and this is very inefficient in term of memory and performance. Fortunately, what db4o does internally is analyzing the CIL of the Predicate delegate's body, building up ASTs (Abstract Syntax Trees) and translating them to SODA query graphs so that they can be executed just as any other SODA queries. (Actually, at run-time QBE queries is also translated to SODA queries but that is a straight-forward translation process and no CIL inspection process is necessary.) The bad news is that not all native queries can be optimized into SODA queries if they contain complex logic not supported by the optimizer (which is improved more and more with each release of db4o) and the worst case scenario is when all objects are instantiated for the query matching. Which API to Use?Having talked about all three db4o's query APIs, let's recap about when we should use which API type.
ConclusionBy now, I hope you have seen how easy and fast it is to write object persistence code with db4o. Unlike when developing application with RDBMS, with db4o, there is no need to worry about inheritance, deeply nested classes, complex associations, primary and foreign keys, XML mapping files, SQL, HQL or JDOQL etc. And while I have not touched many advanced features of db4o such as transaction, concurrency, networking database, and replication etc., I hope that I have provided enough information for you to start the discovery about this interesting product yourselves. Resources
| ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||