Salut, Habr. Il n'y a pas si longtemps, pour l'un de mes projets, j'avais besoin d'une base de données intégrée qui stockerait des éléments de valeur-clé, fournirait un support de transaction et, éventuellement, des données chiffrées. Après une courte recherche, je suis tombé sur un projet Berkeley DB . En plus des fonctionnalités dont j'ai besoin, cette base de données fournit une interface compatible STL qui vous permet de travailler avec la base de données comme avec un conteneur STL normal (presque ordinaire). En fait, cette interface sera discutée ci-dessous.
Berkeley db
Berkeley DB est une base de données open source intégrée, évolutive et hautes performances. Il est disponible gratuitement pour une utilisation dans des projets open source , mais pour les projets propriétaires, il existe des limitations importantes. Fonctionnalités prises en charge:
- transactions
- journal de secours pour le basculement
- Cryptage des données AES
- réplication
- indices
- outils de synchronisation pour les applications multithread
- politique d'accès - un écrivain, plusieurs lecteurs
- mise en cache
Et bien d'autres.
Une fois le système initialisé, l'utilisateur peut spécifier les sous-systèmes à utiliser. Cela élimine le gaspillage de ressources sur des opérations telles que les transactions, la journalisation, les verrous lorsqu'ils ne sont pas nécessaires.
Le choix de la structure de stockage et de l'accès aux données est disponible:
- Btree - implémentation d'un arbre équilibré trié
- Hash - implémentation de hachage linéaire
- Tas - utilise un fichier tas logiquement paginé pour le stockage. Chaque entrée est identifiée par une page et un décalage à l'intérieur. Le stockage est organisé de telle manière que la suppression d'un enregistrement ne nécessite pas de compactage. Cela vous permet de l'utiliser avec un manque d'espace physique.
- File d'attente - une file d'attente qui stocke des enregistrements d'une longueur fixe avec un numéro logique comme clé. Il est conçu pour une insertion rapide à la fin et prend en charge une opération spéciale qui supprime et renvoie une entrée de la tête de la file d'attente en un seul appel.
- Recno - vous permet de sauvegarder des enregistrements de longueurs fixes et variables avec un numéro logique comme clé. Fournit l'accès à un élément par son index.
Pour éviter toute ambiguïté, il est nécessaire de définir plusieurs concepts utilisés pour décrire le travail de Berkeley DB .
La base de données est un stockage de données clé-valeur. Un analogue de la base de données Berkeley DB dans d'autres SGBD peut être un tableau.
Un environnement de base de données est un wrapper pour une ou plusieurs bases de données . Définit les paramètres généraux de toutes les bases de données , tels que la taille du cache, les chemins de stockage des fichiers, l'utilisation et la configuration des sous-systèmes de blocage, de transaction et de journalisation.
Dans un cas d'utilisation typique, un environnement est créé et configuré, et il possède une ou plusieurs bases de données .
Interface STL
Berkeley DB est une bibliothèque écrite en C. Il a des classeurs pour des langages tels que Perl , Java , PHP et autres. L'interface pour C ++ est un wrapper sur du code C avec des objets et l'héritage. Afin de permettre d'accéder à la base de données de manière similaire aux opérations avec des conteneurs STL , il existe une interface STL en tant que module complémentaire sur C ++ . Sous une forme graphique, les couches d'interface ressemblent à ceci:

Ainsi, l'interface STL vous permet d'obtenir un élément de la base de données par clé (pour Btree ou Hash ) ou par index (pour Recno ) de manière similaire aux std::map
ou std::vector
, recherchez un élément dans la base de données via l' std::find_if
, parcourir toute la base de données via la foreach
. Toutes les classes et fonctions de l'interface Berkeley DB STL se trouvent dans l' espace de noms dbstl , pour faire court, dbstl signifiera également l'interface STL .
L'installation
La base de données prend en charge la plupart des plates - formes Linux , Windows , Android , Apple iOS , etc.
Pour Ubuntu 18.04, installez simplement les packages:
- libdb5.3-stl-dev
- libdb5.3 ++ - dev
Pour construire à partir de sources Linux , vous devez installer autoconf et libtool . Le dernier code source peut être trouvé ici .
Par exemple, j'ai téléchargé l'archive avec la version 18.1.32 - db-18.1.32.zip. Vous devez décompresser l'archive et aller dans le dossier source:
unzip db-18.1.32.zip cd db-18.1.32
Ensuite, nous passons au répertoire build_unix et exécutons l'assemblage et l'installation:
cd build_unix ../dist/configure --enable-stl --prefix=/home/user/libraries/berkeley-db make make install
Ajout au projet cmake
Le projet BerkeleyDBSamples est utilisé pour illustrer des exemples avec Berkeley DB .
La structure du projet est la suivante:
+-- CMakeLists.txt +-- sample-usage | +-- CMakeLists.txt | +-- sample-map-usage.cpp | +-- submodules | +-- cmake | | +-- FindBerkeleyDB
La racine CMakeLists.txt décrit les paramètres généraux du projet. Des exemples de fichiers source sont utilisés en exemple . sample-usage / CMakeLists.txt recherche des bibliothèques, définit l'assemblage d'exemples.
Dans les exemples, FindBerkeleyDB est utilisé pour connecter la bibliothèque au projet cmake . Il est ajouté sous forme de sous-module git dans les sous-modules / cmake . Lors de l'assemblage, vous devrez peut-être spécifier BerkeleyDB_ROOT_DIR
. Par exemple, pour la bibliothèque ci-dessus installée à partir des sources, vous devez spécifier l'indicateur cmake -DBerkeleyDB_ROOT_DIR=/home/user/libraries/berkeley-db
.
Dans le fichier racine CMakeLists.txt , ajoutez le chemin d'accès au module FindBerkeleyDB à CMAKE_MODULE_PATH :
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/submodules/cmake/FindBerkeleyDB")
Après cela, sample-usage / CMakeLists.txt effectue une recherche de bibliothèque de la manière standard:
find_package(BerkeleyDB REQUIRED)
Ensuite, ajoutez le fichier exécutable et liez-le à la bibliothèque 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)
Exemple pratique
Pour démontrer l'utilisation de dbstl, examinons un exemple simple du fichier sample-map-usage.cpp . Cette application montre dbstl::db_map
travailler avec le dbstl::db_map
dans un programme à thread unique. Le conteneur lui-même est similaire à std::map
et stocke les données sous forme de paire clé / valeur. La structure de base de données sous-jacente peut être Btree ou Hash . Contrairement à std::map
, pour le dbstl::db_map<std::string, TestElement>
type de valeur réel est dbstl::ElementRef<TestElement>
. Ce type est retourné, par exemple, pour dbstl::db_map<std::string, TestElement>::operator[]
. Il définit les méthodes de stockage d'un objet de type TestElement
dans la base de données. Une telle méthode est operator=
.
Dans l'exemple, le travail avec la base de données est le suivant:
- application appelle les méthodes Berkeley DB pour accéder aux données
- ces méthodes accèdent au cache pour la lecture ou l'écriture
- si nécessaire, l'accès se fait directement au fichier de données
Graphiquement, ce processus est illustré dans la figure:

Pour réduire la complexité de l'exemple, il n'utilise pas la gestion des exceptions. Certaines méthodes de conteneur dbstl peuvent lever des exceptions lorsque des erreurs se produisent.
Analyse de code
Pour travailler avec Berkeley DB, vous devez connecter deux fichiers d'en-tête:
#include <db_cxx.h> #include <dbstl_map.h>
La première ajoute des primitives d'interface C ++ et la seconde définit des classes et des fonctions pour travailler avec la base de données, comme avec un conteneur associatif, ainsi que de nombreuses méthodes utilitaires. L' interface STL est située dans l'espace de noms dbstl .
Pour le stockage, la structure Btree est utilisée , std::string
agit comme la clé et la valeur est la structure utilisateur TestElement
:
struct TestElement{ std::string id; std::string name; };
Dans la fonction main
, initialisez la bibliothèque en appelant dbstl::dbstl_startup()
. Il doit être localisé avant la première utilisation des primitives de l'interface STL .
Après cela, nous initialisons et ouvrons l'environnement de base de données dans le répertoire défini par la variable ENV_FOLDER
:
auto penv = dbstl::open_env(ENV_FOLDER, 0u, DB_INIT_MPOOL | DB_CREATE);
L'indicateur DB_INIT_MPOOL
responsable de l'initialisation du sous-système de mise en cache, DB_CREATE
- pour créer tous les fichiers nécessaires à l'environnement. L'équipe enregistre également cet objet dans le gestionnaire de ressources. Il est responsable de la fermeture de tous les objets enregistrés (les objets de base de données, les curseurs, les transactions, etc. y sont également enregistrés) et de vider la mémoire dynamique. Si vous avez déjà un objet d' environnement de base de données et que vous avez seulement besoin de l'enregistrer auprès du gestionnaire de ressources, vous pouvez utiliser la fonction dbstl::register_db_env
.
Une opération similaire est effectuée avec la base de données :
auto db = dbstl::open_db(penv, "sample-map-usage.db", DB_BTREE, DB_CREATE, 0u);
Les données sur le disque seront écrites dans le fichier sample-map-usage.db , qui sera créé en l'absence (grâce à l'indicateur DB_CREATE
) dans le répertoire ENV_FOLDER
. Une arborescence est utilisée pour le stockage (paramètre DB_BTREE
).
Dans Berkeley DB, les clés et les valeurs sont stockées sous la forme d'un tableau d'octets. Pour utiliser un type personnalisé (dans notre cas TestElement
), vous devez définir des fonctions pour:
- recevoir le nombre d'octets pour stocker l'objet;
- marshaling d'un objet dans un tableau d'octets;
- démêler.
Dans l'exemple, cette fonctionnalité est effectuée par les méthodes statiques de la classe TestMarshaller
. Il TestElement
objets TestElement
en mémoire comme suit:
- la longueur du champ
id
est copiée au début du tampon - octet suivant le contenu du champ
id
est placé - après cela, la taille du champ de
name
est copiée - puis le contenu lui-même est placé dans le champ du
name

Nous décrivons les fonctions de TestMarshaller
:
TestMarshaller::restore
- remplit l'objet TestElement
avec les données du tamponTestMarshaller::size
- retourne la taille du tampon qui est nécessaire pour enregistrer l'objet spécifié.TestMarshaller::store
- enregistre l'objet dans le tampon.
Pour enregistrer les fonctions de marshaling / dbstl::DbstlElemTraits
, utilisez 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 );
Initialisez le conteneur:
dbstl::db_map<std::string, TestElement> elementsMap(db, penv);
Voici à quoi ressemble la copie des éléments de std::map
vers le conteneur créé:
std::copy( std::cbegin(inputValues), std::cend(inputValues), std::inserter(elementsMap, elementsMap.begin()) );
Mais de cette façon, vous pouvez imprimer le contenu de la base de données sur la sortie standard:
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 + "}"; });
L'appel de la méthode begin
dans l'exemple ci-dessus semble un peu inhabituel: elementsMap.begin(dbstl::ReadModifyWriteOption::no_read_modify_write(), true)
.
Cette conception est utilisée pour obtenir un itérateur en lecture seule . dbstl ne définit pas la méthode cbegin
; à la place, le paramètre en readonly
(le second) de la méthode begin
est utilisé. Vous pouvez également utiliser une référence constante au conteneur pour obtenir un itérateur en lecture seule . Un tel itérateur ne permet qu'une opération de lecture; lors de l'écriture, il lèvera une exception.
Pourquoi l'itérateur en lecture seule est-il utilisé dans le code ci-dessus? Tout d'abord, il effectue simplement une opération de lecture via un itérateur. Deuxièmement, la documentation indique qu'elle a de meilleures performances par rapport à la version régulière.
Ajouter une nouvelle paire clé / valeur ou, si la clé existe déjà, mettre à jour la valeur est aussi simple que dans std::map
:
elementsMap["added key 1"] = {"added id 1", "added name 1"};
Comme mentionné ci-dessus, l'instruction elementsMap["added key 1"]
renvoie une classe wrapper avec operator=
redefined, dont l'appel suivant stocke directement l'objet dans la base de données.
Si vous devez insérer un élément dans un conteneur:
auto [iter, res] = elementsMap.insert( std::make_pair(std::string("added key 2"), TestElement{"added id 2", "added name 2"}) );
L'appel à elementsMap.insert
renvoie std::pair<, >
. Si l'objet ne peut pas être inséré, l' indicateur de réussite sera faux . Sinon, l'indicateur de réussite contient true et l' itérateur pointe vers l'objet inséré.
Une autre façon de trouver la valeur par clé est d'utiliser la dbstl::db_map::find
, similaire à std::map::find
:
auto findIter = elementsMap.find("test key 1");
Grâce à l'itérateur obtenu, vous pouvez accéder à la clé - findIter->first
, aux champs de l'élément findIter->second.id
- findIter->second.id
et findIter->second.name
. Pour extraire une paire clé / valeur , l'opérateur de déréférence est utilisé - auto iterPair = *findIter;
.
Lorsque l'opérateur de déréférencement ( * ) ou l'accès à un membre de la classe ( -> ) est appliqué à l'itérateur, la base de données est accessible et les données en sont extraites. De plus, les données extraites précédemment, même si elles ont été modifiées, sont effacées. Cela signifie que dans l'exemple ci-dessous, les modifications apportées à l'itérateur seront ignorées et la valeur stockée dans la base de données sera affichée sur la console.
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;
Pour éviter cela, vous devez obtenir l'encapsuleur de l'objet stocké à partir de l'itérateur en appelant findIter->second
et l'enregistrer dans une variable. Ensuite, apportez toutes les modifications sur ce wrapper et écrivez le résultat dans la base de données en appelant la méthode de wrapper _DB_STL_StoreElement
:
auto ref = findIter->second; ref.id = "new test id 1"; ref.name = "new test name 1"; ref._DB_STL_StoreElement();
La mise à jour des données peut être encore plus facile - il suffit d'obtenir le wrapper avec l'instruction findIter->second
et de lui affecter l'objet TestElement
souhaité, comme dans l'exemple:
if(auto findIter = elementsMap.find("test key 2"); findIter != elementsMap.end()){ findIter->second = {"new test id 2", "new test name 2"}; }
Avant de terminer le programme, vous devez appeler dbstl::dbstl_exit();
pour fermer et supprimer tous les objets enregistrés dans le gestionnaire de ressources.
En conclusion
Cet article fournit un bref aperçu des principales fonctionnalités des conteneurs dbstl::db_map
utilisant dbstl::db_map
comme dbstl::db_map
dans un programme simple à thread unique. Ceci n'est qu'une petite introduction et n'a pas couvert les fonctionnalités telles que la transactionnalité, le verrouillage, la gestion des ressources, la gestion des exceptions et l'exécution multithread.
Je n'ai pas cherché à décrire en détail les méthodes et leurs paramètres, pour cela il vaut mieux se référer à la documentation correspondante sur l' interface C ++ et sur l' interface STL