Interface STL de Berkeley DB

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 buffer
  • TestMarshaller::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

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


All Articles