Introduction to Object Oriented Databases
Object-oriented databases - databases in which information is presented in the form of objects, as in object-oriented programming languages.
To use or not to use object-oriented database management systems (OSBMS) in real projects today? In which cases to use them, and in which not?
Here are the benefits of using an OSBMS:
The article describes everything that is required to start working with the DBMS DBMS .
Today, db4o is one of the most popular object-oriented database management systems.
To get started, download the latest distribution kit from the db4o website (there are versions for Java, .NET 2.0, 3.5). At the time of writing, the latest version is 7.9. The distribution also includes Object Manager Enterprise (OME) - a useful plug-in for the IDE (Eclipse, Visual Studio), which allows you to work with the database autonomously. OME is not included in the last production delivery (currently 7.4), so version 7.9 is recommended for familiarization with the OSBMS.
Later in the article, C # will be used for examples. For Java, the examples are similar, with the exception of the LINQ section, where the use of .NET 3.5 is a prerequisite.
After installing db4o in the appropriate place you can find the excellent tutorial included in the kit. It is to him that I recommend turning after reading this article, if the topic itself seems interesting to you.
I note that all software for working with db4o and the DBMS itself are free for non-commercial use.
To conduct experiments on db4o, create any type of project in our IDE, for example, a console application and add links to db4o assemblies (packages): Db4objects.Db4o.dll and Db4objects.Db4o.Linq.dll (if required).
To perform any actions on the object base in the application, the first thing you need to do is get an object of type IObjectContainer . This is the facade to the database: through it queries to the database are performed to select, save, add and delete data.
The method of obtaining the object depends on the type of connection to the database.
The easiest way - the database is located in a local file, which the application accesses directly. It is done like this:
The database file in this case is opened in exclusive mode and, therefore, difficulties arise when implementing multi-user applications. However, this solution is perfect for single-user stand-alone applications that have a complex data model and that need to store this data between application launches. Example CAD applications.
The next way. To support multi-user mode, that is, the possibility of the existence of several IObjectContainer for the same database at the same time, you should use the client-server architecture. In the case when the client and server work within the same application, this is done as follows:
In this case, when creating the server, you still have to specify the database file. This must be done for all types of connection to the database - linking to the file always remains (one file - one database). By the way, such a file is automatically created upon request, if it was not created before.
The second parameter of the OpenServer function - a port number of 0, means that the server will be available only to local clients created using server.OpenClient () .
The given example is artificial. In a real application, clients are most likely to open in separate threads.
And the last option is to expand the previous one for the case of remote clients.
This option differs from the previous one in the following.
Suppose that somewhere in our application the User class is declared with the Login , Password, and Age fields , and db is an object of type IObjectContainer (the one we got in the last section).
It's all! It is not necessary to set in advance or manually which objects we can save to the database, the structure of these objects, or anything else. When saving the first object, the OSBMS will do all the work for us.
There are several ways to query the data stored in the database.
The use of natural queries (Native Queries, NQ) is a flexible, powerful and convenient method for executing queries on data in the ODB.
Here, a request is made to objects of the User class , and everything that is possible is strictly typed in this example. Objects are filtered in such a way as to satisfy the condition: the user's age is greater than or equal to 18 and the username begins with the capital letter "V". Instead of a lambda expression, you can pass delegates or objects of type Predicate to the Query function. . Predicate - an interface containing a single Match function that takes a parameter of type T and returns a bool . Query will return those objects for which Match returns true .
The concept of the OOBD perfectly rests on the idea of using integrated language queries (LINQ).
Rewrite the previous query using LINQ.
The request is again strongly typed and easy to refactor.
There are other query execution methods besides NQ and LINQ.
Before updating the object, we will extract it from the database, then change it and save it back.
Deleting objects is similar:
Until this moment, we considered how to work with fairly simple User objects that contained only fields of elementary types ( string and int ). However, objects can be compound and refer to other objects. For example, in the User class, the friends field can be declared :
All operations with this class are performed as before - the composite field is correctly stored in the database, but there are some features.
Suppose we are trying to load an object of one specific user ( User ) from the database , as was done in the previous section. If the user himself is loaded, then his friends should be loaded, then his friends' friends, and so on. This may result in having to load all User objects into memory, or even if User hasthere are links to objects of other types, the entire database. Naturally, such an effect is undesirable. Therefore, by default, only the selection objects themselves and the objects to which they refer are loaded up to and including the 5th nesting level. For some situations this is a lot, for others it is not enough. There is a way to configure this option, called the depth of activation ( activation depth ).
Here are examples that establish the depth of activation for both all at once, and for a particular class. The Ext () function returns an extended IExtObjectContainer to access advanced functions such as database configuration settings. This is done for convenience so as not to clog the main IObjectContainer interface .
In the case when the request has already fulfilled, but some data is missing, that is, not all the necessary data has been activated (loaded into memory), you can use the Activate method , as applied to a separate stored object:
A much similar problem arises when saving compound objects. By default, only the fields of the object itself are saved, but not the objects to which it refers. That is, the depth of the update ( the update depth ) defaults to 1. Change it as follows:
In the case of deleting an object, the cascading deletion also does not occur by default: the objects referenced by the deleted object remain. You can configure the behavior of a DBMS in case of deleting objects as follows:
The concept of "depth of removal" is not provided.
Each time a container ( IObjectContainer ) is opened , a transaction context is implicitly created. When the Close operation is performed , the current transaction is committed automatically.
For more flexible transaction management, there are two methods in the IObjectContainer interface :
The purpose of this article is to show that there is a very powerful alternative to existing development approaches using relational DBMSs. The approach using object databases itself is very modern - it is a DBMS that does not lag behind the main trends observed in the development of programming languages such as Java and C #.
There is enough material in the article to start working with the OSBMS, creating real applications. However, many issues were not addressed here, for example, issues related to performance and development of web applications.
In any case, if you don’t even begin to use object-oriented DBMSs in practice today, you should at least think about whether this is the best solution for your project?
To use or not to use object-oriented database management systems (OSBMS) in real projects today? In which cases to use them, and in which not?
Here are the benefits of using an OSBMS:
- There is no problem of inconsistency of the data model in the application and the database ( impedance mismatch ). All data is stored in the database in the same form as in the application model.
- It is not necessary to separately support the data model on the DBMS side.
- All objects at the data source level are strongly typed. No more string column names! Refactoring an object-oriented database and the code that works with it is now automated, rather than a monotonous and boring process.
The article describes everything that is required to start working with the DBMS DBMS .
Db4o installation
Today, db4o is one of the most popular object-oriented database management systems.
To get started, download the latest distribution kit from the db4o website (there are versions for Java, .NET 2.0, 3.5). At the time of writing, the latest version is 7.9. The distribution also includes Object Manager Enterprise (OME) - a useful plug-in for the IDE (Eclipse, Visual Studio), which allows you to work with the database autonomously. OME is not included in the last production delivery (currently 7.4), so version 7.9 is recommended for familiarization with the OSBMS.
Later in the article, C # will be used for examples. For Java, the examples are similar, with the exception of the LINQ section, where the use of .NET 3.5 is a prerequisite.
After installing db4o in the appropriate place you can find the excellent tutorial included in the kit. It is to him that I recommend turning after reading this article, if the topic itself seems interesting to you.
I note that all software for working with db4o and the DBMS itself are free for non-commercial use.
Connection to the database
To conduct experiments on db4o, create any type of project in our IDE, for example, a console application and add links to db4o assemblies (packages): Db4objects.Db4o.dll and Db4objects.Db4o.Linq.dll (if required).
To perform any actions on the object base in the application, the first thing you need to do is get an object of type IObjectContainer . This is the facade to the database: through it queries to the database are performed to select, save, add and delete data.
The method of obtaining the object depends on the type of connection to the database.
The easiest way - the database is located in a local file, which the application accesses directly. It is done like this:
// получаем доступ к файлу БД
IObjectContainer db = Db4oFactory.OpenFile(filename);
try
{
// работаем с ООБД
}
finally
{
// закрываем файл, освобождаем ресурсы
db.Close();
}
* This source code was highlighted with Source Code Highlighter.
The database file in this case is opened in exclusive mode and, therefore, difficulties arise when implementing multi-user applications. However, this solution is perfect for single-user stand-alone applications that have a complex data model and that need to store this data between application launches. Example CAD applications.
The next way. To support multi-user mode, that is, the possibility of the existence of several IObjectContainer for the same database at the same time, you should use the client-server architecture. In the case when the client and server work within the same application, this is done as follows:
// создаем сервер
IObjectServer server = Db4oFactory.OpenServer(filename, 0);
try
{
// подключаем клиентов
IObjectContainer client = server.OpenClient();
IObjectContainer client2 = server.OpenClient();
// работаем с ООБД через экземпляры IObjectContainer
client.Close();
client2.Close();
}
finally
{
// закрываем файл, освобождаем ресурсы сервера
server.Close();
}
* This source code was highlighted with Source Code Highlighter.
In this case, when creating the server, you still have to specify the database file. This must be done for all types of connection to the database - linking to the file always remains (one file - one database). By the way, such a file is automatically created upon request, if it was not created before.
The second parameter of the OpenServer function - a port number of 0, means that the server will be available only to local clients created using server.OpenClient () .
The given example is artificial. In a real application, clients are most likely to open in separate threads.
And the last option is to expand the previous one for the case of remote clients.
// создаем сервер
IObjectServer server = Db4oFactory.OpenServer(filename, serverPort);
server.GrantAccess(serverUser, serverPassword);
try
{
IObjectContainer client = Db4oFactory.OpenClient("localhost", serverPort,
serverUser, serverPassword);
// работаем с ООБД
client.Close();
}
finally
{
server.Close();
}
* This source code was highlighted with Source Code Highlighter.
This option differs from the previous one in the following.
- Specifies the actual value of the port that the server will listen on (using TCP / IP) when calling OpenServer .
- The authorization data for access to the database is indicated.
- The client is created using Db4oFactory.OpenClient and, thus, this can happen not only in another thread, but also in a completely different application running on the remote machine.
Work with data
Suppose that somewhere in our application the User class is declared with the Login , Password, and Age fields , and db is an object of type IObjectContainer (the one we got in the last section).
Saving Object (INSERT)
User user1 = new User("Vasya", "123456", 25);
db.Store(user1);
* This source code was highlighted with Source Code Highlighter.
It's all! It is not necessary to set in advance or manually which objects we can save to the database, the structure of these objects, or anything else. When saving the first object, the OSBMS will do all the work for us.
Data Queries (SELECT)
There are several ways to query the data stored in the database.
The use of natural queries (Native Queries, NQ) is a flexible, powerful and convenient method for executing queries on data in the ODB.
IList result = db.Query(usr => usr.Age >= 18
&& usr.Login.StartsWith("V"));
* This source code was highlighted with Source Code Highlighter.
Here, a request is made to objects of the User class , and everything that is possible is strictly typed in this example. Objects are filtered in such a way as to satisfy the condition: the user's age is greater than or equal to 18 and the username begins with the capital letter "V". Instead of a lambda expression, you can pass delegates or objects of type Predicate to the Query function.
The concept of the OOBD perfectly rests on the idea of using integrated language queries (LINQ).
Rewrite the previous query using LINQ.
IEnumerable result = from User usr in db
where usr.Age >= 18 && usr.Login.StartsWith("V")
select usr;
* This source code was highlighted with Source Code Highlighter.
The request is again strongly typed and easy to refactor.
There are other query execution methods besides NQ and LINQ.
- Queries by sample (query by example). The easiest, but not powerful enough way. Data sampling is carried out on the basis of comparison with a pre-prepared instance of the object - a sample. The sample result is not strongly typed. It is difficult to imagine situations where this method may be useful.
- SODA. The low-level query language that db4o works with. Requests that use the SODA syntax are not type-safe, are not strongly typed, take up a lot of space, but are as flexible as possible and allow you to hone application performance where it is needed.
Updating Objects (UPDATE)
Before updating the object, we will extract it from the database, then change it and save it back.
User usr = db.Query(usr => usr.Login == "Vasya")[0];
usr.SetPassword("111111");
db.Store(usr);
* This source code was highlighted with Source Code Highlighter.
Deleting Objects (DELETE)
Deleting objects is similar:
User usr = db.Query(usr => usr.Login == "Vasya")[0];
db.Delete(usr);
* This source code was highlighted with Source Code Highlighter.
Compound objects
Until this moment, we considered how to work with fairly simple User objects that contained only fields of elementary types ( string and int ). However, objects can be compound and refer to other objects. For example, in the User class, the friends field can be declared :
public class User
{
// ...
IList friends = new List();
}
* This source code was highlighted with Source Code Highlighter.
All operations with this class are performed as before - the composite field is correctly stored in the database, but there are some features.
Suppose we are trying to load an object of one specific user ( User ) from the database , as was done in the previous section. If the user himself is loaded, then his friends should be loaded, then his friends' friends, and so on. This may result in having to load all User objects into memory, or even if User hasthere are links to objects of other types, the entire database. Naturally, such an effect is undesirable. Therefore, by default, only the selection objects themselves and the objects to which they refer are loaded up to and including the 5th nesting level. For some situations this is a lot, for others it is not enough. There is a way to configure this option, called the depth of activation ( activation depth ).
// глубина активации глобально для всех классов
db.Ext().Configure().ActivationDepth(2);
// глубина активации для класса User
db.Ext().Configure().ObjectClass(typeof(User)).MinimumActivationDepth(3);
db.Ext().Configure().ObjectClass(typeof(User)).MaximumActivationDepth(4);
// каскадная активация для объектов User (нет ограничения на глубину)
db.Ext().Configure().ObjectClass(typeof(User)).CascadeOnActivate(true);
* This source code was highlighted with Source Code Highlighter.
Here are examples that establish the depth of activation for both all at once, and for a particular class. The Ext () function returns an extended IExtObjectContainer to access advanced functions such as database configuration settings. This is done for convenience so as not to clog the main IObjectContainer interface .
In the case when the request has already fulfilled, but some data is missing, that is, not all the necessary data has been activated (loaded into memory), you can use the Activate method , as applied to a separate stored object:
// первый параметр – активируемый объект, второй – глубина активации
db.Activate(usr, 5);
* This source code was highlighted with Source Code Highlighter.
A much similar problem arises when saving compound objects. By default, only the fields of the object itself are saved, but not the objects to which it refers. That is, the depth of the update ( the update depth ) defaults to 1. Change it as follows:
// глубина обновления глобально для всех классов
db.Ext().Configure().UpdateDepth(2);
// глубина обновления для класса User
db.Ext().Configure().ObjectClass(typeof(User)).UpdateDepth(3);
// каскадное обновление для объектов User (нет ограничений на вложенность)
db.Ext().Configure().ObjectClass(typeof(User)).CascadeOnUpdate(true);
* This source code was highlighted with Source Code Highlighter.
In the case of deleting an object, the cascading deletion also does not occur by default: the objects referenced by the deleted object remain. You can configure the behavior of a DBMS in case of deleting objects as follows:
// каскадное удаление (нет ограничений на вложенность)
db.Ext().Configure().ObjectClass(typeof(User)).CascadeOnDelete(true);
* This source code was highlighted with Source Code Highlighter.
The concept of "depth of removal" is not provided.
Transactions
Each time a container ( IObjectContainer ) is opened , a transaction context is implicitly created. When the Close operation is performed , the current transaction is committed automatically.
For more flexible transaction management, there are two methods in the IObjectContainer interface :
- Commit () . Explicit completion of the transaction (commit) with a record of all changes in the database.
- Rollback () . Transaction rollback - changes that have occurred since the transaction (container) was opened will not be committed to the database.
Conclusion
The purpose of this article is to show that there is a very powerful alternative to existing development approaches using relational DBMSs. The approach using object databases itself is very modern - it is a DBMS that does not lag behind the main trends observed in the development of programming languages such as Java and C #.
There is enough material in the article to start working with the OSBMS, creating real applications. However, many issues were not addressed here, for example, issues related to performance and development of web applications.
In any case, if you don’t even begin to use object-oriented DBMSs in practice today, you should at least think about whether this is the best solution for your project?