Как многие разработчики в стеке .Net, я периодически поглядываю в сторону объектно-ориентированных баз данных как возможный способ спасения от бессмыслицы ORM-фреймворков и SQL. И вот недавно решил попробовать и оценить конкретный ОО-продукт под названием db4o. В этой статье я хочу рассказать про то, что такое OODBMS, как выглядит db4o и что с ним можно сделать.
Что не так с ORM-ами и SQL?
Если честно – ничего. Все же пользуются, не так ли? В сети есть гора блогов которые фетишизируют такие фреймворки как NHibernate, Entity Framework или LLBLGen, поэтому очевидно многих это устраивает, точно так же как их устраивает тот факт что типичный разработчик ПО должен быть еще и отчасти DBA – знать SQL, отличие inner join от outer join и еще кучу всяких глуинтересных вещей. Вообще я не против всего этого при условии что для этого есть специально обученный люди, которые знают SQL и прочие прелести, и готовы сотрудничасть с мной над совместной реализацией чего-то SQL-зависимого. Лично я дальше использования провайдера Linq на каком-нибудь ORMе (для меня это OpenAccess) двигаться не хочу.
В связи с этим, базы данных которые позволяют использовать простые объекты очень соблазнительны. Одна из таких баз данных – а точнее движковбаз данных – это db4o (db4objects). Этот движок позволяет создавать маленькие базы данных прямо в файлах, не требует установки, а сам движок по размеру всего 700к что удобно – хотя это не такое уж редкое качество (у той же VistaDB движок тоже меньге мегабайта).
Немного о db4o
Итак, db4o – возможная альтернатива тому же Sql Server CE, особенно если вы пытаетесь использовать SQL CE вместе с Entity Framework (не советую). Я использую SQL CE с OpenAccess так что мне по крайней мере попался более отточенный ORM с нормальной POCO поддержкой, но я все равно не прочь бы использовать объекты напрямую, хотя бы потому что метод работы когда не надо переключаться на SQL Server Management Studio должен намного быстрее работать.
db4o обладает двойной лицензией – GPL и коммерческой. На практике это означает что вы можете скачать полнофункциональную версию движка и разобраться подходит вам это или нет. Скачав установочный пакет размером всего 27Мб, вы заметите при установке возможность установки помимо движка add-in’ов для Студии. Для моих экспериментов этот add-in не потребовался, но учтите: в визарде он не устанавливается, его потом нужно будет запускать отдельно.
CRUD, но с объектами
Самое простое, что можно сделать – так это просто попробовать API в действии. На самом деле, API db4o настолько примитивен, что по сути дела и рассказывать-то не о чем. Но раз я уж решил рассказать, давайте начнем. Для начала я создам некую сущность, которую нам предстоит записать в базу:
01.
public
class
Person
02.
{
03.
public
string
Name {
get
;
set
; }
04.
public
int
Age {
get
;
set
; }
05.
public
override
string
ToString()
06.
{
07.
return
string
.Format(
"Name: {0}, Age: {1}"
, Name, Age);
08.
}
09.
}
После этого, мы добавляем ссылки на сборки db4o – потребуется как минимум на `Db4objects.Db4o.dll`, но также советую сослаться на `Db4objects.Db4o.Linq.dll` чтобы получить поддержку Linq – если вы конечно не фетишист который любит такие “модерновые” способы формирования запроса как QBE (фанатам NHibernate это сокращение наверняка покажется знакомым).
Итак, имея объект, мы можем добавить его в базу данных с помощью нехитрых манипуляций.
01.
string
file = @
"c:\temp\test.db4o"
;
02.
var db = Db4oEmbedded.OpenFile(file);
03.
try
04.
{
05.
Person p =
new
Person();
06.
p.Name =
"Gregory House"
;
07.
p.Age = 50;
08.
db.Store(p);
09.
}
10.
finally
11.
{
12.
db.Close();
13.
}
Вызвав `Db4oEmbedded.OpenFile()`, мы либо открыли либо создали новую базу данных, и получили ссылку на `IObjectContainer`. Этот интерфейс (а также его собрат `IExtObjectContainer`, который содержит более “продвинутый” функционал) мы будем использовать для всех операций с нашей базой данных.
В примере выше мы создали сущность всем знакомого доктора и поместили ее в файл базы данных. Если вы сейчас откроете этот файл, то увидите там описание и структуры и собственно записей – конечно данные не слишком читабельны, но все равно, понятно что ваш объект был туда “сериализован”.
Метод `Store()` используется для того чтобы сохранить или обновить запись. Для удаления можно использовать `db.Delete()`. Что касается выборки то, как я уже упомянул, db4o страдает тем же расслоением личности что и NHibernate. Слава богу что и тут есть Linq провайдер, потому что иначе я бы и не взглянул на этот движок. Код для получения всех объектов типа `Person` из базы выглядит предсказуемо:
1.
var people = from Person p
in
db select p;
2.
foreach
(var person
in
people)
3.
Console.WriteLine(person);
Сложные объекты
Наверное вы уже догадались что для сохранения объектов с зависимостями нет никаких преград. Давайте перепишем объект `Person` чтобы у него была ссылка на объект `Drug`:
01.
public
class
Drug
02.
{
03.
public
string
Name {
get
;
set
; }
04.
public
bool
IsMindAltering {
get
;
set
; }
05.
public
override
string
ToString()
06.
{
07.
return
string
.Format(
"Name: {0}, IsMindAltering: {1}"
,
08.
Name, IsMindAltering);
09.
}
10.
}
11.
public
class
Person
12.
{
13.
public
string
Name {
get
;
set
; }
14.
public
int
Age {
get
;
set
; }
15.
public
Drug FavouriteDrug {
get
;
set
; }
16.
public
override
string
ToString()
17.
{
18.
return
string
.Format(
"Name: {0}, Age: {1}, FavouriteDrug: {2}"
,
19.
Name, Age, FavouriteDrug);
20.
}
21.
}
Теперь можно делать объект типа `Person` с привязкой к `Drug` и сериализовывать его точно так же как раньше.
1.
Person p =
new
Person();
2.
p.Name =
"Gregory House"
;
3.
p.Age = 50;
4.
p.FavouriteDrug =
new
Drug {Name =
"Vicodin"
, IsMindAltering =
true
};
5.
db.Store(p);
В данном случае примечательно скорее не то, что при перечислении объектов из базы вы получите инициализированную привязку к соответствующим объектам типа `Drug`. Примечательно то, что объекты типа `Drug` теперь существуют в базе независимо, точно так же как если бы это была еще одна таблица с отношением x-to-many. Поэтому можно смело перечислять объекты типа `Drug`:
1.
var drugs = from Drug d
in
db select d;
2.
foreach
(var drug
in
drugs)
3.
Console.WriteLine(drug);
Согласитесь, это удобно, и избавляет от лишней головной боли. Хотя это не значит что вам не нужно будет контролировать каскадное удаление и прочие напасти, который не специфичны только для SQL-driven баз данных.
Производительность
Думаю никого не удивит, что я решил проверить производительность запросов к базе db4o по сравнению с запрсами к привычной мне связке SQL Server CE + Telerik OpenAccess. Я решил проверить скорость следующих операций:
- Создание большого количества элементов в базе
- Считывание всех элементов
- Считывание элементов через фильтр
- Обновление всех элементов
Я решил протестировать обе базы на двух критичных для меня размерах – 10 тысяч и 100 тысяч записей. Эти размеры сравнимы с количеством пространств имен и типов в GAC, что актуально для моих целей. Для начала, я сделал тест на 10к, получив следующие результаты:
Создание | Считывание | Фильтрация | Обновление | |
---|---|---|---|---|
db4o | 2.9127051 | 1.6635088 | 1.4933795 | 3.1420556 |
sql server ce + openaccess | 10.8982461 | 1.0317851 | 0.3462783 | 2.7075653 |
Тесты были проведены на Core 2 Duo 2.4Ghz, 4Gb RAM, на Release-сборке под x86. (надеюсь все угадали почему именно x86?)
Этот тест показывает, что sql server CE медленнее создает записи, но быстрее позволяет с ними работать – особенно это касается фильтрования. В принципе, разница для обновлений и считывания не значительна, но меня беспокоит именно фильтрация – понятно что SQL Server оптимизировал Linq-запрос в `select … where …` в результате чего данные были возвращены так быстро. Естественно, что db4o подобного сделать не может.
Теперь давайте посмотрим на то, какие результаты мы получим если будем работать с сотней тысяч элементов. Вот новый набор результатов:
Создание | Считывание | Фильтрация | Обновление | |
---|---|---|---|---|
db4o | 17.0099462 | 12.6775694 | 8.3748354 | 29.1141992 |
sql server ce + openaccess | 56.0725222 | 4.4244306 | 1.7642032 | 32.7506183 |
Мы снова получили схожие результаты с тем что было раньше – создание объектов намного медленнее на SQL Server, но зато считывание и фильтрация намного быстрее – где-то в 3-4 раза. Это означает что при работе с базой которая обновляется очень редко (а в моем случае, поскольку я индексирую GAC, это именно так) SQL Server CE дает существенный выигрыш в производительности – я готов представить что пользователь может подождать 1.7 секунд для того чтобы получить подсказку, но ждать 8 секунд он вряд ли будет.
Череда полезных фич
Поскольку мы работаем с объектами, у нас в db4o есть возможность использовать некоторые фичи, которые в традиционных RDBMS даются нам очень тяжело. Вот некоторые аспекты, которые приходят на ум:
- Работа с коллекциями. Объект может содержать например массив чисел. И не нужно думать о создании лишней таблицы и нормализации.
- Перечисления. Ведь SQL не поддерживает парадигму перечислений на корню – там можно разве что колонку типа `int` написать, а на стороне программы сделать самописный `enum` и потом конвертировать его в/из `int` когда нужно. Что жутко неудобно.
- Наследование. Без комментариев.
Помимо этого, у db4o есть поддержка транзакций, прозрачной активации (это когда db4o автоматически решает глубину подгрузки объектов) и прочих интересных (и наверняка сложных) фич. Думаю вы не удивитесь узнав что есть даже книжка по db4o от Apress. Не знаю как у вас, а у меня уже появился соблазн купить ее “про запас”, на тот случай если db4o “зацепит”.
Заключение
Если честно, я ожидал что обладая “чистой” объектно-ориентированностью db4o сможет выжать максимум из более “естественного” мэппинга памяти на объекты. К сожалению, я вижу обратное – SQL база, даже нагруженная лишним ORM-слоем, работает намного резвее. Причем разница в производительности не 20-30%, а 300-400! Конечно, я тестировал на весьма дискретных размерах, и в действительности возможно отношения нелинейны, но даже те результаты что я получил уже демонстративны – SQL медленнее создает объекты, но быстрее потом их считывает.
Для того продукта с которым я сейчас работаю, я пожалуй останусь на паре Sql Server CE + OpenAccess, благо эта связка уже налажена и дает те результаты, которые делают индексирование “юзабельным” для конечных пользователей. Но если у меня будут проекты не критичные к производительности (например всякий web scraping), то я не исключаю возможности использования db4o. И если мне придется использовать этот движок ‘in production’, я обязательно напишу об этом.