哈Ha 不久前,对于我的一个项目,我需要一个嵌入式数据库来存储键值元素,提供事务支持以及加密数据(可选)。 经过短暂搜索,我遇到了一个Berkeley DB项目。 除了我需要的功能之外,该数据库还提供了一个与STL兼容的接口,使您可以像使用常规(几乎普通)的STL容器一样使用该数据库。 实际上,下面将讨论此接口。
伯克利分贝
Berkeley DB是一个嵌入式,可扩展,高性能的开源数据库。 它可免费用于开放源代码项目,但对于专有项目则有很多限制。 支持的功能:
- 交易
- 故障转移日志
- AES数据加密
- 复制
- 指标
- 多线程应用程序的同步工具
- 访问策略-一位作家,很多读者
- 快取
以及许多其他。
初始化系统后,用户可以指定要使用的子系统。 这样可以消除不必要的资源浪费,例如事务,日志,锁等操作。
可以选择存储结构和数据访问:
- Btree-排序平衡树的实现
- 哈希 -线性哈希实现
- 堆 -使用按逻辑分页的堆文件进行存储。 每个条目均由页面及其内的偏移量标识。 存储的组织方式使得删除记录不需要压缩。 这使您可以在缺乏物理空间的情况下使用它。
- 队列 -一种队列,用于存储固定长度的记录,并以逻辑数字作为键。 它设计用于在末尾快速插入,并支持一种特殊操作,该操作可在一次调用中从队列的开头删除并返回一个条目。
- Recno-允许您使用逻辑数字作为键来保存固定长度和可变长度的记录。 通过其索引提供对元素的访问。
为了避免歧义,有必要定义几个用于描述Berkeley DB工作的概念。
数据库是键值数据存储。 一个表可以与其他DBMS中的 Berkeley DB数据库类似。
数据库环境是一个或多个数据库的包装。 定义所有数据库的常规设置,例如缓存大小,文件存储路径,阻塞的使用和配置,事务,日志记录子系统。
在典型的用例中,创建并配置了一个环境 ,该环境具有一个或多个数据库 。
STL接口
Berkeley DB是用C编写的库。 它具有诸如Perl , Java , PHP等语言的活页夹 。 C ++的接口是带有对象和继承的C代码的包装。 为了能够像使用STL容器进行操作一样访问数据库,有一个STL接口作为C ++的附加组件。 在图形形式中,界面层如下所示:

因此, STL接口类似于std::map
或std::vector
容器,使您可以通过键(对于Btree或Hash )或通过索引(对于Recno )从数据库中检索元素,并通过标准std::find_if
在数据库中查找元素,通过foreach
遍历整个数据库。 Berkeley DB STL接口的所有类和功能都在dbstl命名空间中,简而言之, dbstl也表示STL接口。
安装方式
该数据库支持大多数Linux 平台 , Windows , Android , Apple iOS等。
对于Ubuntu 18.04,只需安装以下软件包:
- libdb5.3-stl-dev
- libdb5.3 ++-开发
要从Linux源码构建,您需要安装autoconf和libtool 。 最新的源代码可以在这里找到。
例如,我下载的归档文件的版本为 18.1.32-db-18.1.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项目
BerkeleyDBSamples项目用于说明Berkeley DB的示例。
该项目的结构如下:
+-- CMakeLists.txt +-- sample-usage | +-- CMakeLists.txt | +-- sample-map-usage.cpp | +-- submodules | +-- cmake | | +-- FindBerkeleyDB
根CMakeLists.txt描述了项目的常规参数。 样本源文件在sample-usage中 。 sample-usage / CMakeLists.txt搜索库,定义示例的汇编。
在示例中,使用FindBerkeleyDB将库连接到cmake项目。 它作为git子模块添加到submodules / cmake中 。 在组装期间,您可能需要指定BerkeleyDB_ROOT_DIR
。 例如,对于从源安装的上述库,必须指定标志cmake -DBerkeleyDB_ROOT_DIR=/home/user/libraries/berkeley-db
。
在根文件CMakeLists.txt中 ,将FindBerkeleyDB模块的路径添加到CMAKE_MODULE_PATH :
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/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的用法,让我们检查来自sample-map-usage.cpp文件的一个简单示例。 此应用程序演示了dbstl::db_map
在单线程程序中使用dbstl::db_map
。 容器本身类似于std::map
并将数据存储为键/值对。 基础数据库结构可以是Btree或Hash 。 与std::map
不同,对于dbstl::db_map<std::string, TestElement>
实际值类型是dbstl::ElementRef<TestElement>
。 例如,对于dbstl::db_map<std::string, TestElement>::operator[]
返回此类型。 它定义了在数据库中存储TestElement
类型的对象的方法。 一种这样的方法是operator=
。
在示例中,使用数据库的方法如下:
- 应用程序调用Berkeley DB方法来访问数据
- 这些方法访问缓存以进行读取或写入
- 如有必要,直接访问数据文件
该过程以图形方式显示在图中:

为了降低示例的复杂性,它不使用异常处理。 发生错误时,某些dbstl容器方法可能会引发异常。
代码解析
要使用Berkeley DB,您需要连接两个头文件:
#include <db_cxx.h> #include <dbstl_map.h>
第一个添加了C ++接口原语,第二个定义了用于数据库,关联容器以及许多实用程序方法的类和函数。 STL接口位于dbstl命名空间中。
对于存储,使用Btree结构, std::string
充当键,值是用户结构TestElement
:
struct TestElement{ std::string id; std::string name; };
在main
函数中,通过调用dbstl::dbstl_startup()
初始化库。 它必须位于第一次使用STL接口原语之前。
之后,我们在由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);
磁盘上的数据将被写入sample-map-usage.db文件 ,该文件将在ENV_FOLDER
目录中不存在时(由于DB_CREATE
标志)而ENV_FOLDER
。 一棵树用于存储( DB_BTREE
参数)。
在Berkeley DB中,键和值存储为字节数组。 要使用自定义类型(在我们的示例中为TestElement
),必须为以下功能定义函数:
- 接收用于存储对象的字节数;
- 将对象封送为字节数组;
- 解组。
在示例中,此功能由TestMarshaller
类的静态方法执行。 它在内存中TestElement
对象,如下所示:
id
字段的长度被复制到缓冲区的开头- 下一个字节放置
id
字段的内容 - 之后,复制
name
字段的大小 - 然后将内容本身放置在
name
字段中

我们描述了TestMarshaller
的功能:
TestMarshaller::restore
TestElement
用缓冲区中的数据填充TestElement
对象TestMarshaller::size
返回保存指定对象所需的缓冲区的大小。TestMarshaller::store
将对象保存在缓冲区中。
要注册封送/ dbstl::DbstlElemTraits
功能,请使用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 + "}"; });
在上面的示例中调用begin
方法看起来有点不寻常: elementsMap.begin(dbstl::ReadModifyWriteOption::no_read_modify_write(), true)
。
此设计用于获取只读迭代器。 dbstl没有定义cbegin
方法;相反,使用了begin
方法中的readonly
参数(第二个)。 您还可以使用对容器的常量引用来获取只读的迭代器。 这样的迭代器仅允许读取操作;在写入时,它将引发异常。
为什么在上面的代码中使用只读迭代器? 首先,它只是通过迭代器执行读取操作。 其次,文档说与常规版本相比,它具有更好的性能。
添加新的键/值对,或者,如果键已经存在,则更新值就像在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"}) );
调用elementsMap.insert
返回std::pair<, >
。 如果无法插入对象,则成功标志将为false 。 否则, 成功标志包含true ,并且迭代器指向插入的对象。
通过键查找值的另一种方法是使用dbstl::db_map::find
方法,类似于std::map::find
:
auto findIter = elementsMap.find("test key 1");
通过获得的迭代器,可以将键findIter->first
访问到TestElement
元素的字段findIter->second.id
和findIter->second.name
。 要提取键 / 值对,请使用解引用运算符auto iterPair = *findIter;
。
当将取消引用运算符( * )或对类成员的访问( -> )应用于迭代器时,将访问数据库并从中提取数据。 而且,即使先前提取的数据被修改,也将被擦除。 这意味着在下面的示例中,对迭代器所做的更改将被放弃,并且存储在数据库中的值将显示在控制台上。
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;
为了避免这种情况,您需要通过调用findIter->second
从迭代器中获取存储对象的包装并将其保存在变量中。 接下来,对该包装器进行所有更改,然后通过调用包装器方法_DB_STL_StoreElement
将结果写入数据库:
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::db_map
在一个简单的单线程程序中提供了dbstl容器主要功能的简要概述。 这只是一个小介绍,没有涉及诸如事务性,锁定,资源管理,异常处理和多线程执行等功能。
我的目的不是详细描述方法及其参数,因此最好参考C ++接口和STL接口上的相应文档。