Object-oriented database programming with db4o - Part 1

4. Querying Objects

db4o 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 Depth

Think 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, it is possible that we will pull out the entire database with one single call while what we really need is just the information of one single 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));
   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);
   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);
   Assert.AreEqual(Color.Empty, circle.Color);
   Assert.AreEqual(0f, circle.Radius);

   // Let's activate it
   container.Activate(circle, 1);
   Assert.IsNotNull(circle.Center);
   Assert.AreEqual(Color.Pink, circle.Color);
   Assert.AreEqual(20f, circle.Radius);
   // ...one more activation is required here
   Assert.AreEqual(0, circle.Center.X);

   // Ok, activate the CPoint
   container.Activate(circle.Center, 1);
   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 shapelist and its internal children list (of type IList<IShape>) are activated.  The Circle and Line created in line 4 and line 5 will be loaded in the IList, but not activated, thus their attributes are set to the default values.  The assertions in lines 20, 21, and 23 validate the fact that objects located further than the current Activation Depth are not activated.  In line 25 and 32 we use the IObjectContainer#Activate() method to tell db4o to activate the Circle and Point objects (i.e. populate them with actual values stored in the database). If we happen to have 2 in the parameter list of the activation call in line 25 (which indicates 2 level of depth, including the parameter itself) , the activation made in line 32 will not be necessary. 

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 which will be used for the queries

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-Example

QBE is the most basic mechanism to query objects from db4o’s databases.  To look up objects, we would need to create a template (or an example) 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, null or “” for string type, 0 for numerical types, false for boolean type, and whatever default values [returned by new()] for struct type).  The result set will include objects which match all the specified criteria (AND matching).  Let’s fire up some QBEs

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 all 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:

  • Attributes with default values are never included as part of the search criteria, so you cannot perform searches like “Find all lines with an empty color”, or “Find all persons with age of 0″ etc.
  • Since the template is created by invoking the object constructor, any initialization done in the constructor may break the ability to perform searches by default values.  For example if we happen to initialize the Color of every Line object to Color.Black, then when we call IObjectContainer#Get() on an empty template (new Line(null, null, new Color()), we would only receive Lines which have their color as Black, instead of all Lines, as expected.
  • QBE cannot perform complex query expressions, such as “or”, “not”, “less than”, and “greater than” etc.  For instance, we cannot perform queries like “Find Circles whose radius is bigger than 10f”, or “Find Lines whose Color is different from black” etc.

Simple Object Database Access

The 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 to “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);

   // 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 And(const1) in line 11 is necessary, otherwise SODA will understand our intention as “x is smaller than 100 AND (y is greater than 6 or radius is not 10)” and only two objects match (#1 and #2).   SODA also support other constraint types (via methods of IConstraint such as Identity(), Like(), Contains()), sorting of result sets, searching of array or collection types, and a flexible Evaluator API which allows developers to write code to check whether a candidate object should be included in the result set.  In other words, unlike QBE, with SODA, you can do any kind of queries required for the application.

 

Native Query

NQ API is an attempt by db4o creators to make queries as close and natural to the host programming languages as possible (as LINQ/DLINQ).  In the .NET implementation, NQ makes use of the System.Predicate delegate (whose definition is ‘public delegate bool Predicate(T obj)’) to allow developers to write code to determine whether an object passed into that delegate should be included in the result set or not.  Let’s say we are searching for “all circles whose color is not blue and radius is less than or equal to 10″, the NQ code will look like

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 System.IComparer and passing it as the second parameter of the IObjectContainer#Query() method.  It is also obvious to see that with NQ, we can perform all kinds of queries, just as when using SODA

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 bytecode of the Predicate’s body, building up the ASTs for the code flows 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 bytecode 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 currently supported by the optimizer (which will be 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.

  • Query-By-Example: due to QBE’s limitations, it can only be used for very simple queries.  Given the same query, I personally think that NQ is as simple as QBE yet expresses the intention of the code much better, thus I would suggest NQ to be considered first, even for simple queries.
  • Native Query: NQ can be used to perform all kinds of queries, no matter how complex they are, and at the same time it is very expressive.  In addition, type-safety is also a plus for NQ, in comparison with SODA, and I would recommend NQ to be used by default for all querying needs.  Cases in which NQ needs to be avoided are when the SODA optimizer cannot translate native queries into corresponding SODA queries.  Fortunately, db4o does expose the API for developers to check whether their native queries are successfully optimized or not so that they can either modify the queries or choose to use SODA instead.
  • Simple Object Database Access: the least expressive of all three APIs and should only be used when NQ cannot be optimized and its performance is not acceptable.

5. Summary

By 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 (the list goes on…) etc.  Everything seems just so simple, natural and transparent and you are free to build up rich domain model with being constrained in any way. 

In the next part of this article, I will touch some more advanced topics of db4o.  Stay tuned!

 

Resources

Pages: 1 2 3

9 Comments

Vu ToMarch 7th, 2007 at 11:26 pm

Good Tool!!! From now, I do not worry about data access layer anymore.

I’ve already intended to use Hibernate in my current java project ;), …but now I think I should change my mind after reading this article.

Thank Buu for your introduction obviously.
To^

Buu NguyenMarch 8th, 2007 at 12:52 am

I am glad that you like it, Vu. I got to know db4o when evaluating ORM frameworks to be used for a trivial personal .NET project. My first cut was to look at NHibernate and iBATIS.NET and while they are really good tools, I just did not think that it was worth the effort to use any of them in my project. That was when I found out db4o and decided to give it a try. Within about 6 hours I could learn about the API and coded the data layer of my application, something I could not even believe had I not tried it myself.

Of course there is still a long way for db4o to make itself widely adopted in the enterprise space like RDBMS products like Oracle or SQL Server, but I think there is a chance as db4o itself, the tooling, and the community surrounding it become more mature.

Buu Nguyen’s Blog » I am a dVPSeptember 19th, 2007 at 11:20 pm

[...] have just been recognized as a db4o Most Valued Professional (dVP) for the year 2008 and won a trip to Berlin next year to attend the ICOODB 2008 conference. It [...]

IdetrorceDecember 15th, 2007 at 8:25 pm

very interesting, but I don’t agree with you
Idetrorce

Chu_Thi_HueAugust 22nd, 2008 at 1:23 am

I want to create a button, when i click that button it will create a
new database (with db4o in Winds Form application (C#)). That code is:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
using com.db4o;
using Db4oTools;
using System.IO;

//…………

private void btnThem_Click(object sender, EventArgs e)
{
//File.Delete(Util.YapFileName);
IObjectContainer db = Db4oFactory.OpenFile(Util.YapFileName);
try
{
//
}
finally
{
db.Close();
}

}

and i added: db4o and Db4oTools in References. But i couldn’t create a new database. So, can you help me? Thanks very much!

Buu NguyenAugust 22nd, 2008 at 9:41 am

@Hue:
I suggest you review to see if Util.YapFileName contains the correct link to the db file. If it does, can you describe in detail what the exact problem that you have is? Can you send the stack trace of the exception if any? If there’s no exception, how do you know if you couldn’t create a database?

Chu_Thi_HueAugust 23rd, 2008 at 8:06 pm

Thanks very much! I have just created a new database.
I am doing my “plan for graduating” about OODB and I must write a application ( C#.net and db4o). But i don’t have much document about OODB and db4o, i hope you will send for me if you have!
My name’s Hue. I’m a senior student in University of Technical Education HCM City. I am very glad when I know about you! :)

Buu NguyenAugust 25th, 2008 at 4:13 pm

@Hue:
A great deal of documentation about db4o is located at http://developer.db4o.com/Resources/view.aspx/Documentation (you will need to register for an account).

Or you can refer to this book http://www.amazon.com/Definitive-Guide-db4o-Stefan-Edlich/dp/1590596560, which is by far the only book on db4o. While the book does not contain much depth and is a bit out of date, it should be sufficient for newbies to get up to speed with db4o.

Nice to know you and good luck on your studying!

Chu_Thi_HueAugust 26th, 2008 at 10:17 pm

Thanks very much! And good luck to you everything!

Leave a comment

Your comment