Berkeley DB STL-Schnittstelle

Hallo Habr. Vor nicht allzu langer Zeit benötigte ich für eines meiner Projekte eine eingebettete Datenbank, in der Schlüsselwertelemente gespeichert, Transaktionsunterstützung bereitgestellt und optional Daten verschlüsselt wurden. Nach einer kurzen Suche stieß ich auf ein Berkeley DB- Projekt. Zusätzlich zu den Funktionen, die ich benötige, bietet diese Datenbank eine STL-kompatible Schnittstelle, mit der Sie mit der Datenbank wie mit einem normalen (fast normalen) STL-Container arbeiten können. Tatsächlich wird diese Schnittstelle unten diskutiert.


Berkeley db


Berkeley DB ist eine eingebettete, skalierbare, leistungsstarke Open Source-Datenbank. Es ist kostenlos für die Verwendung in Open Source- Projekten verfügbar, für proprietäre Projekte gibt es jedoch erhebliche Einschränkungen. Unterstützte Funktionen:


  • Transaktionen
  • Fail-Ahead-Protokoll für Failover
  • AES- Datenverschlüsselung
  • Replikation
  • Indizes
  • Synchronisationstools für Multithread-Anwendungen
  • Zugriffsrichtlinie - ein Autor, viele Leser
  • Caching

Wie viele andere.


Bei der Initialisierung des Systems kann der Benutzer angeben, welche Subsysteme verwendet werden sollen. Dadurch wird die Verschwendung von Ressourcen für Vorgänge wie Transaktionen, Protokollierung und Sperren vermieden, wenn diese nicht benötigt werden.


Die Wahl der Speicherstruktur und des Datenzugriffs ist möglich:


  • Btree - Implementierung eines sortierten ausgeglichenen Baums
  • Hash - lineare Hash-Implementierung
  • Heap - Verwendet eine Heap-Datei, die logisch zur Speicherung ausgelagert wird. Jeder Eintrag wird durch eine Seite und einen Versatz darin gekennzeichnet. Der Speicher ist so organisiert, dass das Löschen eines Datensatzes keine Komprimierung erfordert. Dies ermöglicht es Ihnen, es mit einem Mangel an physischem Platz zu verwenden.
  • Warteschlange - Eine Warteschlange, in der Datensätze fester Länge mit einer logischen Nummer als Schlüssel gespeichert werden. Es ist für das schnelle Einfügen am Ende konzipiert und unterstützt eine spezielle Operation, bei der ein Eintrag in einem Anruf aus dem Kopf der Warteschlange entfernt und zurückgegeben wird.
  • Recno - Ermöglicht das Speichern von Datensätzen fester und variabler Länge mit einer logischen Nummer als Schlüssel. Ermöglicht den Zugriff auf ein Element über seinen Index.

Um Mehrdeutigkeiten zu vermeiden, müssen mehrere Konzepte definiert werden, mit denen die Arbeit von Berkeley DB beschrieben wird .


Die Datenbank ist ein Schlüsselwertspeicher. Ein Analogon der Berkeley DB- Datenbank in anderen DBMS kann eine Tabelle sein.


Eine Datenbankumgebung ist ein Wrapper für eine oder mehrere Datenbanken . Definiert allgemeine Einstellungen für alle Datenbanken , z. B. Cache-Größe, Dateispeicherpfade, Verwendung und Konfiguration von Blockierungs-, Transaktions- und Protokollierungs-Subsystemen.


In einem typischen Anwendungsfall wird eine Umgebung erstellt und konfiguriert und verfügt über eine oder mehrere Datenbanken .


STL-Schnittstelle


Berkeley DB ist eine Bibliothek in C. Es enthält Ordner für Sprachen wie Perl , Java , PHP und andere. Die Schnittstelle für C ++ ist ein Wrapper über C- Code mit Objekten und Vererbung. Um den Zugriff auf die Datenbank ähnlich wie bei Operationen mit STL- Containern zu ermöglichen, gibt es eine STL- Schnittstelle als Add-On über C ++ . In grafischer Form sehen die Schnittstellenebenen folgendermaßen aus:



Über die STL- Schnittstelle können Sie ein Element aus der Datenbank nach Schlüssel (für Btree oder Hash ) oder nach Index (für Recno ) abrufen, ähnlich wie bei std::map map- oder std::vector Containern. Suchen Sie ein Element in der Datenbank über den Standardalgorithmus std::find_if . Durchlaufen Sie die gesamte Datenbank durch die foreach . Alle Klassen und Funktionen der Berkeley DB STL- Schnittstelle befinden sich im dbstl- Namespace. Kurz gesagt bedeutet dbstl auch die STL- Schnittstelle.


Installation


Die Datenbank unterstützt die meisten Linux- Plattformen , Windows , Android , Apple iOS usw.


Für Ubuntu 18.04 installieren Sie einfach die Pakete:


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

Um aus Linux- Quellen zu erstellen, müssen Sie autoconf und libtool installieren. Den neuesten Quellcode finden Sie hier .


Zum Beispiel habe ich das Archiv mit der Version 18.1.32 - db-18.1.32.zip heruntergeladen. Sie müssen das Archiv entpacken und zum Quellordner wechseln:


 unzip db-18.1.32.zip cd db-18.1.32 

Als nächstes wechseln wir in das Verzeichnis build_unix und führen die Assembly und Installation aus:


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

Hinzufügen zum cmake-Projekt


Das BerkeleyDBSamples- Projekt wird verwendet, um Beispiele mit Berkeley DB zu veranschaulichen.


Die Struktur des Projekts ist wie folgt:


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

Die Root- Datei CMakeLists.txt beschreibt die allgemeinen Parameter des Projekts. Beispielquelldateien werden als Beispiel verwendet . sample-usage / CMakeLists.txt sucht nach Bibliotheken und definiert die Zusammenstellung von Beispielen.


In Beispielen wird FindBerkeleyDB verwendet, um die Bibliothek mit dem cmake- Projekt zu verbinden. Es wird als Git- Submodul in Submodulen / cmake hinzugefügt . Während der Montage müssen Sie möglicherweise BerkeleyDB_ROOT_DIR angeben. Für die oben aus den Quellen installierte Bibliothek müssen Sie beispielsweise das Flag cmake -DBerkeleyDB_ROOT_DIR=/home/user/libraries/berkeley-db angeben.


Fügen Sie in der Stammdatei CMakeLists.txt den Pfad zum FindBerkeleyDB- Modul zu CMAKE_MODULE_PATH hinzu :


 list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/submodules/cmake/FindBerkeleyDB") 

Danach führt sample-usage / CMakeLists.txt eine Bibliothekssuche auf die folgende Weise durch:


 find_package(BerkeleyDB REQUIRED) 

Fügen Sie als Nächstes die ausführbare Datei hinzu und verknüpfen Sie sie mit der Oracle :: BerkeleyDB- Bibliothek:


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

Praktisches Beispiel


Um die Verwendung von dbstl zu demonstrieren , untersuchen wir ein einfaches Beispiel aus der Datei sample-map-usage.cpp . Diese Anwendung demonstriert die Arbeit mit dem dbstl::db_map in einem Programm mit einem Thread. Der Container selbst ähnelt std::map und speichert Daten als Schlüssel / Wert-Paar. Die zugrunde liegende Datenbankstruktur kann Btree oder Hash sein . Im Gegensatz zu std::map dbstl::db_map<std::string, TestElement> der dbstl::db_map<std::string, TestElement> für den dbstl::db_map<std::string, TestElement> dbstl::ElementRef<TestElement> . Dieser Typ wird beispielsweise für dbstl::db_map<std::string, TestElement>::operator[] . Es definiert Methoden zum Speichern eines Objekts vom Typ TestElement in der Datenbank. Eine solche Methode ist operator= .


Im Beispiel wird mit der Datenbank wie folgt gearbeitet:


  • Anwendung ruft Berkeley DB- Methoden auf, um auf Daten zuzugreifen
  • Diese Methoden greifen zum Lesen oder Schreiben auf den Cache zu
  • Bei Bedarf erfolgt der Zugriff direkt auf die Datendatei

Grafisch ist dieser Vorgang in der Abbildung dargestellt:



Um die Komplexität des Beispiels zu verringern, wird keine Ausnahmebehandlung verwendet. Einige Dbstl- Container- Methoden können Ausnahmen auslösen , wenn Fehler auftreten.


Code-Analyse


Um mit Berkeley DB arbeiten zu können, müssen Sie zwei Header-Dateien verbinden:


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

Das erste fügt C ++ - Schnittstellenprimitive hinzu, und das zweite definiert Klassen und Funktionen für die Arbeit mit der Datenbank, wie bei einem assoziativen Container, sowie viele Dienstprogrammmethoden. Die STL- Schnittstelle befindet sich im dbstl- Namespace.


Für die Speicherung wird die Btree- Struktur verwendet , std::string fungiert als Schlüssel und der Wert ist die Benutzerstruktur TestElement :


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

Initialisieren Sie in der main die Bibliothek, indem Sie dbstl::dbstl_startup() aufrufen. Es muss sich vor der ersten Verwendung der Grundelemente der STL- Schnittstelle befinden.


Danach initialisieren und öffnen wir die Datenbankumgebung in dem Verzeichnis, das durch die Variable ENV_FOLDER wird:


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

Das Flag DB_INIT_MPOOL für die Initialisierung des Caching-Subsystems DB_CREATE - für die Erstellung aller für die Umgebung erforderlichen Dateien. Das Team registriert dieses Objekt auch im Ressourcenmanager. Er ist dafür verantwortlich, alle registrierten Objekte (Datenbankobjekte, Cursor, Transaktionen usw. sind ebenfalls darin registriert) zu schließen und den dynamischen Speicher zu löschen. Wenn Sie bereits ein Datenbankumgebungsobjekt haben und es nur beim Ressourcenmanager registrieren müssen, können Sie die Funktion dbstl::register_db_env verwenden.


Ein ähnlicher Vorgang wird mit der Datenbank ausgeführt :


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

Daten auf der Festplatte werden in die Datei sample-map-usage.db geschrieben , die in Abwesenheit (dank des DB_CREATE Flags) im Verzeichnis ENV_FOLDER . Für die Speicherung wird ein Baum verwendet (Parameter DB_BTREE ).


In Berkeley DB werden Schlüssel und Werte als Array von Bytes gespeichert. Um einen benutzerdefinierten Typ (in unserem Fall TestElement ) zu verwenden, müssen Sie Funktionen definieren für:


  • Empfangen der Anzahl von Bytes zum Speichern des Objekts;
  • Marshalling eines Objekts in ein Array von Bytes;
  • Unmarshaling.

Im Beispiel wird diese Funktionalität von den statischen Methoden der TestMarshaller Klasse ausgeführt. Es TestElement Objekte im Speicher wie folgt zu:


  • Die Länge des id Feldes wird an den Anfang des Puffers kopiert
  • Im nächsten Byte wird der Inhalt des id Feldes platziert
  • Danach wird die Größe des Namensfeldes kopiert
  • dann wird der Inhalt selbst aus dem Namensfeld platziert


Wir beschreiben die Funktionen von TestMarshaller :


  • TestMarshaller::restore - TestElement das TestElement Objekt mit Daten aus dem Puffer
  • TestMarshaller::size - TestMarshaller::size die Größe des Puffers zurück, der zum Speichern des angegebenen Objekts benötigt wird.
  • TestMarshaller::store - speichert das Objekt im Puffer.

Verwenden Sie dbstl::DbstlElemTraits , um Marshalling- / dbstl::DbstlElemTraits Funktionen zu registrieren:


 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 ); 

Initialisieren Sie den Container:


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

So sieht das Kopieren von Elementen aus std::map in den erstellten Container aus:


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

Auf diese Weise können Sie den Inhalt der Datenbank in Standardausgabe drucken:


 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 + "}"; }); 

Das Aufrufen der begin Methode im obigen Beispiel sieht etwas ungewöhnlich aus: elementsMap.begin(dbstl::ReadModifyWriteOption::no_read_modify_write(), true) .
Dieses Design wird verwendet, um einen schreibgeschützten Iterator zu erhalten. dbstl definiert nicht die cbegin Methode, sondern der readonly Parameter (der zweite) in der begin Methode. Sie können auch einen konstanten Verweis auf den Container verwenden, um einen schreibgeschützten Iterator abzurufen. Ein solcher Iterator erlaubt nur eine Leseoperation, beim Schreiben wird eine Ausnahme ausgelöst.


Warum wird der schreibgeschützte Iterator im obigen Code verwendet? Erstens führt es nur eine Leseoperation durch einen Iterator aus. Zweitens heißt es in der Dokumentation, dass die Leistung im Vergleich zur regulären Version besser ist .


Das Hinzufügen eines neuen Schlüssel / Wert-Paares oder, falls der Schlüssel bereits vorhanden ist, das Aktualisieren des Werts ist so einfach wie in std::map :


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

Wie oben erwähnt, gibt die Anweisung elementsMap["added key 1"] eine Wrapper-Klasse mit operator= redefined zurück, deren nachfolgender Aufruf das Objekt direkt in der Datenbank speichert.


Wenn Sie einen Artikel in einen Container einfügen müssen:


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

Der Aufruf von elementsMap.insert gibt std::pair<, > . Wenn das Objekt nicht eingefügt werden kann, ist das Erfolgsflag falsch . Andernfalls enthält das Erfolgsflag true und der Iterator zeigt auf das eingefügte Objekt.


Eine andere Möglichkeit, den Wert anhand des Schlüssels zu ermitteln, ist die Verwendung der Methode dbstl::db_map::find , ähnlich wie bei std::map::find :


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

Über den erhaltenen Iterator können Sie auf den Schlüssel findIter->first in den Feldern des TestElement Elements findIter->second.id und findIter->second.name . Um ein Schlüssel / Wert- Paar zu extrahieren, wird der Dereferenzierungsoperator verwendet - auto iterPair = *findIter; .


Wenn der Dereferenzierungsoperator ( * ) oder der Zugriff auf ein Klassenmitglied ( -> ) auf den Iterator angewendet wird, wird auf die Datenbank zugegriffen und Daten daraus extrahiert. Darüber hinaus werden zuvor extrahierte Daten, selbst wenn sie geändert wurden, gelöscht. Dies bedeutet, dass im folgenden Beispiel die am Iterator vorgenommenen Änderungen verworfen werden und der in der Datenbank gespeicherte Wert auf der Konsole angezeigt wird.


 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; 

Um dies zu vermeiden, müssen Sie den Wrapper des gespeicherten Objekts vom Iterator findIter->second indem Sie findIter->second aufrufen und in einer Variablen speichern. _DB_STL_StoreElement Nächstes alle Änderungen an diesem Wrapper vor und schreiben Sie das Ergebnis in die Datenbank, indem Sie die Wrapper-Methode _DB_STL_StoreElement :


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

Das Aktualisieren der Daten kann noch einfacher sein - holen Sie sich einfach den Wrapper mit der Anweisung findIter->second und weisen Sie ihm das gewünschte TestElement Objekt zu, wie im Beispiel:


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

Bevor Sie das Programm beenden, müssen Sie dbstl::dbstl_exit(); um alle registrierten Objekte im Ressourcenmanager zu schließen und zu löschen.


Abschließend


Dieser Artikel bietet einen kurzen Überblick über die Hauptfunktionen von Dbstl- Containern am Beispiel von dbstl::db_map in einem einfachen Single-Thread-Programm. Dies ist nur eine kleine Einführung und hat Funktionen wie Transaktionalität, Sperren, Ressourcenverwaltung, Ausnahmebehandlung und Multithread-Ausführung nicht behandelt.


Ich wollte die Methoden und ihre Parameter nicht detailliert beschreiben, dazu ist es besser, auf die entsprechende Dokumentation zur C ++ - Schnittstelle und zur STL-Schnittstelle zu verweisen

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


All Articles