📜 ⬆️ ⬇️

Introduction to Object Oriented Databases

OOSUBD 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:
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.
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.

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

Source: https://habr.com/ru/post/56399/


All Articles