MongoDB representa uma abordagem radicalmente diferente ao armazenamento de dados. Enquanto bancos relacionais organizam dados em tabelas com linhas e colunas de schema fixo, o MongoDB organiza dados em coleções de documentos no formato BSON (Binary JSON), sem schema obrigatório. Cada documento pode ter campos diferentes dos demais na mesma coleção.
Isso não significa que o MongoDB é "melhor" que o MySQL — é diferente. Ele brilha em cenários específicos: dados com estrutura variável ou que mudam com frequência, catálogos de produtos com atributos heterogêneos, logs e eventos, e aplicações onde a estrutura de dados evolui rápido. Entender quando usá-lo (e quando não usá-lo) é tão importante quanto saber usá-lo.
Instalação
MongoDB no Linux:
# Instalar MongoDB
sudo apt install mongodb-org
# Instalar a extensão PHP
sudo pecl install mongodb
echo "extension=mongodb.so" | sudo tee -a /etc/php/8.x/cli/php.ini
# Instalar a biblioteca via Composer
composer require mongodb/mongodb
Conceitos fundamentais
Antes de escrever código, alinhe o vocabulário:
| Relacional | MongoDB |
|---|---|
| Banco de dados | Database |
| Tabela | Collection |
| Linha | Document |
| Coluna | Field |
| JOIN | $lookup (aggregation) |
| PRIMARY KEY | _id (ObjectId automático) |
| INDEX | Index (mesma ideia) |
| Schema fixo | Schema flexível |
Conexão e operações básicas
<?php
declare(strict_types=1);
require 'vendor/autoload.php';
use MongoDB\Client;
use MongoDB\BSON\ObjectId;
use MongoDB\BSON\UTCDateTime;
$client = new Client('mongodb://localhost:27017');
// Seleciona o banco e a coleção
// Se não existirem, são criados automaticamente na primeira operação
$db = $client->loja;
$produtos = $db->produtos;
// INSERT — insertOne() retorna InsertOneResult
$resultado = $produtos->insertOne([
'nome' => 'Teclado Mecânico Keychron K2',
'preco' => 299.90,
'estoque' => 15,
'ativo' => true,
'tags' => ['periférico', 'teclado', 'mecânico'],
'fabricante' => [ // documento embutido (embedded document)
'nome' => 'Keychron',
'pais' => 'China',
'site' => 'keychron.com',
],
'variacoes' => [ // array de documentos embutidos
['cor' => 'preto', 'sku' => 'K2-BLK', 'estoque' => 8],
['cor' => 'cinza', 'sku' => 'K2-GRY', 'estoque' => 7],
],
'criado_em' => new UTCDateTime(),
]);
$id = $resultado->getInsertedId(); // ObjectId
echo "Inserido com ID: {$id}\n";
// INSERT em lote — insertMany()
$produtos->insertMany([
['nome' => 'Mouse Logitech MX Master 3', 'preco' => 499.90, 'tags' => ['periférico', 'mouse']],
['nome' => 'Monitor LG 27" 4K', 'preco' => 2899.00, 'tags' => ['monitor', 'display']],
]);
Consultas — find e findOne
<?php
// findOne() retorna um documento ou null
$produto = $produtos->findOne(['nome' => 'Teclado Mecânico Keychron K2']);
echo $produto['nome'] . ": R$ " . $produto['preco'] . "\n";
// Acessando documento embutido
echo "Fabricante: " . $produto['fabricante']['nome'] . "\n";
// find() retorna um Cursor — itere normalmente
$cursor = $produtos->find(['ativo' => true]);
foreach ($cursor as $p) {
echo "- {$p['nome']}: R$ {$p['preco']}\n";
}
// Operadores de comparação
$caros = $produtos->find([
'preco' => ['$gte' => 500], // maior ou igual a 500
'ativo' => true,
]);
// Múltiplas condições — AND implícito
$cursor = $produtos->find([
'preco' => ['$gte' => 100, '$lte' => 1000],
'estoque' => ['$gt' => 0],
]);
// OR explícito
$cursor = $produtos->find([
'$or' => [
['tags' => 'teclado'],
['tags' => 'mouse'],
]
]);
// Busca em array — o MongoDB verifica se o array contém o valor
$cursor = $produtos->find(['tags' => 'mecânico']);
// Busca em documento embutido com notação de ponto
$cursor = $produtos->find(['fabricante.nome' => 'Keychron']);
// Projeção — escolher quais campos retornar (1 = incluir, 0 = excluir)
$cursor = $produtos->find(
filter: ['ativo' => true],
options: [
'projection' => ['nome' => 1, 'preco' => 1, '_id' => 0],
'sort' => ['preco' => -1], // -1 = decrescente
'limit' => 10,
'skip' => 0,
]
);
Atualização e deleção
<?php
// updateOne() — atualiza o primeiro documento que corresponde ao filtro
$produtos->updateOne(
['_id' => new ObjectId($id)],
['$set' => ['preco' => 279.90, 'atualizado_em' => new UTCDateTime()]]
);
// $inc — incrementa um valor
$produtos->updateOne(
['_id' => new ObjectId($id)],
['$inc' => ['estoque' => -1]] // decrementa estoque em 1
);
// $push — adiciona ao array
$produtos->updateOne(
['_id' => new ObjectId($id)],
['$push' => ['tags' => 'bluetooth']]
);
// $pull — remove do array
$produtos->updateOne(
['_id' => new ObjectId($id)],
['$pull' => ['tags' => 'bluetooth']]
);
// updateMany() — atualiza todos que correspondem
$produtos->updateMany(
['estoque' => 0],
['$set' => ['ativo' => false]]
);
// findOneAndUpdate() — atualiza e retorna o documento
$atualizado = $produtos->findOneAndUpdate(
['sku' => 'K2-BLK'],
['$inc' => ['estoque' => -1]],
['returnDocument' => \MongoDB\Operation\FindOneAndUpdate::RETURN_DOCUMENT_AFTER]
);
// deleteOne() e deleteMany()
$produtos->deleteOne(['_id' => new ObjectId($id)]);
$produtos->deleteMany(['ativo' => false, 'estoque' => 0]);
Aggregation Pipeline — o poder do MongoDB
O framework de agregação do MongoDB é equivalente a GROUP BY, JOIN e funções de janela do SQL, mas com uma sintaxe de pipeline:
<?php
$pipeline = [
// $match — filtra documentos (como WHERE)
['$match' => ['ativo' => true]],
// $unwind — "explode" um array em documentos separados
['$unwind' => '$tags'],
// $group — agrupa e acumula (como GROUP BY)
['$group' => [
'_id' => '$tags',
'total_produtos'=> ['$sum' => 1],
'preco_medio' => ['$avg' => '$preco'],
'preco_min' => ['$min' => '$preco'],
'preco_max' => ['$max' => '$preco'],
]],
// $sort — ordena o resultado
['$sort' => ['total_produtos' => -1]],
// $limit
['$limit' => 10],
];
$resultado = $produtos->aggregate($pipeline);
foreach ($resultado as $item) {
echo "Tag: {$item['_id']}, Produtos: {$item['total_produtos']}, "
. "Preço médio: R$ " . number_format($item['preco_medio'], 2, ',', '.') . "\n";
}
Índices
<?php
// Índice simples
$produtos->createIndex(['nome' => 1]);
// Índice composto
$produtos->createIndex(['ativo' => 1, 'preco' => -1]);
// Índice de texto para busca full-text
$produtos->createIndex(['nome' => 'text', 'descricao' => 'text']);
// Busca textual
$cursor = $produtos->find(['$text' => ['$search' => 'teclado mecânico']]);
// Índice em campo de documento embutido
$produtos->createIndex(['fabricante.nome' => 1]);
// TTL index — documentos expiram automaticamente (ótimo para logs/sessões)
$db->sessoes->createIndex(
['criado_em' => 1],
['expireAfterSeconds' => 3600] // expira após 1 hora
);
Quando usar MongoDB vs banco relacional
| Use MongoDB quando... | Use banco relacional quando... |
|---|---|
| Schema varia entre documentos | Schema é bem definido e estável |
| Dados hierárquicos naturais | Dados com muitos relacionamentos |
| Escrita e leitura em alta escala | Consistência ACID é crítica |
| Prototipagem rápida | Relatórios complexos e JOINs |
| Catálogos com atributos variáveis | Transações financeiras |
Exercício da semana
-
Crie uma coleção
artigosde um blog: título, conteúdo, autor (documento embutido com nome e email), tags (array), publicado_em, rascunho (boolean). Insira 5 artigos. -
Escreva queries para: buscar todos os artigos publicados de um autor específico, buscar artigos por tag, buscar artigos com mais de 500 palavras no conteúdo (usando
$wherecom$expr). -
Crie um pipeline de agregação que retorne: por autor, o número de artigos publicados, a data do artigo mais recente e as 3 tags mais usadas.
-
Implemente um TTL index na coleção
tokens_recuperacaoonde cada token expira em 2 horas (expireAfterSeconds: 7200). -
Desafio: implemente um
ArticleRepositorycom interface idêntica aoArticleRepositoryPDOque usa bancos relacionais. Ambos devem implementarArticleRepositoryInterface. Escreva testes que rodem contra ambas as implementações.
Referências
- Documentação oficial MongoDB: https://www.mongodb.com/docs/
- Driver PHP oficial: https://www.mongodb.com/docs/drivers/php/
- Biblioteca PHP MongoDB no Packagist: https://packagist.org/packages/mongodb/mongodb
- MongoDB University (cursos gratuitos): https://learn.mongodb.com/