Object-oriented databases are 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 (OOSUBD) in real projects today? When to use them, and in which not?
Here are the
benefits of using OOSUBD:
- There is no problem of inconsistency of the data model in the application and database ( impedance mismatch ). All data is stored in the database in the same form as in the application model.
- There is no need 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 the object-oriented database and the code that works with it is now an automated rather than a boring and monotonous process.
Interesting? Then it's worth a try!
')
The article describes everything that is required to get started with
db4o .
Db4o installation
Today
db4o is one of the most popular object-oriented database management systems.
First, download the latest distribution
from the db4o site (there are versions for Java, .NET 2.0, 3.5). At the time of this writing, the latest version is 7.9. The distribution also includes Object Manager Enterprise (OME) - a useful plug-in for IDE (Eclipse, Visual Studio), which allows you to work with the database offline. OME is not included in the last
productive delivery (at the moment - 7.4), therefore version 7.9 is recommended for familiarization with OOSUBD.
The following article will use C # for examples. For Java, examples are similar, with the exception of the LINQ section, where a prerequisite is the use of .NET 3.5.
After installing db4o in the appropriate place, you can find a great tutorial included in the package. It is to him that I recommend to refer after reading this article, if the topic itself seems to you interesting.
I note that all software for working with db4o and the database itself is free for non-commercial use.
DB connection
To experiment with db4o, we create a project of any type 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 step is to obtain an object of type
IObjectContainer . This is the facade to the database: through it, queries are made to the database for sampling, saving, adding and deleting 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 to which the application accesses directly. This 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 opens in an exclusive mode and, therefore, difficulties arise in the implementation of multi-user applications. However, this solution is great for single-user stand-alone applications that have a complex data model and who need to save this data between application launches. An example is a CAD application.
Next way. To support multi-user mode, that is, the possibility of the existence of multiple
IObjectContainer for a single database at the same time, you should use a client-server architecture. In the case where 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 - file binding is always (one file - one database). By the way, such a file is created automatically upon the first request, if it has not been created before.
The second parameter of the
OpenServer function is a port number equal to 0, which means that the server will be accessible only to local clients created using
server.OpenClient () .
The given example is artificial. In a real application, clients are likely to open in separate threads.
And the last option is the expansion of 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 as follows.
- Specifies the actual value of the port that the server will listen on (using TCP / IP) when calling OpenServer .
- Authorization data for access to the database is specified.
- The client is created using Db4oFactory.OpenClient and, thus, this can occur not only in another thread, but also in a completely different application running on a remote machine.
So, we looked at all three ways to connect to the database and learned how to get an object of type
IObjectContainer . Now let's see how to work with data using this object.
Work with data
Let somewhere in our application the class
User is declared with the fields
Login ,
Password and
Age , and
db is an object of type
IObjectContainer (the one that we got in the last section).
Saving an object (INSERT)
User user1 = new User("Vasya", "123456", 25);
db.Store(user1);
* This source code was highlighted with Source Code Highlighter .
It's all! There is no need to pre-set or manually specify which objects we can save in the database, the structure of these objects, or anything else. When you save the first object OOSUBD will do all the work for us.
Data Queries (SELECT)
There are several ways to query data stored in a database.
The use of natural queries (Native Queries, NQ) is a flexible, powerful and convenient method for performing queries on data in OODB.
IList<User> result = db.Query<User>(usr => usr.Age >= 18
&& usr.Login.StartsWith("V"));
* This source code was highlighted with Source Code Highlighter .
Here a query is made to objects of the class
User , 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 user's name begins with an uppercase letter “V”. Instead of lambda expressions, you can delegate delegates or objects of type
Predicate <T> to the
Query function.
Predicate <T> is an interface containing a single
Match function, which accepts a parameter of type
T and returns a
bool .
Query returns the objects for which
Match returns
true .
The concept of OOBD is great to go with the idea of ​​using queries integrated into the language (LINQ).
Rewrite the previous query using LINQ.
IEnumerable <User> 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 query is again strongly typed and easy to refactor.
There are other methods for executing queries 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 sample object prepared in advance - a sample. Result-sample is not strictly typed. It is difficult to imagine situations where this method may be useful.
- SODA. Low-level query language that db4o works with. Requests that use SODA syntax are not type-safe, not strictly typed, they take up a lot of space, but they are as flexible as possible and allow you to hone application performance where it is needed.
Update Objects (UPDATE)
Before updating an object, we will extract it from the database, then change it and save it back.
User usr = db.Query<User>(usr => usr.Login == "Vasya" )[0];
usr.SetPassword( "111111" );
db.Store(usr);
* This source code was highlighted with Source Code Highlighter .
Delete Objects (DELETE)
Deleting objects is similar:
User usr = db.Query<User>(usr => usr.Login == "Vasya" )[0];
db.Delete(usr);
* This source code was highlighted with Source Code Highlighter .
Compound objects
Up to this point, we have considered how to work with fairly simple
User objects that contained only fields of elementary types (
string and
int ). However, objects can be composite and refer to other objects. For example, in the
User class the
friends field can be declared:
public class User
{
// ...
IList<User> friends = new List <User>();
}
* This source code was highlighted with Source Code Highlighter .
All operations with this class are performed the same way as before - the composite field is correctly stored in the database, however there are some special features.
Suppose we are trying to load from the database an object of one specific user (
User ), as was done in the last section. If a user is loaded, his friends should also be loaded, then friends of his friends, and so on. This may result in having to load all
User objects into memory, or even if
User has references to objects of other types, the entire database. Naturally, this effect is undesirable. Therefore, by default, only the sample objects themselves and the objects to which they refer to are loaded up to the 5th nesting level inclusively. For some situations it is a lot, for others it is not enough. There is a way to configure this parameter, called the
activation depth (
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 all at once, and for a particular class. The
Ext () function returns an advanced
IExtObjectContainer object for accessing advanced functions like database configuration settings. This is done for convenience, so as not to clutter up the main
IObjectContainer interface.
In the case when the request has already worked, but there is not enough data, that is, not all the necessary data has been activated (loaded into memory), you can use the
Activate method in relation to a separate stored object:
// – , –
db.Activate(usr, 5);
* This source code was highlighted with Source Code Highlighter .
In many ways, a similar problem occurs when
saving composite objects. By default, only the fields of the object itself are saved, but not the objects to which it refers. That is, the default
update depth is 1. It can be changed 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 happen by default: the objects referenced by the deleted object remain. You can customize the behavior of the DBMS in case of deletion of 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 is opened (
IObjectContainer ), a transaction context is implicitly created. When performing a
Close operation, the current transaction is automatically committed.
For more flexible transaction management, the
IObjectContainer interface has two methods:
- Commit () . Explicit completion of a transaction (commit) with a record of all changes in the database.
- Rollback () . Transaction rollback - changes that have occurred since the opening of the transaction (container) will not be recorded in the database.
The transaction isolation level adopted in db4o is
read committed .
Conclusion
The purpose of this article is to show that there is a very powerful alternative to existing approaches to developing using relational databases. The approach using object databases is very modern in itself - it is a DBMS that does not lag behind the main trends observed in the development of programming languages ​​such as Java and C #.
The article contains enough material to start working with an OOSUBD, creating real-world applications. However, many issues were not raised here, for example, issues related to performance and development of web applications.
In any case, if you don’t start using object DBMS in practice today, you should at least think about whether this is not the best solution for your project?