SQLite3: Um Guia Prático para Desenvolvimento e Persistência de Dados

Introdução ao SQLite3

SQLite é um sistema de gerenciamento de banco de dados relacional (SGBDR) notável por sua implementação compacta e eficiente em linguagem C. Diferente de outros SGBDRs tradicionais que operam como servidores separados, o SQLite se destaca por ser "serverless", ou seja, o banco de dados é incorporado diretamente na aplicação que o utiliza. Ele armazena o banco de dados em um único arquivo de disco, o que facilita enormemente sua portabilidade e integração.

Suas características principais incluem:

  • Leveza e Velocidade: Possui uma pegada de memória mínima e oferece alta performance para a maioria das operações.
  • Independência: Não requer um processo de servidor separado, configuração complexa ou administração.
  • Alta Confiabilidade: Projetado para robustez e durabilidade dos dados.
  • Multiplataforma: Compatível com praticamente qualquer sistema operacional.
  • Amplamente Utilizado: Encontrado em milhões de dispositivos e aplicações, desde smartphones e navegadores web até sistemas embarcados e desktops.

Para mais informações, você pode visitar o site oficial: https://www.sqlite.org/index.html

Obtenção do Código Fonte do SQLite3

Para integrar o SQLite3 em projetos C ou C++, é comum baixar o código fonte e compilá-lo juntamente com sua aplicação. O site oficial oferece pacotes de código fonte que incluem o famoso "amalgamation", um arquivo sqlite3.c que contém todo o código necessário em um único arquivo para simplificar a compilação.

Você pode baixar a versão mais recente diretamente do site de downloads do SQLite (https://www.sqlite.org/download.html). Procure pelo pacote sqlite-autoconf-\*.tar.gz.

wget https://www.sqlite.org/2023/sqlite-autoconf-3410200.tar.gz
tar -zxvf sqlite-autoconf-3410200.tar.gz

Após a extração, você encontrará um diretório com diversos arquivos. Para a maioria dos projetos, os arquivos essenciais são sqlite3.c e sqlite3.h. Recomenda-se copiá-los para um subdiretório do seu projeto, por exemplo, lib/sqlite3, e remover o diretório original descompactado:

mkdir -p lib/sqlite3
cp sqlite-autoconf-3410200/sqlite3.c lib/sqlite3/
cp sqlite-autoconf-3410200/sqlite3.h lib/sqlite3/
cp sqlite-autoconf-3410200/sqlite3ext.h lib/sqlite3/ # Opcional, para extensões
rm -r sqlite-autoconf-3410200

A estrutura de arquivos do seu projeto pode ficar assim:

.
├── build/
├── CMakeLists.txt
├── app.cpp
└── lib/
    └── sqlite3/
        ├── sqlite3.c
        ├── sqlite3ext.h
        └── sqlite3.h

Configurando um Projeto CMake

Para um projeto C++ moderno, CMake é uma excelente ferramenta para gerenciar a compilação. Abaixo, um exemplo de CMakeLists.txt que inclui a biblioteca SQLite3 e a vincula a um executável:

cmake_minimum_required(VERSION 3.10 FATAL_ERROR)
project(GerenciadorDadosSQLite LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# Incluir o diretório onde os headers do SQLite estão
include_directories(lib/sqlite3)

# Adicionar a biblioteca SQLite3 estaticamente
add_library(sqlite_core STATIC lib/sqlite3/sqlite3.c)

# Em sistemas Linux, pode ser necessário vincular com 'dl' para funções de carregamento dinâmico
# target_link_libraries(sqlite_core PRIVATE dl) # Descomente se necessário

# Criar o executável principal
add_executable(app_sqlite app.cpp)

# Vincular o executável com a biblioteca SQLite
target_link_libraries(app_sqlite PRIVATE sqlite_core)

Exemplo de Código C++ Simples (app.cpp)

Este exemplo demonstra a abertura de um banco de dados, a criação de uma tabela, inserção de dados e uma consulta simples usando as APIs de C do SQLite3.

#include <iostream>
#include <sqlite3.h>
#include <string>
#include <vector>

// Callback para processar resultados de consultas SELECT
static int callback_exemplo(void *dados_extras, int n_colunas, char **valores, char **nomes_colunas) {
    std::cout << "Registro: ";
    for (int i = 0; i < n_colunas; i++) {
        std::cout << nomes_colunas[i] << " = " << (valores[i] ? valores[i] : "NULL") << (i < n_colunas - 1 ? ", " : "");
    }
    std::cout << std::endl;
    return 0;
}

int main() {
    sqlite3 *conexao_db;
    char *mensagem_erro = nullptr;
    int resultado_operacao;
    const std::string nome_banco = "meu_aplicativo.db";

    // 1. Abrir ou criar o banco de dados
    resultado_operacao = sqlite3_open(nome_banco.c_str(), &conexao_db);
    if (resultado_operacao != SQLITE_OK) {
        std::cerr << "Erro ao abrir o banco de dados: " << sqlite3_errmsg(conexao_db) << std::endl;
        return 1;
    }
    std::cout << "Banco de dados '" << nome_banco << "' aberto com sucesso." << std::endl;

    // 2. Criar uma tabela (se não existir)
    std::string sql_criar_tabela = R"(
        CREATE TABLE IF NOT EXISTS clientes (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            nome TEXT NOT NULL,
            email TEXT UNIQUE,
            idade INTEGER
        );
    )";
    resultado_operacao = sqlite3_exec(conexao_db, sql_criar_tabela.c_str(), nullptr, nullptr, &mensagem_erro);
    if (resultado_operacao != SQLITE_OK) {
        std::cerr << "Erro ao criar tabela: " << mensagem_erro << std::endl;
        sqlite3_free(mensagem_erro);
        sqlite3_close(conexao_db);
        return 1;
    }
    std::cout << "Tabela 'clientes' verificada/criada." << std::endl;

    // 3. Inserir dados
    std::string sql_inserir_cliente1 = "INSERT INTO clientes (nome, email, idade) VALUES ('João Silva', 'joao.silva@email.com', 28);";
    resultado_operacao = sqlite3_exec(conexao_db, sql_inserir_cliente1.c_str(), nullptr, nullptr, &mensagem_erro);
    if (resultado_operacao != SQLITE_OK) {
        std::cerr << "Erro ao inserir cliente 1: " << mensagem_erro << std::endl;
        sqlite3_free(mensagem_erro);
    } else {
        std::cout << "Cliente 'João Silva' inserido." << std::endl;
    }

    std::string sql_inserir_cliente2 = "INSERT INTO clientes (nome, email, idade) VALUES ('Maria Souza', 'maria.souza@email.com', 35);";
    resultado_operacao = sqlite3_exec(conexao_db, sql_inserir_cliente2.c_str(), nullptr, nullptr, &mensagem_erro);
    if (resultado_operacao != SQLITE_OK) {
        std::cerr << "Erro ao inserir cliente 2: " << mensagem_erro << std::endl;
        sqlite3_free(mensagem_erro);
    } else {
        std::cout << "Cliente 'Maria Souza' inserido." << std::endl;
    }

    // 4. Consultar dados e usar o callback
    std::string sql_consultar = "SELECT id, nome, email, idade FROM clientes WHERE idade > 30;";
    std::cout << "\nClientes com mais de 30 anos:" << std::endl;
    resultado_operacao = sqlite3_exec(conexao_db, sql_consultar.c_str(), callback_exemplo, nullptr, &mensagem_erro);
    if (resultado_operacao != SQLITE_OK) {
        std::cerr << "Erro ao consultar dados: " << mensagem_erro << std::endl;
        sqlite3_free(mensagem_erro);
    }

    // 5. Atualizar dados
    std::string sql_atualizar = "UPDATE clientes SET idade = 29 WHERE nome = 'João Silva';";
    resultado_operacao = sqlite3_exec(conexao_db, sql_atualizar.c_str(), nullptr, nullptr, &mensagem_erro);
    if (resultado_operacao != SQLITE_OK) {
        std::cerr << "Erro ao atualizar cliente: " << mensagem_erro << std::endl;
        sqlite3_free(mensagem_erro);
    } else {
        std::cout << "\nIdade de 'João Silva' atualizada." << std::endl;
    }

    // 6. Consultar todos os dados para verificar a atualização
    sql_consultar = "SELECT id, nome, email, idade FROM clientes;";
    std::cout << "\nTodos os clientes após atualização:" << std::endl;
    resultado_operacao = sqlite3_exec(conexao_db, sql_consultar.c_str(), callback_exemplo, nullptr, &mensagem_erro);
    if (resultado_operacao != SQLITE_OK) {
        std::cerr << "Erro ao consultar dados após atualização: " << mensagem_erro << std::endl;
        sqlite3_free(mensagem_erro);
    }

    // 7. Fechar o banco de dados
    sqlite3_close(conexao_db);
    std::cout << "\nBanco de dados fechado." << std::endl;

    return 0;
}

Compilação

Para compilar o projeto, execute os seguintes comandos no diretório raiz do seu projeto:

mkdir -p build && cd build
cmake ..
make

Isso gerará o executável app\_sqlite no diretório build.

Fundamentos de SQL no SQLite

SQLite é compatível com a maior parte do padrão SQL-92, mas possui algumas particularidades. Compreender os comandos SQL básicos é essencial para interagir com o banco de dados.

Instalação da Ferramenta CLI (Command Line Interface)

Para experimentar comandos SQL diretamente, você pode instalar a ferramenta CLI do SQLite em sistemas baseados em Debian/Ubuntu:

sudo apt install sqlite3

Para abrir um banco de dados (ou criar um novo se não existir):

sqlite3 meu_banco_de_testes.db

Dentro do shell do SQLite, os comandos SQL são terminados por um ponto e vírgula (;).

Comandos SQL Essenciais

1. Criação de Tabelas (CREATE TABLE)

Tabelas são as estruturas fundamentais para organizar dados em um banco de dados relacional. Cada tabela possui um nome e uma coleção de colunas, cada uma com seu tipo de dado.

CREATE TABLE IF NOT EXISTS produtos (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    nome TEXT NOT NULL,
    preco REAL,
    estoque INTEGER DEFAULT 0
);

  • IF NOT EXISTS: Garante que a tabela só será criada se ainda não existir.
  • INTEGER PRIMARY KEY AUTOINCREMENT: Define id como chave primária, um inteiro que será automaticamente incrementado.
  • TEXT NOT NULL: nome é uma coluna de texto que não pode ser nula.
  • REAL: Tipo de dado para números de ponto flutuante.
  • INTEGER DEFAULT 0: estoque é um inteiro com valor padrão de 0.

2. Inserção de Dados (INSERT INTO)

Para adicionar novas linhas (registros) a uma tabela:

INSERT INTO produtos (nome, preco, estoque) VALUES ('Notebook', 1500.00, 10);
INSERT INTO produtos (nome, preco, estoque) VALUES ('Mouse', 25.50, 50);

Se você não especificar as colunas, precisará fornecer valores para todas as colunas na ordem em que foram definidas:

INSERT INTO produtos VALUES (NULL, 'Teclado', 75.00, 30); -- NULL para id AUTOINCREMENT

3. Consulta de Dados (SELECT)

Para recuperar dados de uma ou mais tabelas:

SELECT * FROM produtos; -- Seleciona todas as colunas e todas as linhas

Para selecionar colunas específicas e filtrar resultdaos:

SELECT nome, preco FROM produtos WHERE preco > 50.00 ORDER BY preco DESC;

  • WHERE: Filtra as linhas com base em uma condição.
  • ORDER BY: Classifica os resultados.

4. Atualização de Dados (UPDATE)

Para modificar dados existentes em uma ou mais linhas:

UPDATE produtos SET estoque = 15 WHERE nome = 'Notebook';
UPDATE produtos SET preco = preco * 1.05 WHERE id = 2; -- Aumenta o preço em 5%

A cláusula WHERE é crucial para especificar quais registros serão atualiazdos. Sem ela, todos os registros da tabela seriam modificados.

5. Exclusão de Dados (DELETE FROM)

Para remover linhas de uma tabela:

DELETE FROM produtos WHERE estoque < 5;

Para remover todas as linhas de uma tabela (mantendo a estrutura da tabela):

DELETE FROM produtos;

Atenção ao usar DELETE FROM sem a cláusula WHERE, pois isso apaga todos os dados da tabela.

Você pode combinar condições com AND ou OR na cláusula WHERE:

DELETE FROM produtos WHERE preco < 30.00 AND estoque = 0;

SQLiteCpp: Um Wrapper C++ Amigável

Embora as APIs C do SQLite sejam robustas, trabalhar diretamente com ponteiros, char\*\* e callbacks pode ser tedioso em projetos C++. SQLiteCpp é um wrapper C++ de código aberto que oferece uma interface mais idiomática e segura, utilizando classes e exceções C++.

Obtenção do SQLiteCpp

O SQLiteCpp está disponível no GitHub e pode ser incluído como um submódulo Git em seu projeto:

git clone https://github.com/SRombauts/SQLiteCpp.git
cd SQLiteCpp
git submodule init
git submodule update

Configuração CMake para SQLiteCpp

Para integrar SQLiteCpp ao seu projeto CMake, você pode adicioná-lo como um subdiretório:

cmake_minimum_required(VERSION 3.10 FATAL_ERROR)
project(SQLiteCppDemo LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# Adicionar SQLiteCpp como um subdiretório
# Isso adicionará a biblioteca SQLiteCpp ao seu projeto CMake
add_subdirectory(SQLiteCpp)

# Incluir o diretório de headers do SQLiteCpp
include_directories(SQLiteCpp/include)

# Criar o executável principal
add_executable(app_with_wrapper main_wrapper.cpp)

# Vincular o executável com a biblioteca SQLiteCpp
# SQLiteCpp já gerencia a vinculação com a biblioteca C do SQLite internamente
target_link_libraries(app_with_wrapper PRIVATE SQLiteCpp)

Exemplo de Uso do SQLiteCpp (main\_wrapper.cpp)

#include <iostream>
#include <SQLiteCpp/SQLiteCpp.h> // Header principal do SQLiteCpp

int main() {
    try {
        // Abrir um arquivo de banco de dados no modo de leitura/escrita, criando-o se não existir
        // SQLite::OPEN_READWRITE | SQLite::OPEN_CREATE são flags de modo
        SQLite::Database db("dados_clientes.db", SQLite::OPEN_READWRITE | SQLite::OPEN_CREATE);
        std::cout << "Arquivo de banco de dados '" << db.getFilename() << "' aberto com sucesso." << std::endl;

        // Remover a tabela "cadastro" se ela existir, e depois criá-la
        db.exec("DROP TABLE IF EXISTS cadastro");
        db.exec("CREATE TABLE cadastro (id INTEGER PRIMARY KEY AUTOINCREMENT, nome TEXT NOT NULL, idade INTEGER)");
        std::cout << "Tabela 'cadastro' criada." << std::endl;

        // Inserir registros
        int registros_afetados;
        registros_afetados = db.exec("INSERT INTO cadastro (nome, idade) VALUES ('Ana', 25)");
        std::cout << "Inserido 'Ana', registros afetados: " << registros_afetados << std::endl;

        registros_afetados = db.exec("INSERT INTO cadastro (nome, idade) VALUES ('Bruno', 30)");
        std::cout << "Inserido 'Bruno', registros afetados: " << registros_afetados << std::endl;

        // Atualizar um registro
        registros_afetados = db.exec("UPDATE cadastro SET idade = 31 WHERE nome = 'Bruno'");
        std::cout << "Atualizado 'Bruno', registros afetados: " << registros_afetados << std::endl;

        // Consultar e exibir os resultados usando SQLite::Statement
        SQLite::Statement query(db, "SELECT id, nome, idade FROM cadastro");
        std::cout << "\nDados da tabela 'cadastro':" << std::endl;
        while (query.executeStep()) { // Itera por cada linha do resultado
            int id_cliente = query.getColumn(0);
            std::string nome_cliente = query.getColumn(1);
            int idade_cliente = query.getColumn(2);
            std::cout << "ID: " << id_cliente << ", Nome: " << nome_cliente << ", Idade: " << idade_cliente << std::endl;
        }

        // Deletar um registro
        registros_afetados = db.exec("DELETE FROM cadastro WHERE nome = 'Ana'");
        std::cout << "\nDeletado 'Ana', registros afetados: " << registros_afetados << std::endl;

        // Consultar novamente para verificar a exclusão
        SQLite::Statement query_pos_delete(db, "SELECT nome FROM cadastro");
        std::cout << "Registros restantes na tabela 'cadastro':" << std::endl;
        while (query_pos_delete.executeStep()) {
            std::cout << "- " << query_pos_delete.getColumn(0).getText() << std::endl;
        }

        db.exec("DROP TABLE cadastro"); // Excluir a tabela no final
        std::cout << "\nTabela 'cadastro' excluída." << std::endl;
    } catch (std::exception& e) {
        // Tratamento de exceções para erros de banco de dados
        std::cerr << "Exceção SQLite: " << e.what() << std::endl;
        return EXIT_FAILURE;
    }

    return 0;
}

Tags: sqlite3 C++ C database SQL

Publicado em 6-16 21:36 por Thomas