Using an Object-Oriented Database in a Web Site

Mar 5, 11:00 pm

Article Author: James Paterson
.NET 3.5 Books

Introduction


Most web sites which make use of server-side technologies like ASP or ASP.NET have a database in the background providing the dynamic content which is extracted and served up to the user. Usually this is a relational database such as SQL Server or even Access. However, ASP.NET is based on programming languages, primarily C# and VB.NET, which are very much object-oriented in nature. So, is there any benefit to be gained from using a database which works the same way as the programming language, namely an object-oriented database (OODB)? What kinds of applications would be most suited to OODB usage? Where do you get hold of such a database, and how easy is it to get started?


This article describes some of the ways in which an OODB differs in principle and in practice from the more familiar relational databases, and identifies some of the products which are suitable for use with ASP.NET. One of the simplest OODBs to get started with is the open-source db4o, which I will use to demonstrate some of the benefits of this type of database.


The use of object-oriented languages allows application designers and developers to build applications using object-oriented domain models. This kind of model contrasts with the simpler approach where each page fetches some data, does something with it and displays the result. The latter approach, referred to by Martin Fowler, in his book Patterns of Enterprise Application Architecture, as the Transaction Script pattern, is simple but often leads to duplication of code and can be difficult to manage as system size grows and business logic becomes more complex. The domain model approach allows business logic to be associated directly with objects in the application. The code in the domain classes does not depend on the user interface or any other layer of the application.


There the database plays different roles in these approaches. In the Transaction Script pattern, the database is central to the application – the application’s role is to perform transactions on the data, and the associated business logic may be in some tier of application code or in stored procedures in the database. With a domain model, the objects and their interactions are central, and the database simply provides a mechanism to make some of those objects persistent. In this article, I focus on the latter approach.


The problem with using a domain model and a relational database is that you then have two different models of the entities within your application. Although there are superficial similarities, the object-oriented and relational models have some fundamental differences in the way they let you define entities and the relationships between them. One way to bridge the gap between the models is to use an object-relational mapping (O/R-M ) tool. As you will see, however, using an OODB can eliminate the problem of the mismatch between models completely.


System Requirements


To run the code for this sample you should have:


  • IIS running on Windows 2000 or later

  • The .NET Framework version 2.0 or higher.

  • VS 2005 or Visual Web Developer Express 2005

  • db4o version 5.5 for .NET 2.0

  • db4o Object Manager 1.8

db4o and the Object Manager browser are free downloads from the Developer Resources area on the db4objects website. Version 5.5 is the current release at the time of writing. Free registration gives you access to new development versions and other developer resources. Make sure you download the correct variant – db4o is also available for .NET 1.1, Java and Mono. Object Manager is a Java application, but it can browse databases containing .NET objects. Download the "no JVM" version of Object Manager if you already have a Java runtime environment installed.


Installing and Compiling the Sample Code


The sample download for this article contains an ASP.NET web site folder called db4oasptoday. The folder contains three ASP.NET web pages with code-behind pages, and a set of classes in the App_Code folder. All code is written in C#. To install the sample, you should extract the contents and open the db4oasptoday folder in VS 2005 or VWDE 2005. You can run the sample from within the IDE using the ASP.NET development web server.


To use the db4o database in a web site you simply add the db4o DLL as a reference in the project. The db4o download is an MSI file which you run to install all the db4o files on your computer. The appropriate version of db4o.dll is located in the dll/net folder in the db4o download (there is also a version for the .NET Compact Framework – don’t use this). The full db4o distribution as it contains documentation, source code and a useful tutorial.


To run the db4o Object Manager, extract to contents of the ObjectManager download to a folder and run the file objectmanager.bat. When you open a file in Object Manager it is locked for exclusive use. You should stop the web application running before you browse the database contents with Object Manager.


The Impedance Mismatch


To most developers the word "database" is synonymous with "relational database". The remarkable success enjoyed by relational databases over many years has been due to a number of factors: the relational data model, which supports consistent, logical representation of data; SQL, which gives powerful ad-hoc query capabilities, largely standardized across different database products; and the support of powerful vendors such as Oracle and Microsoft.


However, the relational data model presents obstacles for developers of object-oriented systems. The relational and object-oriented models are fundamentally different in a number of ways, although each model has its own strengths. The relational model is motivated by the need to represent information, while the object-oriented model captures the behavior and interactions of entities. It is very common for applications with domain models developed using object-oriented techniques and programmed in an object-oriented language, to use a relational database as its underlying persistent data store. The differences in the models result in a mismatch at the interface between the application and the database – this is often referred to as the impedance mismatch, analogous to the electrical engineering scenario of joining mismatched transmission lines. In both cases, the impedance mismatch leads to a loss of efficiency.


Differences between the Object-Oriented and Relational Models


In simple scenarios, there often appears at first to be little difference between the way entities are represented in the application (as objects) and in the database (as rows in tables). For example, you might have an order processing system which includes StockItem, Order and OrderLine classes to define the objects. So, you just create tables with similar names, and store the state of StockItem objects in the StockItems table, and so on. A UML class diagram of this scenario is shown in Figure 1, and the corresponding entity-relationship diagram (ERD) for the database is shown in Figure 2.



Figure 1. UML class diagram for a simple scenario



Figure 2. ERD for a simple scenario


To make objects persistent, you need to save their state values. In this example, the fields of each class are mapped to columns in the appropriate tables. This is a simple example of object/relational mapping (O/R-M).


These diagrams look pretty similar, but even in this simple case there are some pretty fundamental differences. Differences arise due to the way the two models deal with identity and relationships.


Identity


In a typical object oriented environment, each object is automatically assigned a unique ID by the system. The value of this internal ID is not under the control of, or generally used by, the programmer, and does not depend on the values of any of the fields. The environment can use the ID to uniquely identify any object – if two object references contain the same ID, then they must refer to the same object. However, you can have two distinct objects with identical field values, but different IDs. Internal object IDs are important in defining relationships between objects – an object can hold references which contain the IDs of any objects to which it is related.


In contrast, in the relational environment, entities are identified by values stored in one or more columns. These may be columns specifically included in the table design for the purpose of defining identity, often auto-number columns. By defining a primary key you can ensure that the database will not accept rows with duplicate values in the primary key column or columns.


One of the decisions you need to make when mapping from application to relational database is how to handle IDs. Do you include ID fields in the classes to match the database ID columns? The StockItem class in Figure 1 and the corresponding table in Figure 2 both have a StockID, for example. Do you generate ID values in the application at the point of object creation, or using auto numbering in the database at the point of object storage? Do you enforce uniqueness in the logic of the application, or do you let the database take care of it?


In this example, it may make sense in the domain model to have a StockID field. However, the application does not need such a field to keep track of which object is which. There are in effect two different identity systems active within the application. You need to take care with this. Consider, for example, an application which executes two separate queries which return the same row from the StockItem table. If the application uses the query result in each case to construct a StockItem object, then you could have two objects (with different internal IDs) in memory with the same StockID. You would need to do some extra work to avoid this by maintaining an object cache, or an identity map which tracks what rows of the table correspond to objects currently in memory. With an object database, on the other hand, the application and database use similar identity systems, and object caching is usually a standard feature.


Relationships


You can think of an Order object in this scenario as an object which encapsulates everything about an order. For example it contains everything it needs to calculate and return its total cost. To do so, it contains other objects (a collection of OrderLines, each of which contains a StockItem). These relationships are represented as aggregations or associations in the UML class diagram Figure 1, and implemented by object references in code. References can be navigated – for example to access the OrderLine objects associated with a particular Order object you navigate the OrderLines reference. This navigation is directional – if you want to be able to find the Order for a particular OrderLine object you need to include an appropriate reference in the OrderLine class.


In contrast, the relational database takes the information and puts it in tables in such a way as to support data integrity and efficient storage. Relationships between tables are defined by common values in fields, and these fields can be used to join tables in SQL queries. The integrity of these relationships can be enforced with foreign key relationships, shown in the ERD Figure 2. For example, the OrderLines table has a field OrderID which relates to the equivalent field in the Orders table, and a foreign key is defined to ensure that an OrderLine is associated with a valid Order. Note that a similar level of integrity is easy to ensure in an object-oriented application if it is written carefully – in this case you would just make sure that every new OrderLine gets added to an existing Order object.


If you want to store an object which contains other objects (a structured object), you must map these to separate tables. Storing an Order object in this scenario involves breaking it into its constituent parts and storing each part with a separate INSERT SQL statement. Retrieving a structured object can also be quite complicated – you need to execute queries which join tables to find all the required field values, and reassemble, or marshal, these to form the original object. This is one of the biggest drawbacks of relational storage for objects: everything is normalized for optimum storage, not for retrieving all information about a particular object.


Many-to-Many Relationships and Inheritance


The object-oriented model allows some kinds of relationship which cannot be directly implemented in a relational database. For example, students on a college course may be enrolled on several modules, while each module will probably have many students enrolled on it. This scenario can be modeled in an object-oriented system as a many-to-many relationship – the Student class would have a reference to a collection of Modules, while Module could have a reference to a collection of Students. The relational model does not directly support many-to-many relationships, so mapping this to a relational database would require an additional join table which would contain only the key information to point to enrollments.


Returning to the order system, what if there are different kinds of StockItem, with different attributes and behavior? You might have BookStockItems, DVDStockItems, and so on. In an object oriented system this can be modeled efficiently with inheritance, by placing common behavior in a StockItem abstract class and creating subclasses to represent the specific item types, as shown in Figure 3. The subclasses can add new behavior. Alternatively they can redefine existing behavior – for example, different types of item could calculate their cost in different ways, but each would have a method called Cost() to do so. Polymorphism allows you to use the different item types interchangeably. If an OrderLine contains a StockItem, it doesn’t matter which type of item it is as long as it is a subclass of StockItem. Since the abstract class and the subclasses all have a Cost() method, then any object in this hierarchy can be asked to return its cost – the details of how it does so will depend on the actual class of which the object is an instance.



Figure 3. Inheritance in the object-oriented model


This situation doesn’t work so conveniently when you need to map to a database. The relational model does not support inheritance. A few products have a form of table inheritance (e.g. PostgreSQL), but it is not a standardized feature. There is no single optimum strategy for mapping inheritance hierarchies to tables – the options include:


  • one table per class: in the simple scenario here this would require a StockItems table to hold the common attributes and separate BookStockItems and DVDStockItems tables to hold the specific attributes. Retrieving one entity would require joining to tables (for example StockItems and BookStockItems) to get all the attributes for that entity, and key fields would need to be included to allow joining. The need to join tables is likely to have implications on performance.

  • one table per concrete class: here you just need two tables, separate BookStockItems and DVDStockItems. Each row contains all the information relevant to a particular entity. There is no need to join tables to retrieve entities, but there is duplication within the schema. Adding a new sub-type to the system involves further duplication.

  • one table for the whole hierarchy: here you have a single table, StockItems. Again each row contains all the information relevant to a particular entity, but now a row can represent any of the sub-types. The disadvantage is that the table must have fields to represent all possible attributes, many of which will be redundant for each entity, and a discriminator column is needed. Adding a new sub-type to the system involves altering the table schema.

All of these have drawbacks in one or more of performance, storage efficiency and maintenance, and the drawbacks become more significant as the model becomes more complex.


Bridging the Gap


If you use a relational database and an object-oriented domain model, the impedance mismatch means that work has to be done someplace in your application to handle the mapping. This means that you need code, often quite a lot of code, simply to make your domain objects persistent. There are, however, ways in which life can be made easier and more productive:


O/R mapping tools: these tools aim to allow the developer to work in a purely object-oriented environment while the tool is working behind the scenes to marshal the data in the objects into and out of a relational database. There are many such tools for .NET, including NHibernate and Microsoft’s own forthcoming LINQ/LINQ to SQL (originally called DLINQ) in the next version of Visual Studio. Outside the .NET environment there are many more examples. Ruby on Rails is an example of a tool which has experienced a lot of success through offering a tightly knit, simple O/R-M support In principle O/R mappers are a bit like a duck swimming – the developer sails serenely on the object-oriented surface, while below the surface the mapper is thrashing away like the duck’s legs, generating and executing the SQL which the database needs.


Object-oriented databases: if the application and the database use the same model, then there won’t be a mismatch. The data model can represent identity and relationships in the same way as the application. This is the option I am looking at here.


A Brief History of OODBs


Object-oriented databases are not new. Commercial products started to appear at the end of the 1980s, and there was a lot of interest in so-called "post-relational" databases in the 1990s. However, the term was a bit optimistic as relational databases are clearly still dominant. Object-oriented databases had some real advantages in some niche areas, which led to some success in specific markets, but these were not enough in the mainstream to overcome disadvantages such as poor query support and the fact that they weren’t being sold by large database vendors. Lack of a standard as widely accepted as SQL is often mentioned as a reason for failure, although a lot of effort went into developing the Object Data Management Group (ODMG) standards.


In parallel, some relational database vendors added object-oriented features to their databases to create object relational, or hybrid, databases. The SQL 99 standard has an object-oriented model which supports user-defined types. However, these features do not necessarily make interfacing with .NET applications any simpler.


So why look at object-oriented databases now? Well, the context for object persistence has changed considerably. Some of the factors which make OODBs more interesting than they might have been a few years back are:


  • more object-oriented programs: more developers than ever before are using "proper" object-oriented languages (C#, VB.NET,Java, etc.)

  • more databases: the development of dynamic websites and mobile devices has opened up new applications for databases outside the enterprise datacenter

  • new products: vendors are applying fresh thinking to meet the needs of today’s developers

The renewed interest in OODBs is supported by the fact that the work of the ODMG, which was disbanded in 2001 after establishing version 3 of its OODB standard, is now being resurrected by the Object Database Technology Working Group (ODBT WG), which is working on a 4th generation standard.


OODBs for .NET


There are a number of OODB products which support .NET and can be used with ASP.NET. These tend to fall into two categories, which are illustrated by the products described below. Most of these products also support other languages, mainly Java.


The following are commercial products. They map .NET objects to persistent objects using proxy objects or by code enhancement, and focus on enterprise scalability, and provide SQL or similar query support.


  • Matisse

  • FastObjects.NET

  • Cach

The following open-source products take a very different approach. They are optimized for use embedded within applications, particularly on mobile devices. A key feature is a very small footprint – db4o, for example, simply requires that you add a 600kb DLL to your project. They are true native object databases – plain .NET objects are stored "as they are". One limitation with this kind of database is that the data can only be accessed by native applications with access to the class schema, which makes them unsuitable for applications where the database has to support diverse applications written in different languages:


  • db4o – open-source but with commercial licensing for non-open-source applications

  • Perst

Details of other object database vendors, including those whose products support other languages, can be found on the ODBMS.org portal.


Best-fit Applications for OODBs


Obviously an OODB is only of benefit if your application uses a domain model. The extent of the benefit depends on the nature of that model. The open-source benchmark Pole Position provides some interesting performance comparisons of the db4o OODB with a range of relational database object persistence mechanisms. The products are Java-based, but the conclusions should be valid in the .NET world. In general, if you have complex object structures with deep object graphs or deep inheritance structures, then db4o performs very well. The flatter and simpler your object-oriented model, the more the comparison favours relational solutions. An example of a successful OODB web application is provided by the Institute for Geoinformatics, in Mnster, Germany and the CIE Energy Research Center in Temixco, Mxico, who use db4o to support a comprehensive web-accessible database for complex geochemical data.


The Sample Application


To illustrate the use of an OODB I have chosen to use db4o database. I have done so primarily because of its ease of both configuration and programming, which makes for a straightforward example to illustrate the difference which an object database makes to an application. Although it is targeted largely at embedded use, a look at the user community forums shows that many of its users are developing web sites in ASP.NET. It has limitations in terms of concurrency and locking which would be significant in very high traffic web applications but which are acceptable for many websites. These limitations relate to the focus of the developers of this particular project rather than to inherent problems with OODBs. As a community-driven project, it is likely that db4o will be developed to optimize its support for web environments. The sample demonstrates some basics of using db4o in an ASP.NET environment – accessing the database, executing simple queries and displaying the results.


The Domain Model


The sample application has a very simple domain model based on the order system described earlier in this article. There are Order, Orderline and StockItem classes, and the latter has two subclasses, BookStockItem and DVDStockItem. The classes contain properties and methods as shown in the UML diagrams Figure 1 and Figure 2. Although the model is oversimplified, it does illustrate important relationship types. All code in this article is in C#.


Database Design


Actually, there’s nothing to add under this heading as the classes in the domain model are the database design. Note that there is no database-aware code in these classes as db4o persists plain .NET objects. In fact, some proponents of db4o describe it as a persistent extension of the in-memory object-oriented model rather than a database in the traditional sense. This is true up to a point, but some of the usual database concepts, such as transactions, are still important.


Some db4o Basics


Using db4o is very different from working with relational databases, so before I launch into describing the full sample, I want to show a few code snippets to familiarize you with some of the basic concepts and operations of db4o.


Firstly, there is no need to install or run a database engine – the db4o "engine" is contained in the db4o DLL which is added to your project as a reference. This gives your project access to the API which allows your program to use the database.


Storing Data


The most important API class is ObjectContainer, which encapsulates a database. db4o stores its data in files, by convention with the extension .yap. The following code snippet opens an ObjectContainer using the file demo.yap (a new empty database file is created if no existing file is found), instantiates an object and stores it with a call to the Set method of ObjectContainer. Opening the ObjectContainer starts a transaction, which is committed to the database, and the ObjectContainer is then closed.



using(ObjectContainer db = Db4o.OpenFile("demo.yap"))
{ db.Set(new BookStockItem("B03", "4th of July", "James Patterson", 9.99, 50)); // no relation! db.Commit();
}


Queries


The basic query mechanism for db4o is simple query-by-example (QBE). The following snippet retrieves the object stored by the preceding snippet by constructing a template object with the query criteria (just the Title field) set to the target value and all others null or zero. ObjectSet is another db4o API class which holds all the objects returned by a query.



using(ObjectContainer db = Db4o.OpenFile("demo.yap"))
{ BookStockItem template = new BookStockItem(null, "4th of July", null, 0.0, 0); ObjectSet result = db.Get(template); BookStockItem foundBook = (BookStockItem)result.Next();
}


QBE is not a full-featured query mechanism – it is limited to direct matches for field values. Db4o’s advanced query mechanism is known as Native Queries. A native query is expressed entirely in the application’s programming language, C# in this case. There are no embedded query strings or names of stored procedures which can’t be checked by the compiler. As a result the queries are typesafe, and can benefit from IDE code hints.


The query in the previous snippet can be rewritten as follows. Note that the query expression inside the delegate can contain any code you like to define your criteria, including operators like < and >.



using(ObjectContainer db = Db4o.OpenFile("demo.yap"))
{ IList<BookStockItem> results = db.Query<BookStockItem>(delegate(BookStockItem book) { return book.Title.Equals("4th of July "); } BookStockItem book = (BookStockItem) results0;
}


Note: this code reads as if it instantiates every BookStockItem object in the database and evaluates the query expression against each one, which would be expensive in terms of performance. Actually, some clever optimization is done behind the scenes so that this is usually not the case.


Modes


The previous snippets show the use db4o in its standalone single-user mode. Only one process or thread can access the database in this mode. Db4o also supports two client/server modes, both of which use an ObjectServer object to encapsulate the database and listen for client requests:


  • Network server: the database can be accessed by concurrent clients over a network. Clients specify the IP address or hostname and port for the server process.

  • Embedded server: the database can be accessed by concurrent clients within the same process. This is the most common mode for web applications where the database is one the same computer as the web application. Clients need a reference to a server object to open a connection to the database.

The following snippet shows the use of embedded mode.



ObjectServer server = null;
try
{ server = Db4o.OpenServer("demo.yap", 0); // 0 is port – nonzero // value indicates // network server using(ObjectContainer db = server.OpenClient()) { db.Set(new BookStockItem("B03", "4th of July", "James Patterson", 9.99, 50)); db.Commit(); }
}
finally
{ server.Close();
}


Website Example


Using db4o in a web site is pretty much a case of putting code in your pages or code-behind classes similar to the basic code snippets in the previous section. However there are a few further issues to consider in an ASP.NET environment, including:


  • How, and when, do you start and stop the server?

  • How, and when, do you open and close a client connection to the server?

  • How do you display the results of a query to the user?

Connecting Servers and Clients


When you are connecting to a database from a web page you usually get a connection, do some work with the connection and close or release it. Each request typically gets its own connection, which may be taken from a connection pool to reduce the performance hit associated with creating connections.


You can use db4o in pretty much the same way. However, there are a couple of things about db4o which you might want to think about.


  • Firstly, db4o does not currently have a connection pooling mechanism, although it would be possible to implement one.

  • Secondly, there is a difference between the way relational and object databases associate objects in memory to the contents of the database. For example, if two queries within the same transaction on a relational database return the same entity, then it is likely that two distinct objects will be created in memory representing the same information (to prevent this you can maintain an identity map, which allows you to check each entity returned by a query to see if any in-memory objects match its properties). Object databases, on the other hand, usually maintain their own cache of objects recently retrieved from the database. With db4o this is a cache of weak references to these objects, which means they are eligible for garbage collection when no longer in use. A subsequent query for an object which has been accessed previously in the transaction simply returns the cached copy, which is fast and avoids unnecessary use of memory.

So, how many client ObjectContainers should you open? One per request? One per session? One for the application? There is some debate about this in the db4o community. The latter option minimizes the number of connections made and allows all requests to share the same object cache – good for performance and memory usage. However, objects in the cache may potentially be updated by multiple requests at the same time, and should be synchronized to prevent this, and some people are uncomfortable with such long-running transactions. Per-request or per-session connections give each user a separate object cache, at the expense of performance. The choice will probably depend on traffic levels, whether different users access the same objects and whether they update the objects, and should be guided by real-world usage tests. I use a single application client in the sample, but it is easy to change this.


Making Clients Available to Pages


To access the database within the code for a page you need a reference to a client ObjectContainer. One way of doing this is to use the global.asax file to start an ObjectServer when the application starts. You can then store a reference to the ObjectServer in the Application object and use this reference in each page to open a client ObjectContainer (one client per request). Alternatively you can open a client when a session starts in the Session_start event in global.asax and place the client reference in the Session object (one per session) or simply place a client reference in the Application object (one per application). A listing of global.asax with the last of these options is shown below:



<%@ Application Language="C#" %>
<%@ Import Namespace="com.db4o" %>
<%@ Import Namespace="com.db4o.config" %>
<%@ Import Namespace="System.IO" %>
<%@ Import Namespace="System.Text" %>
<script runat="server"> void Application_Start(object sender, EventArgs e) { string filePath = Server.MapPath("~") + ".yap"; ObjectServer server = Db4o.OpenServer(filePath, 0); Application["db4oserver"] = server; ObjectContainer client = server.OpenClient(); Application["db4oclient"] = client; } void Application_End(object sender, EventArgs e) { ObjectContainer client = (ObjectContainer) Application["db4oclient"]; client.Close(); ObjectServer server = (ObjectServer) Application["db4oserver"]; server.Close(); } void Session_Start(object sender, EventArgs e) { // could alternatively open client here } void Session_End(object sender, EventArgs e) { // could close client here }
</script>


Alternatively, you could open the ObjectServer and ObjectContainers within an HttpModule rather than global.asax, which would have the advantage of encapsulating db4o-specific event handling within a separate reusable module. An example is given in The Definitive Guide to db4o found in the Related Links section.


Storing Data


The sample application simply stores some data when the default page loads. The following code, in the Page_load() method of default.aspx.cs, creates some stock items (books and DVDs), and creates an Order which includes each of these items. The resultant Order object is a structured object which contains all the other objects. The whole object graph is therefore stored with a single call to Set.



// create some test stock item objects
BookStockItem b1 = new BookStockItem( "B01", "Angels Fall", "Nora Roberts", 17.13, 50);
BookStockItem b2 = new BookStockItem( "B02", "Can’t Wait to Get to Heaven", "Fannie Flagg", 17.13, 50);
BookStockItem b3 = new BookStockItem( "B03", "4th of July", "James Patterson", 9.99, 50);
BookStockItem b4 = new BookStockItem( "B04", "Chill Factor", "Sandra Brown", 16.99, 50);
DVDStockItem d1 = new DVDStockItem( "D01", "Sentinel", "PG-13", 12.00, 50);
DVDStockItem d2 = new DVDStockItem( "D02", "Cars", "G", 15.00, 50);
DVDStockItem d3 = new DVDStockItem( "D03", "X-Men – The Last Stand", "PG-13", 23.00, 50);
DVDStockItem d4 = new DVDStockItem( "D04", "The Constant Gardener", "R", 13.00, 50);
// create an order
Order o1 = new Order("OR0001");
o1.newOrderLine(b1, 1);
o1.newOrderLine(b2, 2);
o1.newOrderLine(b3, 5);
o1.newOrderLine(b4, 2);
o1.newOrderLine(d1, 4);
o1.newOrderLine(d2, 3);
o1.newOrderLine(d3, 2);
o1.newOrderLine(d4, 1);
// store the order – first get client reference
ObjectContainer db = (ObjectContainer)Application["db4oClient"];
if (db.Get(new Order(o1.OrderID)).Count > 0) // check for unique OrderID LabelResult.Text = "Order " + o1.OrderID + " already exists: could not store!";
else
{ db.Set(o1); // store structured object LabelResult.Text = "Data stored " + Order.AssemblyName();
}
db.Commit();


Note that the application takes responsibility for ensuring that duplicate orders are not created by running a query on the OrderID. The database could also do this if you want – db4o does not have primary keys, but there is a callback mechanism which can be used to achieve a similar effect within the database.


So what does the stored data look like? Db4o has a graphical database browser, the Object Manager. This is a bit different from typical database browsers, instead of the usual tables you see an object tree which shows the stored objects and allows you to navigate references within them. The object tree view for the stored Order object, shown in Figure 4, shows that all the objects created in the listing above have been stored (including the .NET collection object which holds the OrderLines), and the database contents appear just as the "live" objects would do when viewed in the debugger in the running application. Note that each object has a unique id associated with it – these are assigned by db4o and are not related to contents of any of the object fields.



Figure 4. The database contents viewed in the Object Manager object tree view


The Object Manager is actually a Java application, but it can access databases containing .NET objects.


Retrieving an Order


findorder.aspx displays a simple form which allows the user to search for an order ID, and displays the details of the matching order as the text of a label, as shown in Figure 5.



Figure 5. Finding and displaying an order


The code behind the button is listed below. Note that the single query retrieves the whole structured object, and the remainder of the code simply traverses the object graph to get the required information to display. Since you are retrieving domain objects directly, it is simple to make use of business logic encapsulated in those objects. The simple example in this case is that the Order object is asked to calculate its own total cost.



protected void Button1_Click(object sender, EventArgs e)
{ ObjectContainer db = (ObjectContainer)Application["db4oClient"]; ObjectSet results = db.Get( new Order(TextBox1.Text)); // Query-by-example StringBuilder htmlStr = new StringBuilder(""); if (results.HasNext()) { Order foundOrder = (Order) results.Next(); IList<OrderLine> orderLines = foundOrder.OrderLines; foreach (OrderLine orderLine in orderLines) { htmlStr.Append(orderLine.Quantity + " x " + orderLine.Item.StockID + ": " + orderLine.Item.Title + ", " + System.String.Format("{0:C}", orderLine.LineCost()) + "<br/>"); } htmlStr.Append("<br/>Total cost is " + System.String.Format("{0:C}", foundOrder.TotalCost())); } else htmlStr.Append("Order not found"); Label1.Text = htmlStr.ToString();
}


Note that it is not always desirable to load a whole object graph. Particularly in cases where the domain model contains circular references, you may want to retrieve a few objects but end up loading thousands, or millions, of objects into memory. db4o has a feature called activation depth that allows you to control how many object references will be followed when loading. db4o’s standard query mechanisms are designed to work with complete objects – unlike SQL you can’t choose which fields you want to load. There is, however, a community project which has implemented an SQL-like query capability which does allow you to select fields. Most other object database products also provide similar capabilities.


Listing Stock Items


The viewstock.asp shows a master/detail view of all the stock items in the database, as shown in Figure 6. It demonstrates the use of a native query, and also some basic data binding of ASP.NET controls to the results of a db4o query.



Figure 6. Master/detail view of stock items


The code behind this page is listed below. The Page_Load() method runs the query, which returns a list of StockItem objects. This query demonstrates the capability of native queries to sort results. Sorting uses a .NET Comparison object which gives the programmer control of the sorting criteria (sorting by the value of the Title property only here). The query itself makes use of the Comparison object. This example doesn’t apply any further filtering – the query expression simply returns true so that all objects of the specified type match. Note that the query is polymorphic -instances of all the subclasses of StockItem are returned. The result of the query can be bound to any data aware control which supports binding to a List.



public partial class viewstock : System.Web.UI.Page
{ private IList<StockItem> results; protected void Page_Load(object sender, EventArgs e) { ObjectContainer db = (ObjectContainer)Application["db4oClient"]; // Set up Comparison for sorted Native Query Comparison<StockItem> stockCmp = new Comparison<StockItem>(delegate(StockItem s1, StockItem s2) { return s2.Title.CompareTo(s1.Title); }); // Native Query using Comparison results = db.Query<StockItem>(delegate(StockItem item) { return true; }, stockCmp); // bind result to grid control GridView1.DataSource = results; GridView1.DataBind(); } protected void GridView1_SelectedIndexChanged(object sender, EventArgs e) { // bind selected list member to detail view control DetailsView1.DataSource = results; DetailsView1.PageIndex = GridView1.SelectedIndex; DetailsView1.DataBind(); }
}


The details view control is bound to the same list of StockItem objects. Selecting a row in the data grid simply changes the page index displayed in the detail view. Note that the detail view can display any subclass of StockItemBookStockItems, as shown in Figure 6, or DVDStockItems as shown in Figure 7. These figures show the different sets of fields which are used to auto-generate the row names in the control, although in a real application you would probably want to specify bound fields to allow greater control of formatting.



Figure 7. Details of different kinds of stock items


Assembly Names and Dynamic Compilation


Native .NET objects are stored with class names in the format namespace.classname, assembly. You can see in Figure 4 that the class names are Order, App_Code.i6wprojf, and so on. By default ASP.NET files are dynamically compiled when a resource is first requested, and the compiled versions are cached for subsequent requests. Assembly names are generated randomly and assigned at compilation – hence the strange assembly name you see here.


If you stop and start the web application, your classes will then have a different assembly name. As a result, the names of domain classes located in the App_Code folder will not, after recompilation, match the names of those stored in the database. This is a serious issue – db4o queries will not be able to find previously stored data. There are, however, a number of ways around this:


  • Precompile the web site so that dynamic compilation is not done (Web Developer Express does not support this option)

  • Precompile all domain classes into a class library (Web Developer Express does not support this option either, but can use class libraries created elsewhere)

  • Use the Web Application Project Model, which was released in May 2006 as an update for Visual Studio 2005 (this is probably the best option, but again Web Developer Express does not support it)

  • Allow dynamic compilation but use db4o’s aliases feature to translate assembly names

Here’s how to do use the last of these options. You simply note the assembly name stored with the initial data, and include code similar to the following in global.asax before you open the client ObjectContainer:



StringBuilder currentAssembly = new StringBuilder("*, ");
currentAssembly.Append(Order.AssemblyName());
Db4o.Configure().AddAlias( new WildcardAlias( "*, App_Code.i6wprojf", currentAssembly.ToString() )
);


This assumes that the Order class has a method to return its assembly name:



public static string AssemblyName()
{ return Assembly.GetExecutingAssembly().GetName().Name;
}


The assembly name coded into the sample corresponds to the objects stored in the database file which you download with the sample. If you delete and recreate the database file you will need to change the assembly name in the alias.


Adding an alias is an example of a db4o configuration. Configuration options are generally not stored in the database – they are set up in the application before the ObjectContainer is opened, and define how the application will interact with the database itself.


Native Queries and LINQ


The Native Queries mechanism in db4o has some similarities with Microsoft’s LINQ and its SQL-based implementation LINQ to SQL, which will feature in the upcoming release of Visual Studio. Native Queries and LINQ offer language-integrated querying, so that querying becomes an integrated feature of the developer’s primary programming language (C# for example). With language integrated querying, query expressions can benefit from compile-time syntax checking and IDE code completion. In contrast, string-based declarative query languages (e.g. SQL and other query languages used by O/R mappers) embedded within applications do not offer these capabilities, and are not type-safe.


The difference between the current implementations lies in what happens behind the scenes. Native Queries (which is a released technology, not a preview, and supports Java as well as .NET) is at the moment only implemented for db4o. Each query is converted to a fully object-oriented db4o query graph. A LINQ query, on the other hand, is converted into an SQL statement which is executed against a relational database via ADO.NET. There is likely to be convergence between these technologies in future. The Native Queries mechanism can be extended to support other kinds of data store, while OODBs including db4o may provide support for LINQ queries in their products.


Further Work


The example here uses a very basic form of data binding to demonstrate that you can use ASP.NET data controls to view data from an object database. For real applications you will probably want to take a more sophisticated approach, for example by using an ObjectDataSource with suitable data access classes, or by using a custom BindingList implementation to support features such as paging, filtering, sorting and editing (see the Db4o.BindingList.NET project for an example).


A further issue is the fact that applications are usually subject to change. As result, the data model may evolve during the lifetime of a project. What happens to data if the data model of a db4o database is changed? The simple answer is that nothing happens – it is still stored in the database. For example, if you added an extra string field Category to the StockItem class in Figure 1 then all that would happen would be that old objects retrieved by an application using the new class definition would have a null value for the Category field, while new objects would be stored with the new field present. Db4o also allows you, with a single method call, to rename a class already stored in a database.


Conclusion


In this article I have shown that there is a significant mismatch between the relational model used by most databases and the object-oriented model used in object oriented applications. This means that if you are using a domain model in your ASP.NET application, then a significant part of your development time is likely to be taken up with mapping from one model to the other. A solution to this is to use an object-oriented database. OODBs, which seemed to have failed to deliver on early promise in the 90s, are attracting a lot of interest again. There are several products on the market which can be used with .NET. Of these, the simplest to get to grips with are native object databases like db4o, which, though targeted mainly at embedded applications, are proving useful in web sites. For an ASP.NET web site with moderate traffic levels, db4o can provide a viable database solution, with significant savings in development time.

Founders at Work

Commenting is closed for this article.