MongoDB: Modelos do Mongoose e projeto de esquemas
Última atualização: 2026-08-26
O Mongoose Schema é o ODM do MongoDB mais popular no ecossistema do Node.js — dominar o projeto de esquemas é a base para a criação de aplicativos robustos.
Este curso oferece uma visão geral sistemática dos tipos de campo do Mongoose Schema, da diferença entre modelos e documentos, dos campos virtuais e do middleware.
ODM x Driver: Por que escolher o Mongoose: Existem duas abordagens principais no ecossistema do MongoDB para Node.js — 1. Driver nativo (o pacote mongodb): leve, flexível e sem abstração; ele manipula diretamente documentos BSON e é adequado para cenários com requisitos extremos de desempenho e controle; 2. Mongoose (ODM): oferece recursos avançados, como definição de esquema, conversão automática de tipos, validadores, middleware e populate, tornando-o adequado para o desenvolvimento de aplicativos de negócios. Principais motivos para escolher o Mongoose: 1. Esquema como documento (tipos de campo e restrições autodescritivos); 2. A validação automática bloqueia dados inválidos; 3. O mecanismo de middleware lida com questões transversais (hash de senhas, exclusões temporárias, registro em log); 4. populate substitui $lookup para simplificar consultas de junção. Cenários em que o driver nativo é preferível: 1. Cenários sensíveis ao desempenho (o Mongoose gera sobrecarga de abstração); 2. Esquemas não fixos (logs, dados de IoT); 3. Quando você já possui seu próprio sistema de validação ou middleware.
1. O que você vai aprender
- Esquema do Mongoose: mais de 12 tipos de campo
- A diferença fundamental entre um modelo e um documento
- virtual: Um campo virtual (não armazenado, mas consultável)
- Middleware (ganchos pré e pós)
- Métodos de instância e métodos estáticos
- Herança de esquemas e o mecanismo de plug-ins
2. Uma análise do guia “Introdução ao Mongoose”
mongoose = MongoDB + aprimoramento de ORM:
- O esquema define estruturas de dados (equivalente à DDL)
- O modelo oferece uma API CRUD
- Documentar os métodos de instância (salvar, validar)
- O middleware intercepta as solicitações antes e depois das operações
Camadas de abstração principais do Mongoose: O Mongoose oferece três camadas de abstração — 1. Esquema (definição da estrutura): declara tipos de campos, regras de validação, valores padrão, índices, campos virtuais e middleware; isso equivale ao DDL e às restrições do SQL; 2. Modelo (operações de coleção): compilado a partir do Esquema, fornece métodos de classe como find, create, update e delete; isso é equivalente à interface CRUD do SQL; 3. Documento (instância de documento): um objeto de instância criado pelo Modelo, com métodos de instância como save, validate e remove, além de campos virtuais; isso equivale a um único registro em um ORM. Essas três camadas de abstração permitem que os desenvolvedores interajam com bancos de dados de documentos usando uma abordagem orientada a objetos.
Escolhendo entre o Mongoose e os drivers nativos: Quando usar o Mongoose e quando usar o driver nativo do MongoDB? O Mongoose é adequado para: 1. Lógica de negócios complexa (que exija validação, middleware e campos virtuais); 2. Colaboração em equipe (esquema como documento, com segurança de tipos que reduz erros); 3. Estruturas de dados estáveis (alterações controladas no esquema). O driver nativo é adequado para: 1. Busca pelo desempenho máximo (o encapsulamento de documentos do Mongoose acarreta uma sobrecarga de desempenho de 10 a 20%); 2. Estruturas de dados altamente dinâmicas (esquemas podem, na verdade, limitar a flexibilidade); 3. Operações simples de leitura e gravação (CRUD sem validação, manipulação direta de BSON). Para a maioria dos projetos em Node.js, usar o Mongoose é a escolha certa — a eficiência no desenvolvimento é mais importante do que pequenas diferenças de desempenho.
const mongoose = require('mongoose');
// Connect
await mongoose.connect('mongodb://localhost:27017/shopdb');
// Definition Schema
const UserSchema = new mongoose.Schema({...});
// Create Model
const User = mongoose.model('User', UserSchema);
// Usage Model CRUD
const user = await User.create({...});
graph TB
A[mongoose Schema] --> B[SchemaType<br/>Field Type]
A --> C[Model<br/>Constructor]
A --> D[Document<br/>Examples]
A --> E[virtual<br/>Virtual Fields]
A --> F[middleware<br/>Middleware]
B --> B1[String/Number/Date]
B --> B2[ObjectId/Decimal128]
B --> B3[Mixed/Map/Array]
C --> C1[find/create]
D --> D1[save/validate]
F --> F1[pre/post hooks]
style C fill:#d4edda
style D fill:#cce5ff
3. Tipos de campos do esquema
Explicação do conceito: Um esquema é a camada de definição do Mongoose para as estruturas de documentos do MongoDB. Ele declara o tipo, as regras de validação, os valores padrão e as estratégias de indexação para cada campo. Embora o próprio MongoDB seja sem esquema, o Mongoose impõe a tipagem e a validação na camada de aplicação, proporcionando às aplicações Node.js uma segurança de dados semelhante à dos ORMs tradicionais.
Como funciona: O Mongoose Schema mantém os metadados dos campos (objetos SchemaType) na memória do aplicativo. Quando um documento é criado ou atualizado, o Mongoose realiza a conversão de tipos e a validação campo por campo; valores que não estiverem em conformidade com as regras gerarão a exceção ValidationError antes de save(). O esquema não afeta o armazenamento no MongoDB — quando um documento antigo não possui um novo campo, o Mongoose retorna undefined (que pode ser preenchido com default).
Comportamento implícito do Schema: O Schema apresenta vários comportamentos implícitos que podem passar despercebidos — 1. Todo documento inclui automaticamente _id: ObjectId (mesmo que não esteja declarado no Schema); 2. Por padrão, _id não é incluído na saída de toJSON (a menos que toJSON: {virtuals: true} esteja definido); 3. A conversão de tipos é implícita — passar '123' para um campo Number converte-o automaticamente para 123 (no modo estrito, strict: true); passar null para um campo com required: true falha na validação (null ≠ undefined, e required verifica apenas undefined); 4. Os valores padrão para objetos aninhados devem ser retornados por uma função (padrão: () => ({})); caso contrário, todos os documentos compartilharão a mesma referência (uma armadilha clássica do JavaScript).
graph LR
A[Schema Definition] --> B[SchemaType<br/>Field Metadata]
B --> C[Type Conversion<br/>String/Number/Date...]
B --> D[Validation Rules<br/>required/min/max/enum]
B --> E[Default value<br/>default/immutable]
B --> F[Indexing Strategies<br/>index/unique]
C --> G[Document.save]
D --> G
E --> G
G --> H{Verification Passed?}
H -->|Yes| I[MongoDB insertOne]
H -->|No| J[ValidationError]
style I fill:#d4edda
style J fill:#f8d7da
| Classificação por tipo | Tipo Mongoose | Cenários típicos |
|---|---|---|
| Tipos básicos | String/Número/Booleano/Data | Nome, Preço, Botão de alternância, Carimbo de data/hora |
| Binário | Buffer | Miniaturas de imagens, conteúdo de arquivos |
| Referência | ObjectId + ref | Relação de chave estrangeira (por exemplo, categoryId → Categoria) |
| Valor exato | Decimal128 | Valor em moeda (para evitar erros de ponto flutuante) |
| Aninhamento | Aninhamento de esquemas | Estruturas fixas, como endereços e especificações |
| Matriz | [Tipo] | Lista de tags, coleção de imagens |
| Notícias | Misto/Mapa | Metadados com estrutura incerta, tradução multilíngue |
(1) Mais de 12 tipos de SchemaType
Decisões sobre a seleção do SchemaType: Escolher o SchemaType correto é o primeiro passo na modelagem de dados — uma escolha incorreta pode levar a problemas de qualidade dos dados e riscos de desempenho. Princípios fundamentais: 1. Valores monetários devem usar Decimal128 em vez de Number (para evitar a armadilha da virgula flutuante, em que 0,1 + 0,2 ≠ 0,3); 2. Use ObjectId + ref para relações de chave estrangeira em vez de String (o método populate do Mongoose depende de ObjectId); 3. Use Mixed para campos com estruturas incertas em vez de Object (Mixed permite qualquer valor, enquanto Object pode acionar validações inesperadas); 4. Use Map em vez de Object para um grande número de pares chave-valor (as chaves do Map podem ser de qualquer tipo e suportam forEach e map).
| Tipo | Definição do Mongoose | Tipo BSON | Exemplo |
|---|---|---|---|
| String | String |
String | String |
| Número | Number |
Duplo | Number |
| Booleano | Boolean |
Booleano | Boolean |
| Dados | Date |
Dados | Date |
| Buffer | Buffer |
Binário | Buffer |
| ObjectId | mongoose.Schema.Types.ObjectId |
ObjectId | ObjectId |
| Decimal128 | mongoose.Schema.Types.Decimal128 |
Decimal128 | Decimal128 |
| Mapa | Map |
Objeto | Map |
| Esquema | new mongoose.Schema({...}) |
Objeto | incorporado |
| Matriz | [Type] |
Matriz | [String] |
| Misto | mongoose.Schema.Types.Mixed |
Objeto | Mixed |
Princípios de Design: As definições dos campos do esquema seguem a filosofia de “restrições como documentação” — as opções para cada campo (obrigatório, mínimo, máximo, enumeração, correspondência) não são apenas regras de validação em tempo de execução, mas também declarações explícitas do contrato de dados. Campos bem definidos tornam o próprio esquema um padrão de documentação executável, permitindo que os membros da equipe compreendam as restrições de cada campo sem precisar consultar o Wiki.
Decisões arquitetônicas: A escolha das opções de campos exige encontrar um equilíbrio entre rigor e flexibilidade. Restrições excessivamente rígidas (como um número muito grande de campos obrigatórios) podem prejudicar a capacidade de evolução do sistema — novos campos devem ser opcionais por padrão, com restrições mais rígidas aplicadas somente após a estabilização do sistema. Por outro lado, restrições excessivamente flexíveis levam ao acúmulo de dívida técnica. Estratégia recomendada: aplique restrições rígidas aos campos identificadores essenciais (e-mail, sku); trate os campos auxiliares (apelido, avatar) com mais flexibilidade; e use enums para restringir os campos de status de negócios (função, status).
(2) Opções de definição de campos
const UserSchema = new mongoose.Schema({
email: {
type: String,
required: [true, 'Email is required'],
unique: true,
lowercase: true,
trim: true,
match: [/^\S+@\S+\.\S+$/, 'Invalid email'],
minlength: 5,
maxlength: 100,
index: true
},
age: {
type: Number,
required: true,
min: [0, 'Age cannot be negative'],
max: 150,
default: 18
},
role: {
type: String,
enum: {
values: ['customer', 'admin', 'moderator'],
message: 'Invalid role: {VALUE}'
},
default: 'customer'
},
isActive: {
type: Boolean,
default: true
},
createdAt: {
type: Date,
default: Date.now,
immutable: true // Cannot be modified after creation
}
});
Melhores práticas: Guia para a seleção de tipos de esquema para ambientes de produção — 1. Sempre use Decimal128 para valores monetários em vez de Number; erros de ponto flutuante são inaceitáveis em cálculos financeiros; 2. Use ObjectId + ref para relações de referência em vez de incorporar documentos completos, a fim de evitar redundância de dados e inconsistências nas atualizações; 3. Use Mixed ou Map para metadados com estruturas incertas, mas esteja ciente do risco de falhas na validação; 4. Sempre use o tipo Date com timestamps: true para campos de data, a fim de evitar confusão de fuso horário com datas baseadas em strings.
Configuração de produção das opções de esquema: O objeto de opções de esquema do Mongoose controla o comportamento global — 1. timestamps: true: Gerencia automaticamente os campos createdAt/updatedAt; defina createdAt como immutable: true para evitar modificações acidentais; 2. toJSON: {virtuals: true}: A serialização para JSON inclui campos virtuais (não incluídos por padrão); 3. toJSON: {virtuals: true}: toJSON() também inclui campos virtuais; 4. minimize: false: Não comprime objetos vazios (por padrão, o Mongoose remove campos vazios, o que pode fazer com que campos esperados fiquem ausentes no front-end); 5. strict: true: Rejeita campos não definidos no esquema (ativado por padrão; deve permanecer ativado em produção). Essas opções devem ser definidas no início do projeto, pois modificações posteriores podem afetar os dados e o comportamento existentes.
Prós e contras da incorporação versus referência: Essa é a decisão mais crítica no projeto de esquema do MongoDB. A incorporação armazena dados relacionados dentro do mesmo documento, permitindo que todos os dados sejam recuperados em uma única consulta, mas enfrenta problemas como o aumento excessivo do tamanho do documento (limite de 16 MB) e a complexidade das atualizações. A referência usa o ObjectId para estabelecer relações; ela oferece maior independência dos dados e não tem limites de tamanho, mas requer consultas adicionais populate/$lookup. Critérios de decisão: 1. Os dados são sempre lidos juntos? Sim → Incorporação; 2. Os dados associados crescerão indefinidamente? Sim → Referência; 3. Os dados associados precisam ser atualizados de forma independente? Sim → Referência.
| Dimensão | Inserção | Referenciamento |
|---|---|---|
| Desempenho da consulta | Alto (leitura única) | Baixo (requer preenchimento) |
| Consistência dos dados | Fraca (atualizações redundantes) | Forte (atualizações em um único ponto) |
| Tamanho do documento | ⚠️ Pode ultrapassar 16 MB | ✅ Separado para cada documento |
| Caso de uso | 1:N — Pequeno e fixo | 1:N — Grande ou em crescimento |
▶ Exemplo 1: Aplicação prática de tipos de esquema composto
const ProductSchema = new mongoose.Schema({
// Basic Types
sku: { type: String, required: true, unique: true },
title: { type: String, required: true },
price: { type: mongoose.Schema.Types.Decimal128, required: true },
stock: { type: Number, default: 0, min: 0 },
isActive: { type: Boolean, default: true },
// Date
releaseDate: { type: Date, required: true },
expiryDate: { type: Date },
// Binary
thumbnail: { type: Buffer },
// Quote(Foreign Key)
categoryId: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Category',
required: true
},
// Nested Documents
specs: {
screen: String,
battery: String,
weight: Number
},
// Array
tags: [String],
images: [{
url: String,
alt: String
}],
// Map(Dynamic key-value pairs)
translations: {
type: Map,
of: String
},
// Mixed(Any type)
metadata: mongoose.Schema.Types.Mixed
}, { timestamps: true });
4. Modelar e documentar
Explicação do conceito: Um Modelo é a abstração que o Mongoose faz de uma coleção do MongoDB — trata-se de um construtor (classe) que fornece métodos estáticos, como find, create e updateOne. Um Document é uma instância de um Model que representa um registro no banco de dados e fornece métodos de instância, como save, validate e remove. Compreender a diferença entre Model e Document é a base para o uso correto do Mongoose.
Como funciona: mongoose.model('User', schema) realiza duas ações: (1) Compila o esquema em um construtor de modelo; (2) O registra na conexão do Mongoose e o mapeia para a coleção users (pluralizando automaticamente o nome). new User({...}) cria uma instância de Document; nesse momento, os dados existem apenas na memória e só são gravados no MongoDB quando save() é chamado. User.create({...}) é equivalente a new User() + save().
sequenceDiagram
participant App as Application Code
participant Model as User Model
participant Doc as User Document
participant DB as MongoDB
App->>Model: User.create({email, age})
Model->>Doc: new User(data)
Doc->>Doc: validate()
Doc->>DB: insertOne()
DB-->>Doc: _id, createdAt
Doc-->>App: Back Document
App->>Model: User.find({role: 'admin'})
Model->>DB: find().toArray()
DB-->>Model: Array of original documents
Model->>Doc: hydrate(docs)
Doc-->>App: Document Array
style Model fill:#d4edda
style Doc fill:#cce5ff
(1) Principais diferenças
| Dimensão | Modelo | Documento |
|---|---|---|
| Essência | Construtor (classe) | Instância do modelo |
| Criar | mongoose.model('User', schema) |
new User({...}) ou User.create() |
| Quantidade | 1 por coleção | 1 por documento |
| Método | Métodos estáticos (find, create) | Métodos de instância (save, validate) |
Princípios de projeto do sistema de tipos: O sistema de tipos do Mongoose oferece segurança de tipos na camada de aplicação, recurso que não está disponível nativamente no MongoDB. O objeto SchemaType realiza a conversão de tipos quando um documento é criado (por exemplo, a string “42” é automaticamente convertida para o número 42); se a conversão falhar, é lançada uma exceção CastError. Embora essa conversão implícita seja conveniente, ela também pode mascarar problemas de dados — em ambientes de produção, recomenda-se habilitar o modo estrito strict: true no esquema (habilitado por padrão) para rejeitar campos indefinidos.
Riscos e precauções da conversão implícita de tipos: A conversão implícita de tipos do Mongoose é uma faca de dois gumes — 1. Conveniência: quando o front-end passa {age: "25"}, ele é automaticamente convertido para o valor Number 25, de modo que os desenvolvedores não precisam realizar a conversão manualmente; 2. Riscos: “abc” se torna NaN (CastError) ao ser convertido para um Number, mas se o esquema estiver definido como String e o MongoDB armazenar um Number, a consulta poderá retornar “não encontrado” (devido à incompatibilidade de tipos); 3. Estratégias de prevenção: definir explicitamente os tipos no esquema (não armazenar números onde o esquema especifica String), validar os tipos na camada de aplicação (usando a validação do joi antes que o Mongoose realize a conversão) e sanitizar a entrada na camada de roteamento (remover campos redundantes). A conversão implícita mais perigosa: o campo ObjectId gera uma exceção CastError ao receber uma string hexadecimal com menos de 24 dígitos (por exemplo, quando req.params.id recebe “abc”); o formato ObjectId deve ser validado na camada de roteamento.
(2) Métodos de instância de documento
// === Create Document ===
const user = new User({ email: 'alice@example.com' });
// === Document Properties ===
user.email; // 'alice@example.com'
user._id; // ObjectId
user.createdAt; // Date
user.isNew; // true(Not saved)
// === Document Instance Methods ===
await user.save(); // Save
await user.validate(); // Verification(Do not save)
user.toJSON(); // Convert to JSON
user.toObject(); // Convert to a regular object
user.remove(); // Delete (Obsolete, use deleteOne)
await user.deleteOne(); // Delete(Recommendations)
await user.populate('orders'); // Associative Filling
▶ Exemplo 2: Operações práticas com documentos
Gerenciamento do ciclo de vida do documento: Um documento passa por quatro etapas, desde a criação até a destruição — 1. new User(data) cria uma instância na memória (isNew: verdadeiro, ainda não gravado no banco de dados); 2. await user.save() persiste os dados no MongoDB (acionando o middleware pre-save e a validação); 3. user.property = newValue modifica uma propriedade na memória (marcando o campo modificado com rastreamento de alterações); 4. await user.deleteOne() exclui o documento. Principal diferença: new + save é uma operação em duas etapas (os dados podem ser modificados antes do salvamento), enquanto User.create() é uma operação em uma única etapa (grava diretamente no banco de dados).
// === Create and Save ===
const user = new User({
email: 'alice@example.com',
username: 'alice',
passwordHash: '...'
});
await user.save();
// === Save after making changes ===
user.lastLoginAt = new Date();
user.loginCount += 1;
await user.save();
// === Convert to an object ===
const userObj = user.toObject();
delete userObj.passwordHash;
// === populate Relationship ===
const user = await User.findById(userId).populate({
path: 'orders',
options: { sort: { createdAt: -1 } }
});
5. campos virtuais
Explicação do conceito: virtual é um mecanismo de “campo calculado” fornecido pelo Mongoose — ele define getters e setters no esquema, mas não é persistido no MongoDB. Os campos virtuais são calculados quando um documento é transformado por toJSON() ou toObject(), tornando-os ideais para propriedades derivadas (como fullName = firstName + lastName) e estatísticas de associação (como orderCount).
Como funciona: Um getter virtual é uma função que realiza um cálculo sempre que doc.fullName é acessado. Um setter virtual aceita um valor e o divide em vários campos. Um campo virtual relacional (ref + localField + foreignField) é, essencialmente, uma declaração abreviada para populate(); quando consultado, populate('orders') ainda precisa ser chamado para acionar o preenchimento da associação.
graph TB
A[virtual Field] --> B[Computational<br/>fullName = firstName + lastName<br/>discountedPrice = price * 1-discount]
A --> C[Associative<br/>orders: ref Order<br/>orderCount: count true]
B --> D[Non-persistent<br/>In-Memory Computing]
C --> E[Requires populate<br/>Triggering a Joined Query]
D --> F[toJSONReal-time Output<br/>Needs to be set virtuals true]
E --> F
style D fill:#d4edda
style E fill:#cce5ff
| tipo virtual | Método de definição | É persistente? | Requer preenchimento? |
|---|---|---|---|
| Getter calculado | schema.virtual('x').get(fn) |
Não | Não |
| Definidor computacional | schema.virtual('x').set(fn) |
Não | Não |
| Associativo (Documento) | schema.virtual('x', {ref, localField, foreignField}) |
Não | Sim |
| Associativo (contável) | Igual ao anterior + count: true |
Não | Sim |
// === Definition virtual ===
UserSchema.virtual('fullName').get(function() {
return `${this.firstName} ${this.lastName}`;
});
UserSchema.virtual('isAdult').get(function() {
return this.age >= 18;
});
// === virtual setter(Reverse Settings)===
UserSchema.virtual('fullName').set(function(name) {
const parts = name.split(' ');
this.firstName = parts[0];
this.lastName = parts[1];
});
// === Enable virtual ===
UserSchema.set('toJSON', { virtuals: true });
UserSchema.set('toObject', { virtuals: true });
▶ Exemplo 3: Uso prático de virtual
Características de desempenho do virtual: Um getter virtual é calculado (não armazenado em cache) a cada acesso — se um campo do qual o virtual depende for modificado, o próximo acesso retornará automaticamente o novo valor. Isso significa: 1. Em consultas de lista, chamar um campo virtual em cada Document executará a função de cálculo uma vez; 2. Os campos virtual não podem ser usados na filtragem $match (o MongoDB não reconhece a existência dos campos virtual); 3. Os campos virtual não podem ser usados para classificação (da mesma forma que acima); 4. Quando virtuals: true está habilitado em toJSON, todos os campos virtual são calculados durante a serialização JSON e incluídos na saída.
Escolhendo entre campos virtuais e campos calculados: Quando usar campos virtuais e quando armazenar campos calculados no esquema — 1. Os campos virtuais são adequados quando: o resultado do cálculo depende de campos do documento atual (por exemplo, fullName = firstName + lastName), o resultado não é necessário para consultas, ordenação ou agregações, e o custo do cálculo é baixo (concatenação simples de strings ou operações matemáticas); 2. Os campos armazenados são adequados quando: são necessários para consultas ou ordenação (por exemplo, discountedPrice precisa ser ordenado por preço com desconto), o custo do cálculo é alto (por exemplo, agregações entre coleções) ou é necessária persistência (por exemplo, contagens redundantes como commentCount). Princípio de seleção — “Se precisar ser usado em consultas no MongoDB, deve ser armazenado; se for exibido apenas na camada de aplicação, usar um campo virtual é mais simples.”
// === Calculated Fields ===
ProductSchema.virtual('discountedPrice').get(function() {
if (!this.discount) return this.price;
return this.price * (1 - this.discount);
});
// === Related Fields(Non-persistent)===
UserSchema.virtual('orders', {
ref: 'Order',
localField: '_id',
foreignField: 'userId'
});
// Usage:
const user = await User.findById(userId).populate('orders');
console.log(user.orders); // Array of associated orders
// === Inverse Correlation ===
UserSchema.virtual('orderCount', {
ref: 'Order',
localField: '_id',
foreignField: 'userId',
count: true // Count only the number,Does not return a document
});
const user = await User.findById(userId).populate('orderCount');
console.log(user.orderCount); // 25
6. Middleware
Explicação do conceito: Os middlewares do Mongoose são funções de hook que são executadas automaticamente antes e depois de operações específicas no banco de dados. Os middlewares pré-operação são executados antes de uma operação (por exemplo, hash de senha, limpeza de dados), enquanto os middlewares pós-operação são executados após uma operação (por exemplo, registro de auditoria, notificações push). O middleware é o mecanismo de extensão mais poderoso do Mongoose, permitindo que a lógica de negócios seja dissociada das operações de dados.
Como funciona: O middleware do Mongoose utiliza o “modelo cebola” — várias funções de middleware pre são executadas na ordem em que foram registradas, seguidas pela operação propriamente dita e, por fim, as funções de middleware post são executadas na ordem em que foram registradas. Cada função de middleware pre deve chamar next() ou retornar uma Promise; caso contrário, a operação é suspensa. As funções de middleware post recebem o resultado da operação como parâmetro e não podem modificar o comportamento da operação.
sequenceDiagram
participant App as Application Code
participant Pre1 as pre save #1<br/>Password Hash
participant Pre2 as pre save #2<br/>Email (lowercase)
participant DB as MongoDB
participant Post1 as post save #1<br/>Audit Log
participant Post2 as post save #2<br/>Welcome Email
App->>Pre1: doc.save()
Pre1->>Pre2: next()
Pre2->>DB: insertOne()
DB-->>Post1: Success
Post1->>Post2: next(doc)
Post2-->>App: Back doc
(1) Tipos de middleware
Projeto da cadeia de execução do middleware: O middleware do Mongoose utiliza o modelo “cebola” — as solicitações percorrem o caminho da camada externa para a camada interna, e as respostas percorrem o caminho da camada interna para a camada externa. Os hooks pre são executados sequencialmente na ordem em que foram registrados, e cada um deve chamar next() para passar o controle; após a execução da operação propriamente dita, os hooks post são executados na ordem em que foram registrados. Esse projeto separa naturalmente as responsabilidades: o hash de senhas, a limpeza de dados e o registro de auditoria ocupam, cada um, um middleware separado, sem acoplamento entre eles.
| Tipo | Condição de acionamento | Finalidade |
| pre('save') | Antes de salvar | Hash da senha, carimbo de data/hora |
| post('save') | Após salvar | Registros, Notificações |
| pre('validate') | Pré-validação | Limpeza de dados |
| pre('find') | Antes da consulta | Critérios de filtragem |
| pre('remove') | Antes de excluir | Limpar dados associados |
(2) middleware de pré-salvamento
Melhores práticas: Os três usos mais comuns do middleware pre-save são o hash de senhas (usando isModified para evitar hashes duplicados), a normalização de dados (converter endereços de e-mail para letras minúsculas, cortar strings) e a manutenção de carimbos de data e hora. O ponto-chave é que this se refere à instância atual do Documento; portanto, o middleware pre-save não pode ser acionado durante operações em lote, como Model.updateOne() — para operações em lote, use o middleware Query pre('updateOne') em vez disso.
// === Password Hash(Classic Scenes)===
UserSchema.pre('save', async function(next) {
if (!this.isModified('passwordHash')) return next();
// The password has been changed.,Re-hash
this.passwordHash = await bcrypt.hash(this.passwordHash, 10);
next();
});
// === Timestamp ===
UserSchema.pre('save', function(next) {
this.updatedAt = new Date();
next();
});
(3) middleware de pré-localização
Middleware de consulta vs. middleware de documento: pre find é um middleware de consulta (em que this se refere ao objeto de consulta, e não ao objeto de documento) e é adequado para controlar o comportamento global da consulta — como filtrar automaticamente documentos excluídos, preencher associações por padrão e classificar por padrão. O formato de expressão regular pre(/^find/) corresponde a todas as operações de consulta, incluindo find, findOne e findById, garantindo um comportamento consistente. Observação: o middleware de consulta não pode acessar dados de documentos (pois a consulta ainda não foi executada); ele só pode modificar as condições da consulta.
// === Automatically filter deleted documents ===
UserSchema.pre(/^find/, function(next) {
this.find({ isDeleted: { $ne: true } });
next();
});
// === Automatic populate Relationship ===
UserSchema.pre('find', function(next) {
this.populate('categoryId');
next();
});
// === Default Sort Order ===
UserSchema.pre('find', function(next) {
this.sort({ createdAt: -1 });
next();
});
(4) O middleware “post save”
Projeto de efeitos colaterais no middleware pós-operação: O middleware pós-operação é executado após a conclusão da operação e não pode modificar os dados do documento (que já foram gravados no banco de dados); ele é adequado para acionar efeitos colaterais — como registros de auditoria, notificações push e atualizações de cache. Principais recursos: 1. post_save aceita um parâmetro doc (o documento salvo); 2. this.wasNew determina se a operação é uma criação ou uma atualização; 3. this.modifiedPaths() recupera uma lista dos campos modificados; 4. O middleware de tratamento de erros utiliza a versão de 4 parâmetros (err, doc, next) e foi projetado especificamente para capturar exceções de operação.
// === Send the welcome email after saving ===
UserSchema.post('save', function(doc, next) {
if (this.wasNew) {
sendWelcomeEmail(doc.email);
}
next();
});
// === Record an audit log after saving ===
UserSchema.post('save', function(doc) {
AuditLog.create({
action: 'user.updated',
userId: doc._id,
changes: this.modifiedPaths()
});
});
▶ Exemplo 4: Prática prática com middleware integrado
// === User Schema Middleware ===
UserSchema.pre('save', async function(next) {
// 1. Password Hash
if (this.isModified('passwordHash')) {
this.passwordHash = await bcrypt.hash(this.passwordHash, 10);
}
// 2. Email (lowercase)
if (this.isModified('email')) {
this.email = this.email.toLowerCase();
}
next();
});
UserSchema.pre(/^find/, function(next) {
// By default, deleted users are not returned.
this.find({ isDeleted: { $ne: true } });
next();
});
// === Error-handling middleware ===
UserSchema.post('save', function(error, doc, next) {
if (error.name === 'MongoServerError' && error.code === 11000) {
next(new Error('Email already exists'));
} else {
next(error);
}
});
7. Métodos de instância e métodos estáticos
Explicação do conceito: O Mongoose permite definir dois tipos de métodos personalizados em um esquema: métodos de instância, que estão associados a cada documento e operam sobre documentos individuais (por exemplo, user.comparePassword()); e métodos estáticos, que estão associados a um modelo e operam sobre toda a coleção (por exemplo, User.findByEmail()). Esses dois tipos de métodos permitem que a lógica de negócios seja encapsulada dentro do modelo de dados, seguindo o princípio de design do “modelo robusto”.
Como funciona: Os métodos de instância são definidos por meio de Schema.methods. Em new Model(), o Mongoose vincula o método à cadeia de protótipos do Document e, dentro do método, this aponta para o Document atual. Os métodos estáticos são definidos por meio de Schema.statics e montados no construtor Model; dentro do método, this refere-se ao próprio Model, permitindo chamadas diretas a this.find() e assim por diante.
graph TB
A[Schema Methods] --> B[Instance Methods<br/>Schema.methods]
A --> C[Static Methods<br/>Schema.statics]
B --> D["user.comparePassword(pwd)<br/>this = Currently Document"]
B --> E["user.generateToken()<br/>this = Currently Document"]
B --> F["user.softDelete()<br/>this = Currently Document"]
C --> G["User.findByEmail(email)<br/>this = User Model"]
C --> H["User.findActive()<br/>this = User Model"]
C --> I["User.getStatistics()<br/>this = User Model"]
style B fill:#cce5ff
style C fill:#d4edda
| Critérios de comparação | Métodos de instância | Métodos estáticos |
|---|---|---|
| Definir localização | Schema.methods |
Schema.statics |
| Objeto de anexo | Protótipo de documento | Construtor de modelo |
| isso aponta para | o documento atual | o próprio modelo |
| Método de chamada | doc.method() |
Model.method() |
| Usos típicos | Comparação de senhas, exclusão temporária | Pesquisas condicionais, estatísticas agregadas |
Limites do projeto de métodos: A distinção entre métodos de instância e métodos estáticos segue o Princípio da Responsabilidade Única — 1. Métodos de instância: operam sobre os dados de um único documento (por exemplo, comparePassword para comparação de senha, softDelete para marcação de exclusão, toJSON para saída anônima), sem consultar o banco de dados (ou consultando apenas seus próprios dados associados); 2. Métodos estáticos: operam sobre dados no nível da coleção (por exemplo, findByEmail para consultas entre documentos, getStatistics para estatísticas agregadas e bulkImport para importações em massa) e requerem os recursos de consulta do Modelo. Esbater essas fronteiras leva a confusão no projeto — colocar findByEmail em métodos de instância (já que uma instância deve existir antes que uma busca possa ser realizada, criando uma contradição lógica) ou colocar comparePassword em métodos estáticos (o que exige a passagem tanto do documento quanto da senha, tornando-o redundante).
(1) Métodos de instância
// === Define an instance method ===
UserSchema.methods.comparePassword = async function(candidatePassword) {
return await bcrypt.compare(candidatePassword, this.passwordHash);
};
UserSchema.methods.generateAuthToken = function() {
return jwt.sign({ id: this._id }, process.env.JWT_SECRET, { expiresIn: '7d' });
};
// === Usage ===
const user = await User.findOne({ email: 'alice@example.com' });
const isValid = await user.comparePassword('password123');
const token = user.generateAuthToken();
(2) Métodos estáticos
// === Defining Static Methods ===
UserSchema.statics.findByEmail = function(email) {
return this.findOne({ email: email.toLowerCase() });
};
UserSchema.statics.findActive = function() {
return this.find({ isActive: true });
};
// === Usage ===
const user = await User.findByEmail('ALICE@example.com');
const activeUsers = await User.findActive();
▶ Exemplo 5: Colocando a abordagem integrada em prática
Modelos “gordos” x modelos “enxutos”: O Mongoose defende o design de “modelos gordos” — em que a lógica de negócios é encapsulada nos métodos do Modelo/Documento, de modo que os Controladores simplesmente chamem user.comparePassword() em vez de implementarem bcrypt.compare por conta própria. Vantagens dos modelos pesados: 1. Reutilização de código (vários controladores compartilham o mesmo método); 2. Encapsulamento (o código externo não tem conhecimento de como as senhas são verificadas); 3. Testabilidade (os métodos do modelo podem ser testados unitariamente de forma independente). Com modelos leves, os controladores ficam repletos de código duplicado; alterar uma única regra de validação exige a modificação de N controladores.
// === Complete User Model ===
const UserSchema = new mongoose.Schema({...});
// Instance Methods
UserSchema.methods = {
comparePassword: async function(candidate) {
return await bcrypt.compare(candidate, this.passwordHash);
},
softDelete: async function() {
this.isDeleted = true;
this.deletedAt = new Date();
return await this.save();
}
};
// Static Methods
UserSchema.statics = {
findByEmail: function(email) {
return this.findOne({ email: email.toLowerCase() });
},
getStatistics: async function() {
return await this.aggregate([
{ $group: { _id: '$role', count: { $sum: 1 } } }
]);
}
};
❓ Perguntas Frequentes
P: Se o esquema do Mongoose mudar, preciso migrar o banco de dados? R: Não. O esquema do Mongoose está na camada de aplicação, e o MongoDB não possui esquema. Novos campos são adicionados automaticamente, e os documentos antigos podem não conter alguns campos.
P: Os campos “virtuais” são salvos no banco de dados? R: Não. Os campos “virtuais” são campos calculados e não são salvos. No entanto, é necessário habilitar
virtuals: trueemtoJSONpara que eles sejam retornados na API.
P: O middleware de pré-salvamento pode ser assíncrono? R: Sim. Use
async functionou retorne uma Promise. É necessário chamarnext()ou retornar uma Promise; caso contrário, ele será suspenso.
P: Como faço para lidar com erros no middleware? R: Os erros gerados na fase
prenext(error)e na fasepostnext(error)são encaminhados para o mecanismo de tratamento de erros do Mongoose.
P: Como a herança de esquemas é implementada? R: Usando
discriminators(discriminador):const AdminUser = User.discriminator('admin', AdminSchema), em que todos os discriminadores compartilham o mesmo conjunto.
📖 Resumo
- Tipos de esquema do Mongoose 12+: String/Number/Date/ObjectId/Decimal128/Map/Mixed
- Um
Modelé um construtor (classe), e umDocumenté uma instância - os campos virtuais não são salvos, mas podem ser retornados pelo método toJSON
- Middleware: pré (antes da operação) + pós (depois da operação)
- Métodos de instância: exclusivos de cada Documento (por exemplo, comparePassword)
- Métodos estáticos: exclusivos de cada modelo (por exemplo, findByEmail)
📝 Exercícios
- Exercício básico (⭐): Defina o esquema do Usuário (incluindo e-mail, nome de usuário, idade e função) e crie o modelo do Usuário.
- Pergunta básica (⭐): Use
virtualpara definir o campofullName(firstName+lastName). - Exercício avançado (⭐⭐): Implemente o hash de senha (verificação isModified) usando o middleware
pre-save. - Problema avançado (⭐⭐): Use o middleware
pre_findpara filtrar automaticamente os usuários excluídos. - Desafio (⭐⭐⭐): Implemente um modelo de Usuário completo (incluindo hash de senha, campos virtuais, métodos de instância e métodos estáticos) que ofereça suporte a recursos de cadastro, login, exclusão temporária e geração de relatórios.