Oi Habr. Há pouco tempo, para um dos meus projetos, eu precisava de um banco de dados incorporado que armazenasse elementos de valor-chave, forneça suporte a transações e, opcionalmente, dados criptografados. Após uma breve pesquisa, me deparei com um projeto do Berkeley DB . Além dos recursos de que preciso, esse banco de dados fornece uma interface compatível com STL que permite trabalhar com o banco de dados como em um contêiner STL regular (quase comum). Na verdade, essa interface será discutida abaixo.
Berkeley db
O Berkeley DB é um banco de dados de código aberto incorporado, escalável e de alto desempenho. Está disponível gratuitamente para uso em projetos de código aberto , mas para os proprietários há limitações significativas. Recursos suportados:
- transações
- log de failover antecipado para failover
- Criptografia de dados AES
- replicação
- índices
- ferramentas de sincronização para aplicativos multithread
- política de acesso - um escritor, muitos leitores
- cache
Assim como muitos outros.
Quando o sistema é inicializado, o usuário pode especificar quais subsistemas usar. Isso elimina o desperdício de recursos em operações como transações, log, bloqueios quando não são necessários.
A opção de estrutura de armazenamento e acesso a dados está disponível:
- Btree - implementação de árvore balanceada classificada
- Hash - implementação linear de hash
- Pilha - usa um arquivo de pilha paginada logicamente para armazenamento. Cada entrada é identificada por uma página e um deslocamento dentro dela. O armazenamento é organizado de forma que a exclusão de um registro não exija compactação. Isso permite que você o use com falta de espaço físico.
- Fila - uma fila que armazena registros de comprimento fixo com um número lógico como chave. Ele foi projetado para inserção rápida no final e suporta uma operação especial que remove e retorna uma entrada do início da fila em uma única chamada.
- Recno - permite salvar registros de comprimentos fixos e variáveis com um número lógico como chave. Fornece acesso a um elemento por seu índice.
Para evitar ambiguidade, é necessário definir vários conceitos que são usados para descrever o trabalho do Berkeley DB .
Banco de dados é um armazenamento de dados de valor-chave. Um análogo do banco de dados Berkeley DB em outros DBMSs pode ser uma tabela.
Um ambiente de banco de dados é um invólucro para um ou mais bancos de dados . Define configurações gerais para todos os bancos de dados , como tamanho do cache, caminhos de armazenamento de arquivos, uso e configuração de subsistemas de bloqueio, transação e log.
Em um caso de uso típico, um ambiente é criado e configurado e possui um ou mais bancos de dados .
Interface STL
Berkeley DB é uma biblioteca escrita em C. Possui ligantes para linguagens como Perl , Java , PHP e outros. A interface para C ++ é um invólucro sobre código C com objetos e herança. Para tornar possível acessar o banco de dados de maneira semelhante às operações com contêineres STL , existe uma interface STL como um complemento sobre C ++ . Em uma forma gráfica, as camadas da interface são assim:

Portanto, a interface STL permite recuperar um elemento do banco de dados por chave (para Btree ou Hash ) ou por índice (para Recno ), semelhante aos contêineres std::map
ou std::vector
; encontre um elemento no banco de dados através do std::find_if
padrão std::find_if
, itere sobre todo o banco de dados através do foreach
. Todas as classes e funções da interface STL do Berkeley DB estão no espaço de nomes dbstl ; em resumo, dbstl também significa a interface STL .
Instalação
O banco de dados suporta a maioria das plataformas Linux , Windows , Android , Apple iOS , etc.
Para o Ubuntu 18.04, basta instalar os pacotes:
- libdb5.3-stl-dev
- libdb5.3 ++ - dev
Para construir a partir de fontes Linux , você precisa instalar o autoconf e o libtool . O código-fonte mais recente pode ser encontrado aqui .
Por exemplo, baixei o arquivo com a versão 18.1.32 - db-18.1.32.zip. Você precisa descompactar o arquivo morto e ir para a pasta de origem:
unzip db-18.1.32.zip cd db-18.1.32
Em seguida, passamos para o diretório build_unix e executamos a montagem e instalação:
cd build_unix ../dist/configure --enable-stl --prefix=/home/user/libraries/berkeley-db make make install
Adicionando ao projeto cmake
O projeto BerkeleyDBSamples é usado para ilustrar exemplos com o Berkeley DB .
A estrutura do projeto é a seguinte:
+-- CMakeLists.txt +-- sample-usage | +-- CMakeLists.txt | +-- sample-map-usage.cpp | +-- submodules | +-- cmake | | +-- FindBerkeleyDB
A raiz CMakeLists.txt descreve os parâmetros gerais do projeto. Os arquivos de origem de amostra estão em uso de amostra . sample-use / CMakeLists.txt procura bibliotecas, define a montagem de exemplos.
Em exemplos, o FindBerkeleyDB é usado para conectar a biblioteca ao projeto cmake . É adicionado como um submódulo git em submódulos / cmake . Durante a montagem, pode ser necessário especificar BerkeleyDB_ROOT_DIR
. Por exemplo, para a biblioteca acima instalada a partir das fontes, você deve especificar o sinalizador cmake -DBerkeleyDB_ROOT_DIR=/home/user/libraries/berkeley-db
.
No arquivo raiz CMakeLists.txt , adicione o caminho ao módulo FindBerkeleyDB em CMAKE_MODULE_PATH :
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/submodules/cmake/FindBerkeleyDB")
Depois disso, o sample-use / CMakeLists.txt realiza uma pesquisa de biblioteca da maneira padrão:
find_package(BerkeleyDB REQUIRED)
Em seguida, adicione o arquivo executável e vincule-o à 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)
Exemplo prático
Para demonstrar o uso do dbstl, vamos examinar um exemplo simples do arquivo sample-map-use.cpp . Este aplicativo demonstra o trabalho com o dbstl::db_map
em um programa de thread único. O contêiner em si é semelhante ao std::map
e armazena dados como um par de chave / valor. A estrutura do banco de dados subjacente pode ser Btree ou Hash . Diferentemente de std::map
, para o dbstl::db_map<std::string, TestElement>
tipo de valor real é dbstl::ElementRef<TestElement>
. Este tipo é retornado, por exemplo, para dbstl::db_map<std::string, TestElement>::operator[]
. Ele define métodos para armazenar um objeto do tipo TestElement
no banco de dados. Um desses métodos é operator=
.
No exemplo, o trabalho com o banco de dados é o seguinte:
- aplicativo chama métodos Berkeley DB para acessar dados
- esses métodos acessam o cache para leitura ou gravação
- se necessário, o acesso é diretamente ao arquivo de dados
Graficamente, esse processo é mostrado na figura:

Para reduzir a complexidade do exemplo, ele não usa o tratamento de exceções. Alguns métodos de contêiner dbstl podem lançar exceções quando ocorrem erros.
Análise de código
Para trabalhar com o Berkeley DB, você precisa conectar dois arquivos de cabeçalho:
#include <db_cxx.h> #include <dbstl_map.h>
O primeiro adiciona primitivas da interface C ++ e o segundo define classes e funções para trabalhar com o banco de dados, como em um contêiner associativo, bem como em muitos métodos utilitários. A interface STL está localizada no espaço para nome dbstl .
Para armazenamento, a estrutura Btree é usada , std::string
atua como a chave e o valor é a estrutura do usuário TestElement
:
struct TestElement{ std::string id; std::string name; };
Na função main
, inicialize a biblioteca chamando dbstl::dbstl_startup()
. Ele deve estar localizado antes do primeiro uso das primitivas da interface STL .
Depois disso, inicializamos e abrimos o ambiente do banco de dados no diretório definido pela variável ENV_FOLDER
:
auto penv = dbstl::open_env(ENV_FOLDER, 0u, DB_INIT_MPOOL | DB_CREATE);
O sinalizador DB_INIT_MPOOL
responsável pela inicialização do subsistema de armazenamento em cache, DB_CREATE
- pela criação de todos os arquivos necessários para o ambiente. A equipe também registra esse objeto no gerenciador de recursos. Ele é responsável por fechar todos os objetos registrados (objetos de banco de dados, cursores, transações etc. também estão registrados nele) e limpar a memória dinâmica. Se você já possui um objeto de ambiente de banco de dados e precisa registrá-lo apenas no gerenciador de recursos, poderá usar a função dbstl::register_db_env
.
Uma operação semelhante é realizada com o banco de dados :
auto db = dbstl::open_db(penv, "sample-map-usage.db", DB_BTREE, DB_CREATE, 0u);
Os dados no disco serão gravados no arquivo sample-map-use.db , que será criado na ausência (graças ao sinalizador DB_CREATE
) no diretório ENV_FOLDER
. Uma árvore é usada para armazenamento (parâmetro DB_BTREE
).
No Berkeley DB, chaves e valores são armazenados como uma matriz de bytes. Para usar um tipo personalizado (no nosso caso TestElement
), você deve definir funções para:
- receber o número de bytes para armazenar o objeto;
- organizar um objeto em uma matriz de bytes;
- desembaraçar.
No exemplo, essa funcionalidade é executada pelos métodos estáticos da classe TestMarshaller
. Ele TestElement
objetos TestElement
na memória da seguinte maneira:
- o comprimento do campo
id
é copiado para o início do buffer - próximo byte, o conteúdo do campo
id
é colocado - depois, o tamanho do campo de
name
é copiado - então o conteúdo em si é colocado no campo de
name

Nós descrevemos as funções do TestMarshaller
:
TestMarshaller::restore
- preenche o objeto TestElement
com dados do bufferTestMarshaller::size
- retorna o tamanho do buffer necessário para salvar o objeto especificado.TestMarshaller::store
- salva o objeto no buffer.
Para registrar funções de empacotamento / 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 );
Inicialize o contêiner:
dbstl::db_map<std::string, TestElement> elementsMap(db, penv);
É assim que a cópia de elementos do std::map
para o contêiner criado se parece:
std::copy( std::cbegin(inputValues), std::cend(inputValues), std::inserter(elementsMap, elementsMap.begin()) );
Mas, dessa maneira, você pode imprimir o conteúdo do banco de dados na saída padrão:
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 + "}"; });
Chamar o método begin
no exemplo acima parece um pouco incomum: elementsMap.begin(dbstl::ReadModifyWriteOption::no_read_modify_write(), true)
.
Esse design é usado para obter um iterador somente leitura . O dbstl não define o método cbegin
; em vez disso, o parâmetro readonly
(o segundo) no método begin
é usado. Você também pode usar uma referência constante ao contêiner para obter um iterador somente leitura . Esse iterador permite apenas uma operação de leitura; ao escrever, gera uma exceção.
Por que o iterador somente leitura é usado no código acima? Primeiramente, ele apenas executa uma operação de leitura através de um iterador. Em segundo lugar, a documentação diz que tem melhor desempenho em comparação com a versão regular.
Adicionar um novo par de chave / valor ou, se a chave já existir, atualizar o valor é tão simples quanto em std::map
:
elementsMap["added key 1"] = {"added id 1", "added name 1"};
Como mencionado acima, a instrução elementsMap["added key 1"]
retorna uma classe de wrapper com operator=
redefined, cuja chamada subsequente armazena diretamente o objeto no banco de dados.
Se você precisar inserir um item em um contêiner:
auto [iter, res] = elementsMap.insert( std::make_pair(std::string("added key 2"), TestElement{"added id 2", "added name 2"}) );
A chamada para elementsMap.insert
retorna std::pair<, >
. Se o objeto não puder ser inserido, o sinalizador de sucesso será falso . Caso contrário, o sinalizador de sucesso contém true e o iterador aponta para o objeto inserido.
Outra maneira de encontrar o valor pela chave é usar o dbstl::db_map::find
, semelhante ao std::map::find
:
auto findIter = elementsMap.find("test key 1");
Por meio do iterador obtido, é possível acessar a chave - findIter->first
, nos campos do elemento findIter->second.id
- findIter->second.id
e findIter->second.name
. Para extrair um par de chave / valor , o operador de desreferência é usado - auto iterPair = *findIter;
.
Quando o operador de desreferenciamento ( * ) ou o acesso a um membro da classe ( -> ) é aplicado ao iterador, o banco de dados é acessado e os dados são extraídos. Além disso, os dados extraídos anteriormente, mesmo que tenham sido modificados, são apagados. Isso significa que, no exemplo abaixo, as alterações feitas no iterador serão descartadas e o valor armazenado no banco de dados será exibido no 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;
Para evitar isso, você precisa obter o wrapper do objeto armazenado do iterador chamando findIter->second
e salvá-lo em uma variável. Em seguida, faça todas as alterações nesse wrapper e _DB_STL_StoreElement
o resultado no banco de dados chamando o método 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();
A atualização dos dados pode ser ainda mais fácil - basta obter o wrapper com a findIter->second
e atribuir o objeto TestElement
desejado, como no exemplo:
if(auto findIter = elementsMap.find("test key 2"); findIter != elementsMap.end()){ findIter->second = {"new test id 2", "new test name 2"}; }
Antes de finalizar o programa, você deve chamar dbstl::dbstl_exit();
fechar e excluir todos os objetos registrados no gerenciador de recursos.
Em conclusão
Este artigo fornece uma breve visão geral dos principais recursos dos contêineres dbstl::db_map
usando dbstl::db_map
como um dbstl::db_map
em um programa simples de thread único. Esta é apenas uma pequena introdução e não abordou recursos como transacionalidade, bloqueio, gerenciamento de recursos, tratamento de exceções e execução multithread.
Não pretendi descrever detalhadamente os métodos e seus parâmetros; para isso, é melhor consultar a documentação correspondente na interface C ++ e na interface STL