Bancos de documentos
Dados guardados como documentos aninhados, com esquema flexível e as consultas que isso dificulta.
O código desta página foi executado nas cinco linguagens contra um MongoDB 8.3, num conjunto de réplicas de um nó. As medições do nível Conceito vêm do mesmo banco.
Baixar o estoque sem vender o que não existe, em cada linguagem
A função recebe o produto e a quantidade e faz um único findOneAndUpdate: o filtro exige estoque suficiente, e a alteração usa $inc. Se o filtro não encontra o documento, não havia estoque, e nada foi alterado. O programa dispara 30 compras simultâneas de um produto com estoque 10.
import { MongoClient } from "mongodb";
const cliente = new MongoClient(process.env.MONGODB_URL!, { maxPoolSize: 50 });
const produtos = cliente.db("loja").collection<{ _id: number; estoque: number }>("produtos");
// A condição e a alteração vão juntas: o banco confere e grava num passo só
async function baixarEstoque(produtoId: number, quantidade: number): Promise<boolean> {
const produto = await produtos.findOneAndUpdate(
{ _id: produtoId, estoque: { $gte: quantidade } },
{ $inc: { estoque: -quantidade } },
);
return produto !== null; // null: não havia estoque suficiente
}
await produtos.deleteMany({});
await produtos.insertOne({ _id: 7, estoque: 10 });
const resultados = await Promise.all(Array.from({ length: 30 }, () => baixarEstoque(7, 1)));
const final = await produtos.findOne({ _id: 7 });
console.log(`aceitas: ${resultados.filter(Boolean).length}, estoque final: ${final?.estoque}`);
await cliente.close();import os
from concurrent.futures import ThreadPoolExecutor
from pymongo import MongoClient
cliente = MongoClient(os.environ["MONGODB_URL"], maxPoolSize=50)
produtos = cliente["loja"]["produtos"]
def baixar_estoque(produto_id: int, quantidade: int) -> bool:
# A condição e a alteração vão juntas: o banco confere e grava num passo só
produto = produtos.find_one_and_update(
{"_id": produto_id, "estoque": {"$gte": quantidade}},
{"$inc": {"estoque": -quantidade}},
)
return produto is not None # None: não havia estoque suficiente
produtos.delete_many({})
produtos.insert_one({"_id": 7, "estoque": 10})
with ThreadPoolExecutor(max_workers=30) as executor:
resultados = list(executor.map(lambda _: baixar_estoque(7, 1), range(30)))
final = produtos.find_one({"_id": 7})
print(f"aceitas: {sum(resultados)}, estoque final: {final['estoque']}")package main
import (
"context"
"errors"
"fmt"
"os"
"sync"
"sync/atomic"
"go.mongodb.org/mongo-driver/v2/bson"
"go.mongodb.org/mongo-driver/v2/mongo"
"go.mongodb.org/mongo-driver/v2/mongo/options"
)
// baixarEstoque confere e grava num passo só, no próprio banco
func baixarEstoque(
ctx context.Context, produtos *mongo.Collection, id, quantidade int,
) (bool, error) {
filtro := bson.D{
{Key: "_id", Value: id},
{Key: "estoque", Value: bson.D{{Key: "$gte", Value: quantidade}}},
}
alteracao := bson.D{{Key: "$inc", Value: bson.D{{Key: "estoque", Value: -quantidade}}}}
err := produtos.FindOneAndUpdate(ctx, filtro, alteracao).Err()
if errors.Is(err, mongo.ErrNoDocuments) {
return false, nil // não havia estoque suficiente
}
return err == nil, err
}
func main() {
ctx := context.Background()
cliente, err := mongo.Connect(options.Client().ApplyURI(os.Getenv("MONGODB_URL")))
if err != nil {
panic(err)
}
defer cliente.Disconnect(ctx)
produtos := cliente.Database("loja").Collection("produtos")
produtos.DeleteMany(ctx, bson.D{})
produtos.InsertOne(ctx, bson.D{{Key: "_id", Value: 7}, {Key: "estoque", Value: 10}})
var aceitas atomic.Int64
var wg sync.WaitGroup
for range 30 {
wg.Go(func() {
if ok, _ := baixarEstoque(ctx, produtos, 7, 1); ok {
aceitas.Add(1)
}
})
}
wg.Wait()
var final struct{ Estoque int }
produtos.FindOne(ctx, bson.D{{Key: "_id", Value: 7}}).Decode(&final)
fmt.Printf("aceitas: %d, estoque final: %d\n", aceitas.Load(), final.Estoque)
}import static com.mongodb.client.model.Filters.and;
import static com.mongodb.client.model.Filters.eq;
import static com.mongodb.client.model.Filters.gte;
import static com.mongodb.client.model.Updates.inc;
import com.mongodb.client.MongoClients;
import com.mongodb.client.MongoCollection;
import java.util.ArrayList;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import org.bson.Document;
public class Estoque {
// A condição e a alteração vão juntas: o banco confere e grava num passo só
static boolean baixarEstoque(MongoCollection<Document> produtos, int id, int quantidade) {
var produto = produtos.findOneAndUpdate(
and(eq("_id", id), gte("estoque", quantidade)),
inc("estoque", -quantidade));
return produto != null; // null: não havia estoque suficiente
}
public static void main(String[] args) throws Exception {
try (var cliente = MongoClients.create(System.getenv("MONGODB_URL"))) {
var produtos = cliente.getDatabase("loja").getCollection("produtos");
produtos.deleteMany(new Document());
produtos.insertOne(new Document("_id", 7).append("estoque", 10));
var resultados = new ArrayList<Future<Boolean>>();
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < 30; i++) {
resultados.add(executor.submit(() -> baixarEstoque(produtos, 7, 1)));
}
}
int aceitas = 0;
for (var r : resultados) if (r.get()) aceitas++;
var finalDoc = produtos.find(eq("_id", 7)).first();
System.out.println("aceitas: " + aceitas
+ ", estoque final: " + finalDoc.getInteger("estoque"));
}
}
}using MongoDB.Bson;
using MongoDB.Driver;
var cliente = new MongoClient(Environment.GetEnvironmentVariable("MONGODB_URL"));
var produtos = cliente.GetDatabase("loja").GetCollection<BsonDocument>("produtos");
// A condição e a alteração vão juntas: o banco confere e grava num passo só
async Task<bool> BaixarEstoque(int id, int quantidade)
{
var filtro = Builders<BsonDocument>.Filter.Eq("_id", id)
& Builders<BsonDocument>.Filter.Gte("estoque", quantidade);
var alteracao = Builders<BsonDocument>.Update.Inc("estoque", -quantidade);
var produto = await produtos.FindOneAndUpdateAsync(filtro, alteracao);
return produto is not null; // null: não havia estoque suficiente
}
await produtos.DeleteManyAsync(FilterDefinition<BsonDocument>.Empty);
await produtos.InsertOneAsync(new BsonDocument { { "_id", 7 }, { "estoque", 10 } });
var resultados = await Task.WhenAll(Enumerable.Range(0, 30).Select(_ => BaixarEstoque(7, 1)));
var final = await produtos.Find(new BsonDocument("_id", 7)).FirstAsync();
Console.WriteLine($"aceitas: {resultados.Count(r => r)}, estoque final: {final["estoque"]}");O resultado, nas cinco linguagens
| Driver | Compras aceitas | Estoque final | |
|---|---|---|---|
| TypeScript | mongodb 7.9 | 10 | 0 |
| Python | pymongo 4.20 | 10 | 0 |
| Go | mongo-driver/v2 2.10 | 10 | 0 |
| Java | mongodb-driver-sync 5.5 | 10 | 0 |
| C# | MongoDB.Driver 3.14 | 10 | 0 |
No teste do nível Conceito, a forma "ler, conferir e gravar" aceitou 30 compras e terminou com estoque 9.
O que muda de uma linguagem para outra
| Montar o filtro | "Nenhum documento" | |
|---|---|---|
| TypeScript | Objeto literal { _id, estoque: { $gte } } | null |
| Python | Dicionário | None |
| Go | bson.D com bson.E ordenados | mongo.ErrNoDocuments |
| Java | Filters.and, eq e gte | null |
| C# | Builders<T>.Filter, combinados com & | null |
Por padrão, o findOneAndUpdate devolve o documento antes da alteração. Para receber o documento já alterado, use a opção de retorno do documento novo (returnDocument: "after" no Node, ReturnDocument.AFTER no PyMongo e no Java, ReturnDocument.After no C#, SetReturnDocument(options.After) no Go). Quando só importa saber se alterou, updateOne com o mesmo filtro e conferir modifiedCount também serve.
Validação de esquema
A regra testada, criada no mongosh:
db.createCollection("pedidos", {
validator: {
$jsonSchema: {
bsonType: "object",
required: ["total", "status"],
properties: {
total: { bsonType: ["int", "double", "decimal"], minimum: 0 },
status: { enum: ["aberto", "pago", "enviado"] },
},
},
},
});Com ela, { total: "120", status: "aberto" } foi recusado com o erro 121 (DocumentValidationFailure), e os detalhes do erro apontaram o campo total. O pedido com total: 120 foi aceito. Para uma coleção que já existe, use collMod com o mesmo validator, e comece com validationAction: "warn" para descobrir os documentos fora do padrão.
Os documentos antigos com o tipo errado aparecem com $type:
db.pedidos.aggregate([{ $group: { _id: { $type: "$total" }, n: { $sum: 1 } } }]);
// no teste: [{ _id: "int", n: 1000 }, { _id: "string", n: 200 }]Índice em campo aninhado
db.pedidos.createIndex({ "itens.produto_id": 1 });
db.pedidos.find({ "itens.produto_id": 4242 }).explain("executionStats");
// no teste, com 100 mil pedidos: 60 documentos lidos e 1 ms (antes: 100 mil e 81 ms)Lista de revisão
Modelagem
- Os documentos foram desenhados a partir das leituras mais frequentes?
- As listas embutidas têm tamanho limitado?
- Dados compartilhados copiados nos documentos têm um plano para quando mudarem?
- Os documentos têm um campo de versão do esquema, se as formas vão conviver?
Esquema
- Há validação com
$jsonSchemana coleção, além da validação na aplicação? - Os documentos antigos foram conferidos com
$typeantes de ligar a validação?
Consistência
- Alterações com condição usam o filtro e operadores no mesmo comando, e não ler, conferir e gravar?
- Operações em vários documentos usam transação, e são a exceção?
- Unicidades têm índice único?
Consultas
- Os campos consultados, inclusive os aninhados, têm índice?
- As consultas principais foram conferidas com
explain("executionStats")?
Prompts para aprofundar
Ao abrir no ChatGPT ou no Claude, o prompt é enviado ao serviço escolhido pela URL e também fica copiado na sua área de transferência.
Implementar operações atômicas
Quero implementar as operações de negócio do meu sistema num banco de documentos sem perder atualizações. Pergunte qual banco, linguagem e driver eu uso, e quais operações alteram dados com alguma condição. Vamos em etapas: 1. Reescrever cada "ler, conferir e gravar" como atualização condicional com operadores. 2. Tratar o caso em que a condição não é satisfeita. 3. Usar transação entre documentos onde for inevitável. 4. Um teste com dezenas de operações simultâneas para cada uma. 5. Índices únicos onde há regras de unicidade.
Validar o esquema de uma coleção existente
Quero ligar a validação de esquema numa coleção que já tem dados. Pergunte qual banco eu uso e como os documentos são hoje. Me guie: 1. Escrever o $jsonSchema a partir dos documentos atuais. 2. Encontrar os documentos fora do padrão com $type e com o próprio esquema no filtro. 3. Corrigir os documentos antigos em lotes. 4. Ligar a validação em modo de aviso, acompanhar e depois passar para recusa. 5. Manter o esquema no repositório, junto com o código.
Revisar uma modelagem de documentos
Vou colar abaixo exemplos de documentos de um sistema e o código que os grava e consulta. Revise a modelagem. Verifique, e para cada item diga se está correto, incorreto ou ausente, citando o trecho: 1. Há listas embutidas que crescem sem limite? 2. Os dados compartilhados copiados têm um plano para quando mudarem? 3. Há validação de esquema? 4. Há ler, conferir e gravar em vez de atualização condicional? 5. Os campos consultados têm índice? 6. Há junções ($lookup) ou transações em excesso, sinal de dados relacionais? Para cada problema, descreva o efeito em produção e mostre a correção. Documentos e código: [cole aqui]