Хабрахабр

STL интерфейс Berkeley DB

Не так давно для одного моего проекта понадобилась встраиваемая база данных, которая бы хранила элементы в виде ключ-значение, обеспечивала поддержку транзакций, и, опционально, шифровала данные. Привет, Хабр. Кроме нужных мне возможностей, эта БД предоставляет STL-совместимый интерфейс, который позволяет работать с базой данных, как с обычным (почти обычным) STL-контейнером. После непродолжительных поисков, я наткнулся на проект Berkeley DB. Собственно про этот интерфейс речь пойдет ниже.

Berkeley DB

Она доступна бесплатно для использования в open source проектах, но для проприетарных есть существенные ограничения. Berkeley DB — это встраиваемая масштабируемая высокопроизводительная БД с открытым исходным кодом. Поддерживаемые возможности:

  • транзакции
  • лог с упреждающей записью для восстановления после отказов
  • шифрование данных алгоритмом AES
  • репликация
  • индексы
  • средства синхронизации для многопоточных приложений
  • политика доступа — один писатель, множество читателей
  • кеширование

А так же многие другие.

Это позволяет исключить трату ресурсов на такие операции, как транзакции, логирование, блокировки, когда они не нужны. При инициализации системы пользователь может указать, какие подсистемы использовать.

Доступен выбор структуры хранения и доступа к данным:

  • Btree — реализация отсортированного сбалансированного дерева
  • Hash — имплементация линейного хеша
  • Heap — для хранения использует heap file, логически разбитый на страницы. Каждая запись идентифицируется страницей и смещением внутри нее. Хранилище организовано таким образом, что удаление записи не требует уплотнения. Это позволяет использовать его при недостатке физического места.
  • Queue — очередь, хранит записи фиксированной длины с логическим номером в качестве ключа. Она спроектирована для быстрой вставки в конец, и поддерживает специальную операцию, которая удаляет и возвращает запись из головы очереди за один вызов.
  • Recno — позволяет сохранить записи как фиксированной, так и переменной длины с логическим номером в качестве ключа. Обеспечивает доступ к элементу по его индексу.

Чтобы избежать неоднозначности, необходимо определить несколько понятий, которые используются при описании работы Berkeley DB.

Аналогом базы данных Berkeley DB в других СУБД может служить таблица. База данных — хранилище данных в виде ключ-значение.

Определяет общие настройки для всех баз данных, такие как размер кеша, пути хранения файлов, использование и конфигурацию подсистем блокировки, транзакций, логирования. Среда баз данных — оболочка для одной или нескольких баз данных.

В типичном случае использования создается и настраивается среда, а в ней одна или несколько баз данных.

STL-интерфейс

Она имеет биндинги к таким языкам, как Perl, Java, PHP и другие. Berkeley DB представляет из себя библиотеку, написанную на С. Для того, чтобы сделать возможным доступ к базе данных аналогично операциям с STL-контейнерами, имеется STL-интерфейс, как надстройка над С++. Интерфейс для С++ представляет из себя оболочку над С кодом с объектами и наследованием. В графическом виде слои интерфейсов выглядят так:

Все классы и функции STL-интерфейса Berkeley DB находятся в пространстве имен dbstl, для сокращения, под dbstl будет подразумеваться так же и STL-интерфейс. Так, STL-интерфейс позволяет получить элемент из базы данных по ключу (для Btree или Hash) или по индексу (для Recno) аналогично контейнерам std::map или std::vector, найти элемент в БД через стандартный алгоритм std::find_if, проитерировать по всей базе через цикл foreach.

Установка

База данных поддерживает большинство Linux-платформ, Windows, Android, Apple iOS и пр.

04 достаточно установить пакеты: Для Ubuntu 18.

  • libdb5.3-stl-dev
  • libdb5.3++-dev

Последнюю версию исходных кодов можно найти по ссылке. Для сборки из исходников под Linux необходимо установить autoconf и libtool.

1. Для примера я скачал архив с версией 18. 1. 32 — db-18. Необходимо распаковать архив и перейти в папку с исходниками: 32.zip.

unzip db-18.1.32.zip
cd db-18.1.32

Далее перемещаемся в директорию build_unix и запускаем сборку и установку:

cd build_unix
../dist/configure --enable-stl --prefix=/home/user/libraries/berkeley-db
make
make install

Добавление в cmake-проект

Для иллюстрации примеров с Berkeley DB используется проект BerkeleyDBSamples.

Структура проекта выглядит следующим образом:

+-- CMakeLists.txt
+-- sample-usage
| +-- CMakeLists.txt
| +-- sample-map-usage.cpp
|
+-- submodules
| +-- cmake
| | +-- FindBerkeleyDB

Исходные файлы с примерами находятся в sample-usage. Корневой CMakeLists.txt описывает общие параметры проекта. sample-usage/CMakeLists.txt выполняет поиск библиотек, определяет сборку примеров.

Он добавлен как подмодуль git в submodules/cmake. Для подключения библиотеки в проект cmake в примерах применяется FindBerkeleyDB. Например, для библиотеки выше, установленной из исходников, необходимо указать флаг cmake -DBerkeleyDB_ROOT_DIR=/home/user/libraries/berkeley-db. При сборке может потребоваться указать BerkeleyDB_ROOT_DIR.

В корневом файле CMakeLists.txt необходимо добавить в CMAKE_MODULE_PATH путь к модулю FindBerkeleyDB:

list(APPEND CMAKE_MODULE_PATH "$/submodules/cmake/FindBerkeleyDB")

После этого в sample-usage/CMakeLists.txt выполняется поиск библиотеки стандартным образом:

find_package(BerkeleyDB REQUIRED)

Далее, добавляем исполняемый файл и линкуем его с библиотекой Oracle::BerkeleyDB:

add_executable(sample-map-usage "sample-map-usage.cpp")
target_link_libraries(sample-map-usage PRIVATE Oracle::BerkeleyDB ${CMAKE_THREAD_LIBS_INIT} stdc++fs)

Практический пример

Это приложение демонстрирует работу с контейнером dbstl::db_map в однопоточной программе. Для демонстрации применения dbstl разберем простой пример из файла sample-map-usage.cpp. В качестве нижележащей структуры БД может использоваться Btree или Hash. Сам контейнер аналогичен std::map и хранит данные в виде пары ключ/значение. Этот тип возвращается, например, для dbstl::db_map<std::string, TestElement>::operator[]. В отличии от std::map, для контейнера dbstl::db_map<std::string, TestElement> фактическим типом значения является dbstl::ElementRef<TestElement>. Одним из таких методов является operator=. Он определяет методы для сохранения объекта типа TestElement в БД.

В примере работа с базой происходит следующим образом:

  • приложение вызывает методы Berkeley DB для доступа к данным
  • эти методы обращаются к кешу для чтения или записи
  • при необходимости идет обращение непосредственно к файлу с данными

Графически этот процесс показан на рисунке:

Некоторые методы контейнеров dbstl могут выбрасывать исключения при возникновении ошибок. Для уменьшения сложности примера, в нем не используется обработка исключений.

Разбор кода

Для работы c Berkeley DB необходимо подключить два заголовочных файла:

#include <db_cxx.h>
#include <dbstl_map.h>

STL-интерфейс располагается в пространстве имен dbstl. Первый — добавляет примитивы интерфейса C++, а второй — определяет классы и функции для работы с БД, как с ассоциативным контейнером, а так же многие служебные методы.

Для хранения используется структура Btree, в качестве ключа выступает std::string, а значением — пользовательская структура TestElement:

struct TestElement{ std::string id; std::string name;
};

Он должен располагаться до первого использования примитивов STL-интерфейса. В функции main инициализируем библиотеку вызовом dbstl::dbstl_startup().

После этого инициализируем и откроем среду баз данных в директории, которая задана переменной ENV_FOLDER:

auto penv = dbstl::open_env(ENV_FOLDER, 0u, DB_INIT_MPOOL | DB_CREATE);

Так же команда регистрирует данный объект в менеджере ресурсов. Флаг DB_INIT_MPOOL отвечает за инициализацию подсистемы кеширования, DB_CREATE — за создание всех необходимых среде файлов. Если уже есть объект среды баз данных и его нужно только зарегистрировать в менеджере ресурсов, то можно воспользоваться функцией dbstl::register_db_env. Он несет ответственность за закрытие всех зарегистрированных объектов (в нем регистрируются так же объекты баз данных, курсоры, транзакции и пр) и очистку динамической памяти.

Подобная операция выполняется и с базой данных:

auto db = dbstl::open_db(penv, "sample-map-usage.db", DB_BTREE, DB_CREATE, 0u);

Для хранения используется дерево (параметр DB_BTREE). Данные на диск будут записаны в файл sample-map-usage.db, который будет создан при отсутствии (благодаря флагу DB_CREATE) в директории ENV_FOLDER.

Для применения пользовательского типа (в нашем случае TestElement) необходимо задать функции для: В Berkeley DB ключи и значения хранятся как массив байт.

  • получения количества байт под хранения объекта;
  • маршалинга объекта в массив байт;
  • демаршалинга.

Он располагает объекты TestElement в памяти, следующим образом: В примере этот функционал выполняют статические методы класса TestMarshaller.

  • в начало буфера копируется значение длины поля id
  • следом побайтово размещается само содержимое поле id
  • после него копируется размер поля name
  • далее помещается само содержимое из поля name

Опишем функции TestMarshaller:

  • TestMarshaller::restore — заполняет данными из буфера объект TestElement
  • TestMarshaller::size — возвращает размер буфера, который необходим для сохранения указанного объекта.
  • TestMarshaller::store — сохраняет объект в буфере.

Для регистрации функций маршалинга/демаршалинга используется dbstl::DbstlElemTraits:

dbstl::DbstlElemTraits<TestElement>::instance()->set_size_function(&TestMarshaller::size);
dbstl::DbstlElemTraits<TestElement>::instance()->set_copy_function(&TestMarshaller::store);
dbstl::DbstlElemTraits<TestElement>::instance()->set_restore_function( &TestMarshaller::restore
);

Инициализируем контейнер:

dbstl::db_map<std::string, TestElement> elementsMap(db, penv);

Вот так выглядит копирование элементов из std::map в созданный контейнер:

std::copy( std::cbegin(inputValues), std::cend(inputValues), std::inserter(elementsMap, elementsMap.begin())
);

А вот таким образом можно распечатать содержимое БД на стандартный вывод:

std::transform( elementsMap.begin(dbstl::ReadModifyWriteOption::no_read_modify_write(), true), elementsMap.end(), std::ostream_iterator<std::string>(std::cout, "\n"), [](const auto data) -> std::string { return data.first + "=> { id: " + data.second.id + ", name: " + data.second.name + "}"; });

dbstl не определяет cbegin метода, вместо этого используется параметр readonly (второй по счету) в методе begin. Немного необычно выглядит вызов метода begin в примере выше: elementsMap.begin(dbstl::ReadModifyWriteOption::no_read_modify_write(), true).
Такая конструкция используется для получения итератора только на чтение. Такой итератор разрешает только операцию чтения, при выполнении записи он выбросит исключение. Так же можно использовать константную ссылку на контейнер, чтобы получить итератор только на чтение.

Во-первых, выполняется всего лишь операция чтения через итератор. Почему в коде выше используется итератор только на чтение? Во-вторых, в документации говорится о том, что он имеет лучшую производительность, по сравнению с обычной версией.

Добавить новую пару ключ/значение, или, если ключ уже существует, обновить значение, так же просто, как и в std::map:

elementsMap["added key 1"] = {"added id 1", "added name 1"};

Как указывалось выше, интрукция elementsMap["added key 1"] возвращает класс-оболочку, у которой переопределен operator=, последующий вызов которого выполняет непосредственое сохранение объекта в базе.

Если необходимо вставить элемент в контейнер:

auto [iter, res] = elementsMap.insert( std::make_pair(std::string("added key 2"), TestElement{"added id 2", "added name 2"})
);

Если вставить объект не удалось, то флаг успешности будет false. Вызов elementsMap.insert возвращает std::pair<итератор, флаг успешности>. В противном случае флаг успешности содержит true, а итератор указывает на вставленный объект.

Еще одним способом найти значение по ключу является использование метода dbstl::db_map::find, аналогичному std::map::find:

auto findIter = elementsMap.find("test key 1");

Для извлечения пары ключ/значение используется оператор разыменования -auto iterPair = *findIter;. Через полученный итератор можно выполнить доступ к ключу — findIter->first, к полям элемента TestElementfindIter->second.id и findIter->second.name.

Причем ранее извлеченные данные, даже если их модифицировали, затираются. При применении к итератору оператора разыменования (*) или доступа к члену класса (->) происходит обращение к БД и извлечение из нее данных. Это означает, что в примере ниже, изменения, выполненные над итератором, будут отброшены, и на консоль выведено значение, хранившееся в базе данных.

findIter->second.id = "skipped id";
findIter->second.name = "skipped name";
std::cout << "Found elem for key " << "test key 1" << ": id: " << findIter->second.id << ", name: " << findIter->second.name << std::endl;

Далее, все изменения произвести над этим враппером, а результат записать в БД вызвав метод враппера _DB_STL_StoreElement: Чтобы этого избежать необходимо получить враппер хранимого объекта из итератора вызовом findIter->second и сохранить его в переменную.

auto ref = findIter->second;
ref.id = "new test id 1";
ref.name = "new test name 1";
ref._DB_STL_StoreElement();

Обновить данные можно еще проще — просто получить враппер инструкцией findIter->second и присвоить ему нужный объект TestElement, как в примере:

if(auto findIter = elementsMap.find("test key 2"); findIter != elementsMap.end()){ findIter->second = {"new test id 2", "new test name 2"};
}

Перед завершением программы необходимо вызвать dbstl::dbstl_exit(); для закрытия и удаления всех зарегистрированных объектов в менеджере ресурсов.

В заключении

Это лишь небольшое введение и здесь не рассмотрены такие возможности, как транзакционность, блокировки, управление ресурсами, обработка исключений и выполнение в многопоточной среде. В этой статье сделан краткий обзор основных возможностей контейнеров dbstl на примере dbstl::db_map в простой однопоточной программе.

Я не ставил своей целью подробно описать методы и их параметры, для этого лучше обратиться к соответствующей документации по C++ интерфейсу и по STL-интерфейсу

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть