PHP

SQLite com PHP Já leu

7 min de leitura

SQLite com PHP
SQLite é o banco de dados mais instalado do mundo — está presente em todos os smartphones Android e iPhone, em todos os navegadores, no sistema operacional macOS, em centenas de milhões de dispositivos embarcados. Mas na

SQLite é o banco de dados mais instalado do mundo — está presente em todos os smartphones Android e iPhone, em todos os navegadores, no sistema operacional macOS, em centenas de milhões de dispositivos embarcados. Mas na web com PHP, é frequentemente ignorado ou mal compreendido.

SQLite não é um banco de dados "de brinquedo". É uma escolha deliberada e poderosa para cenários específicos: aplicações de pequeno a médio porte com tráfego moderado, ferramentas de linha de comando, ambientes de teste, cache local, e qualquer situação onde você não queira (ou não possa) manter um servidor de banco de dados separado.


O que é o SQLite e como ele funciona

Ao contrário do MySQL e PostgreSQL, que são sistemas cliente-servidor, o SQLite é uma biblioteca que se incorpora diretamente à sua aplicação. O banco de dados inteiro é um único arquivo em disco. Não há servidor para iniciar, não há configuração de usuários e senhas, não há processo separado.

Isso traz consequências importantes:

Vantagens: zero configuração, portabilidade total (copie o arquivo e leve o banco junto), perfeito para desenvolvimento e testes, funciona em qualquer hospedagem sem dependências externas.

Limitações: escritas concorrentes são serialized (apenas uma escrita por vez, embora o WAL mode melhore isso), não é adequado para aplicações com alta concorrência de escrita, não suporta alguns tipos de dados avançados.


Criando e conectando ao banco

<?php
declare(strict_types=1);

// O banco é criado automaticamente se não existir
// Use :memory: para um banco em memória (ideal para testes)
$pdo = new PDO('sqlite:banco.db', options: [
    PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);

// Ativar WAL mode: melhora performance de leitura concorrente
$pdo->exec('PRAGMA journal_mode = WAL');

// Ativar foreign keys (desativado por padrão no SQLite!)
$pdo->exec('PRAGMA foreign_keys = ON');

// Ajustar tamanho do cache
$pdo->exec('PRAGMA cache_size = -64000'); // 64MB

echo "SQLite " . $pdo->query('SELECT sqlite_version()')->fetchColumn() . "\n";

O detalhe do PRAGMA foreign_keys = ON é crítico e frequentemente esquecido. Por padrão, o SQLite não verifica integridade referencial — chaves estrangeiras são aceitas mas não validadas. Sempre ative esse pragma na abertura da conexão.


Schema e tipos de dados

O SQLite usa um sistema de tipos chamado "type affinity" — diferente de outros bancos, ele é bastante flexível (você pode inserir um texto numa coluna INTEGER). Na prática, use os tipos normalmente e o SQLite fará o melhor:

<?php

$pdo->exec("
    CREATE TABLE IF NOT EXISTS notas (
        id          INTEGER PRIMARY KEY AUTOINCREMENT,
        titulo      TEXT    NOT NULL,
        conteudo    TEXT,
        criada_em   TEXT    NOT NULL DEFAULT (datetime('now')),
        atualizada_em TEXT,
        arquivada   INTEGER NOT NULL DEFAULT 0  -- 0 = false, 1 = true
    )
");

$pdo->exec("
    CREATE TABLE IF NOT EXISTS tags (
        id   INTEGER PRIMARY KEY AUTOINCREMENT,
        nome TEXT NOT NULL UNIQUE
    )
");

$pdo->exec("
    CREATE TABLE IF NOT EXISTS notas_tags (
        nota_id INTEGER NOT NULL REFERENCES notas(id) ON DELETE CASCADE,
        tag_id  INTEGER NOT NULL REFERENCES tags(id)  ON DELETE CASCADE,
        PRIMARY KEY (nota_id, tag_id)
    )
");

// Índice para busca por texto
$pdo->exec("CREATE INDEX IF NOT EXISTS idx_notas_titulo ON notas(titulo)");

Full-Text Search — busca de texto com FTS5

Uma das joias do SQLite é o módulo FTS5 (Full-Text Search version 5), que permite busca textual eficiente sem precisar de Elasticsearch ou Typesense para casos simples:

<?php

// Criando tabela virtual de FTS
$pdo->exec("
    CREATE VIRTUAL TABLE IF NOT EXISTS notas_fts
    USING fts5(titulo, conteudo, content='notas', content_rowid='id')
");

// Trigger para manter o índice atualizado
$pdo->exec("
    CREATE TRIGGER IF NOT EXISTS notas_fts_insert
    AFTER INSERT ON notas BEGIN
        INSERT INTO notas_fts(rowid, titulo, conteudo)
        VALUES (new.id, new.titulo, new.conteudo);
    END
");

$pdo->exec("
    CREATE TRIGGER IF NOT EXISTS notas_fts_delete
    AFTER DELETE ON notas BEGIN
        INSERT INTO notas_fts(notas_fts, rowid, titulo, conteudo)
        VALUES ('delete', old.id, old.titulo, old.conteudo);
    END
");

// Inserindo e buscando
$pdo->prepare("INSERT INTO notas (titulo, conteudo) VALUES (?, ?)")
    ->execute(['PHP e SQLite', 'SQLite é excelente para aplicações PHP de médio porte']);

$pdo->prepare("INSERT INTO notas (titulo, conteudo) VALUES (?, ?)")
    ->execute(['PostgreSQL avançado', 'JSONB e arrays nativos tornam o Postgres poderoso']);

// Busca FTS com ranking por relevância
$stmt = $pdo->prepare("
    SELECT n.id, n.titulo, n.conteudo,
           bm25(notas_fts) AS relevancia
    FROM notas_fts
    JOIN notas n ON n.id = notas_fts.rowid
    WHERE notas_fts MATCH :termo
    ORDER BY relevancia
    LIMIT 10
");
$stmt->execute([':termo' => 'PHP SQLite']);
$resultados = $stmt->fetchAll();

SQLite para testes — o caso de uso mais valioso

O uso mais poderoso do SQLite em projetos profissionais é como banco de dados para testes automatizados. Em vez de criar um banco MySQL real para os testes (lento, com estado compartilhado entre execuções), use SQLite em memória:

<?php

// Em um TestCase base (PHPUnit)
abstract class DatabaseTestCase extends TestCase
{
    protected PDO $pdo;

    protected function setUp(): void
    {
        // Banco em memória: criado do zero a cada teste, destruído ao final
        $this->pdo = new PDO('sqlite::memory:', options: [
            PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
        ]);
        $this->pdo->exec('PRAGMA foreign_keys = ON');
        $this->migrarSchema();
        $this->inserirDadosDeFixture();
    }

    private function migrarSchema(): void
    {
        // Roda as mesmas migrations do projeto — garantindo paridade
        $sql = file_get_contents(__DIR__ . '/../schema.sql');
        $this->pdo->exec($sql);
    }

    private function inserirDadosDeFixture(): void
    {
        $this->pdo->exec("
            INSERT INTO usuarios (nome, email) VALUES
            ('Alice', 'alice@teste.com'),
            ('Bob', 'bob@teste.com')
        ");
    }
}

class UsuarioRepositorioTest extends DatabaseTestCase
{
    public function test_buscar_por_email_retorna_usuario_existente(): void
    {
        $repo = new UsuarioRepositorio($this->pdo);
        $usuario = $repo->buscarPorEmail('alice@teste.com');

        $this->assertNotNull($usuario);
        $this->assertSame('Alice', $usuario->nome);
    }

    public function test_buscar_por_email_retorna_null_para_inexistente(): void
    {
        $repo = new UsuarioRepositorio($this->pdo);
        $usuario = $repo->buscarPorEmail('inexistente@teste.com');

        $this->assertNull($usuario);
    }
}

Cada teste começa com um banco limpo, sem afetar outros testes. Muito mais rápido que MySQL ou PostgreSQL em memória real.


Resumo

Característica SQLite
Configuração Zero — um arquivo
Concorrência de escrita Limitada (WAL mode ajuda)
Full-Text Search FTS5 nativo e eficiente
Testes automatizados Ideal — banco em :memory:
Portabilidade Total — o arquivo é o banco
Foreign keys Desativadas por padrão — sempre ative

Exercício da semana

  1. Crie uma aplicação de notas pessoais com SQLite: CRUD completo (criar, listar, editar, arquivar, deletar) com tags. Use PRAGMA foreign_keys = ON e WAL mode.

  2. Implemente FTS5 na tabela de notas. Escreva uma função buscar(string $termo): array que use MATCH e retorne resultados ordenados por relevância (bm25).

  3. Implemente a mesma aplicação usando MySQL (ou PostgreSQL). Crie uma interface NotaRepositorioInterface que ambas as implementações satisfaçam. Escreva testes que rodem contra SQLite :memory: mas garantam que o código funciona.

  4. Desafio: implemente backup automático do SQLite: uma função backup(string $destinoPath): void que usa a API de backup do SQLite via $pdo->sqliteCreateFunction() ou via linha de comando .dump.


Referências

  • Documentação oficial do SQLite: https://www.sqlite.org/docs.html
  • SQLite FTS5: https://www.sqlite.org/fts5.html
  • PHP Manual — PDO_SQLite: https://www.php.net/manual/pt_BR/ref.pdo-sqlite.php
  • Artigo "Consider SQLite" de Ben Johnson: https://litestream.io/alternatives/

Comentários

Mais em PHP

Arrays em Profundidade
Arrays em Profundidade

Arrays s&atilde;o a estrutura de dados mais usada no PHP. Praticamente tudo q...

APIs REST com PHP
APIs REST com PHP

REST (Representational State Transfer) é o estilo arquitetural dominante para...

Query Builder e Migrations
Query Builder e Migrations

No artigo anterior construímos um repositório com SQL escrito à mão. Isso fun...