Hola Habr No hace mucho tiempo, para uno de mis proyectos, necesitaba una base de datos integrada que almacenara elementos clave-valor, proporcionara soporte de transacciones y, opcionalmente, datos cifrados. Después de una breve búsqueda, me encontré con un proyecto de Berkeley DB . Además de las características que necesito, esta base de datos proporciona una interfaz compatible con STL que le permite trabajar con la base de datos como con un contenedor STL normal (casi normal). En realidad, esta interfaz se discutirá a continuación.
Berkeley db
Berkeley DB es una base de datos de código abierto incrustada, escalable y de alto rendimiento. Está disponible de forma gratuita para su uso en proyectos de código abierto, pero para los propietarios existen limitaciones significativas. Características soportadas:
- transacciones
- registro de falla para la conmutación por error
- Cifrado de datos AES
- replicación
- índices
- herramientas de sincronización para aplicaciones multiproceso
- política de acceso: un escritor, muchos lectores
- almacenamiento en caché
Así como muchos otros.
Cuando se inicializa el sistema, el usuario puede especificar qué subsistemas usar. Esto elimina el desperdicio de recursos en operaciones como transacciones, registros, bloqueos cuando no son necesarios.
La opción de estructura de almacenamiento y acceso a datos está disponible:
- Btree - implementación de árbol balanceado ordenado
- Hash : implementación de hash lineal
- Montón : utiliza un archivo de montón lógicamente paginado para almacenamiento. Cada entrada se identifica mediante una página y un desplazamiento dentro de ella. El almacenamiento está organizado de tal manera que eliminar un registro no requiere compactación. Esto le permite usarlo con falta de espacio físico.
- Cola : una cola que almacena registros de una longitud fija con un número lógico como clave. Está diseñado para una inserción rápida al final y es compatible con una operación especial que elimina y devuelve una entrada del encabezado de la cola en una sola llamada.
- Recno : le permite guardar registros de longitudes fijas y variables con un número lógico como clave. Proporciona acceso a un elemento por su índice.
Para evitar la ambigüedad, es necesario definir varios conceptos que se utilizan para describir el trabajo de Berkeley DB .
La base de datos es un almacenamiento de datos de valor clave. Un análogo de la base de datos de Berkeley DB en otros DBMS puede ser una tabla.
Un entorno de base de datos es un contenedor para una o más bases de datos . Define la configuración general para todas las bases de datos , como el tamaño de la memoria caché, las rutas de almacenamiento de archivos, el uso y la configuración de los subsistemas de bloqueo, transacción y registro.
En un caso de uso típico, se crea y configura un entorno y tiene una o más bases de datos .
Interfaz STL
Berkeley DB es una biblioteca escrita en C. Tiene carpetas para lenguajes como Perl , Java , PHP y otros. La interfaz para C ++ es un contenedor sobre código C con objetos y herencia. Para poder acceder a la base de datos de manera similar a las operaciones con contenedores STL , hay una interfaz STL como complemento sobre C ++ . En forma gráfica, las capas de interfaz se ven así:

Entonces, la interfaz STL le permite recuperar un elemento de la base de datos por clave (para Btree o Hash ) o por índice (para Recno ) de manera similar a los contenedores std::map
o std::vector
, encuentre un elemento en la base de datos a través del std::find_if
estándar std::find_if
, iterar sobre toda la base de datos a través del foreach
. Todas las clases y funciones de la interfaz Berkeley DB STL están en el espacio de nombres dbstl , para abreviar, dbstl también significará la interfaz STL .
Instalación
La base de datos es compatible con la mayoría de las plataformas Linux , Windows , Android , Apple iOS , etc.
Para Ubuntu 18.04, solo instale los paquetes:
- libdb5.3-stl-dev
- libdb5.3 ++ - dev
Para construir desde fuentes Linux , necesita instalar autoconf y libtool . El último código fuente se puede encontrar aquí .
Por ejemplo, descargué el archivo con la versión 18.1.32 - db-18.1.32.zip. Necesita descomprimir el archivo e ir a la carpeta de origen:
unzip db-18.1.32.zip cd db-18.1.32
A continuación, nos movemos al directorio build_unix y ejecutamos el ensamblaje y la instalación:
cd build_unix ../dist/configure --enable-stl --prefix=/home/user/libraries/berkeley-db make make install
Agregando al proyecto cmake
El proyecto BerkeleyDBSamples se utiliza para ilustrar ejemplos con Berkeley DB .
La estructura del proyecto es la siguiente:
+-- CMakeLists.txt +-- sample-usage | +-- CMakeLists.txt | +-- sample-map-usage.cpp | +-- submodules | +-- cmake | | +-- FindBerkeleyDB
La raíz CMakeLists.txt describe los parámetros generales del proyecto. Los archivos fuente de muestra están en uso de muestra . sample-use / CMakeLists.txt busca bibliotecas, define el conjunto de ejemplos.
En ejemplos, FindBerkeleyDB se utiliza para conectar la biblioteca al proyecto cmake . Se agrega como un submódulo git en submodules / cmake . Durante el ensamblaje, es posible que deba especificar BerkeleyDB_ROOT_DIR
. Por ejemplo, para la biblioteca anterior instalada desde las fuentes, debe especificar el indicador cmake -DBerkeleyDB_ROOT_DIR=/home/user/libraries/berkeley-db
.
En el archivo raíz CMakeLists.txt , agregue la ruta al módulo FindBerkeleyDB a CMAKE_MODULE_PATH :
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/submodules/cmake/FindBerkeleyDB")
Después de eso, sample-use / CMakeLists.txt realiza una búsqueda en la biblioteca de la manera estándar:
find_package(BerkeleyDB REQUIRED)
A continuación, agregue el archivo ejecutable y vincúlelo a la biblioteca 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)
Ejemplo práctico
Para demostrar el uso de dbstl, examinemos un ejemplo simple del archivo sample-map-use.cpp . Esta aplicación demuestra trabajar con el dbstl::db_map
en un programa de subproceso único. El contenedor en sí es similar a std::map
y almacena datos como un par clave / valor. La estructura de la base de datos subyacente puede ser Btree o Hash . A diferencia de std::map
, para el dbstl::db_map<std::string, TestElement>
tipo de valor real es dbstl::ElementRef<TestElement>
. Este tipo se devuelve, por ejemplo, para dbstl::db_map<std::string, TestElement>::operator[]
. Define métodos para almacenar un objeto de tipo TestElement
en la base de datos. Uno de estos métodos es operator=
.
En el ejemplo, el trabajo con la base de datos es el siguiente:
- la aplicación llama a los métodos de Berkeley DB para acceder a los datos
- estos métodos acceden al caché para leer o escribir
- si es necesario, el acceso es directamente al archivo de datos
Gráficamente, este proceso se muestra en la figura:

Para reducir la complejidad del ejemplo, no utiliza el manejo de excepciones. Algunos métodos de contenedor dbstl pueden generar excepciones cuando se producen errores.
Análisis de código
Para trabajar con Berkeley DB, debe conectar dos archivos de encabezado:
#include <db_cxx.h> #include <dbstl_map.h>
El primero agrega primitivas de interfaz C ++ , y el segundo define clases y funciones para trabajar con la base de datos, como con un contenedor asociativo, así como muchos métodos de utilidad. La interfaz STL se encuentra en el espacio de nombres dbstl .
Para el almacenamiento, se utiliza la estructura Btree , std::string
actúa como la clave y el valor es la estructura de usuario TestElement
:
struct TestElement{ std::string id; std::string name; };
En la función main
, inicialice la biblioteca llamando a dbstl::dbstl_startup()
. Debe ubicarse antes del primer uso de las primitivas de la interfaz STL .
Después de eso, inicializamos y abrimos el entorno de la base de datos en el directorio que establece la variable ENV_FOLDER
:
auto penv = dbstl::open_env(ENV_FOLDER, 0u, DB_INIT_MPOOL | DB_CREATE);
El indicador DB_INIT_MPOOL
responsable de inicializar el subsistema de almacenamiento en caché, DB_CREATE
, para crear todos los archivos necesarios para el entorno. El equipo también registra este objeto en el administrador de recursos. Es responsable de cerrar todos los objetos registrados (los objetos de la base de datos, cursores, transacciones, etc. también están registrados en él) y borrar la memoria dinámica. Si ya tiene un objeto de entorno de base de datos y solo necesita registrarlo con el administrador de recursos, puede usar la función dbstl::register_db_env
.
Se realiza una operación similar con la base de datos :
auto db = dbstl::open_db(penv, "sample-map-usage.db", DB_BTREE, DB_CREATE, 0u);
Los datos en el disco se escribirán en el archivo sample-map-use.db , que se creará en ausencia (gracias al indicador DB_CREATE
) en el directorio ENV_FOLDER
. Se utiliza un árbol para el almacenamiento (parámetro DB_BTREE
).
En Berkeley DB, las claves y los valores se almacenan como una matriz de bytes. Para usar un tipo personalizado (en nuestro caso TestElement
), debe definir funciones para:
- recibir el número de bytes para almacenar el objeto;
- ordenar un objeto en una matriz de bytes;
- desordenar.
En el ejemplo, esta funcionalidad se realiza mediante los métodos estáticos de la clase TestMarshaller
. TestElement
objetos TestElement
en la memoria de la siguiente manera:
- la longitud del campo
id
se copia al comienzo del búfer - siguiente byte se colocan los contenidos del campo
id
- después, se copia el tamaño del campo de
name
- entonces el contenido en sí se coloca desde el campo de
name

Describimos las funciones de TestMarshaller
:
TestMarshaller::restore
- llena el objeto TestElement
con datos del búferTestMarshaller::size
: devuelve el tamaño del búfer que se necesita para guardar el objeto especificado.TestMarshaller::store
- guarda el objeto en el búfer.
Para registrar funciones dbstl::DbstlElemTraits
/ dbstl::DbstlElemTraits
, use 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 );
Inicializar el contenedor:
dbstl::db_map<std::string, TestElement> elementsMap(db, penv);
Así es como se ve copiar elementos de std::map
al contenedor creado:
std::copy( std::cbegin(inputValues), std::cend(inputValues), std::inserter(elementsMap, elementsMap.begin()) );
Pero de esta manera puede imprimir el contenido de la base de datos a la salida estándar:
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 + "}"; });
Llamar al método begin
en el ejemplo anterior parece un poco inusual: elementsMap.begin(dbstl::ReadModifyWriteOption::no_read_modify_write(), true)
.
Este diseño se utiliza para obtener un iterador de solo lectura . dbstl no define el método cbegin
; en su lugar, se utiliza el parámetro de readonly
(el segundo) en el método de begin
. También puede usar una referencia constante al contenedor para obtener un iterador de solo lectura . Tal iterador solo permite una operación de lectura; al escribir, arrojará una excepción.
¿Por qué se usa el iterador de solo lectura en el código anterior? En primer lugar, solo realiza una operación de lectura a través de un iterador. En segundo lugar, la documentación dice que tiene un mejor rendimiento en comparación con la versión normal.
Agregar un nuevo par clave / valor o, si la clave ya existe, actualizar el valor es tan simple como en std::map
:
elementsMap["added key 1"] = {"added id 1", "added name 1"};
Como se mencionó anteriormente, la instrucción elementsMap["added key 1"]
devuelve una clase contenedora con operator=
redefined, cuya llamada posterior almacena directamente el objeto en la base de datos.
Si necesita insertar un artículo en un contenedor:
auto [iter, res] = elementsMap.insert( std::make_pair(std::string("added key 2"), TestElement{"added id 2", "added name 2"}) );
La llamada a elementsMap.insert
devuelve std::pair<, >
. Si el objeto no se puede insertar, el indicador de éxito será falso . De lo contrario, el indicador de éxito contiene verdadero , y el iterador apunta al objeto insertado.
Otra forma de encontrar el valor por clave es usar el dbstl::db_map::find
, similar a std::map::find
:
auto findIter = elementsMap.find("test key 1");
A través del iterador obtenido, puede acceder a la clave - findIter->first
, a los campos del elemento findIter->second.id
- findIter->second.id
y findIter->second.name
. Para extraer un par clave / valor , se utiliza el operador de referencia: auto iterPair = *findIter;
.
Cuando el operador de desreferenciación ( * ) o el acceso a un miembro de la clase ( -> ) se aplica al iterador, se accede a la base de datos y se extraen los datos de ella. Además, los datos extraídos previamente, incluso si fueron modificados, se borran. Esto significa que en el siguiente ejemplo, los cambios realizados en el iterador se descartarán y el valor almacenado en la base de datos se mostrará en la consola.
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;
Para evitar esto, debe obtener el contenedor del objeto almacenado del iterador llamando a findIter->second
y guardarlo en una variable. A continuación, realice todos los cambios sobre este contenedor y escriba el resultado en la base de datos llamando al método de contenedor _DB_STL_StoreElement
:
auto ref = findIter->second; ref.id = "new test id 1"; ref.name = "new test name 1"; ref._DB_STL_StoreElement();
Actualizar los datos puede ser aún más fácil: solo obtenga el contenedor con la instrucción findIter->second
y asígnele el objeto TestElement
deseado, como en el ejemplo:
if(auto findIter = elementsMap.find("test key 2"); findIter != elementsMap.end()){ findIter->second = {"new test id 2", "new test name 2"}; }
Antes de finalizar el programa, debe llamar a dbstl::dbstl_exit();
para cerrar y eliminar todos los objetos registrados en el administrador de recursos.
En conclusión
Este artículo proporciona una breve descripción de las características principales de los contenedores dbstl que usan dbstl::db_map
como dbstl::db_map
en un programa simple de subproceso único. Esta es solo una pequeña introducción y no ha cubierto características como la transaccionalidad, el bloqueo, la administración de recursos, el manejo de excepciones y la ejecución multiproceso.
No pretendía describir en detalle los métodos y sus parámetros, para esto es mejor consultar la documentación correspondiente en la interfaz C ++ y en la interfaz STL