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


2. Uma análise do guia “Introdução ao Mongoose”

mongoose = MongoDB + aprimoramento de ORM:

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.

JAVASCRIPT
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({...});

100%
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).

100%
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

JAVASCRIPT
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

JAVASCRIPT
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().

100%
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

JAVASCRIPT
// === 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).

JAVASCRIPT
// === 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.

100%
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
JAVASCRIPT
// === 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.”

JAVASCRIPT
// === 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.

100%
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.

JAVASCRIPT
// === 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.

JAVASCRIPT
// === 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.

JAVASCRIPT
// === 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

JAVASCRIPT
// === 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.

100%
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

JAVASCRIPT
// === 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

JAVASCRIPT
// === 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.

JAVASCRIPT
// === 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: true em toJSON para que eles sejam retornados na API.

P: O middleware de pré-salvamento pode ser assíncrono? R: Sim. Use async function ou retorne uma Promise. É necessário chamar next() 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 pre next(error) e na fase post next(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


📝 Exercícios

  1. 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.
  2. Pergunta básica (⭐): Use virtual para definir o campo fullName (firstName + lastName).
  3. Exercício avançado (⭐⭐): Implemente o hash de senha (verificação isModified) usando o middleware pre-save.
  4. Problema avançado (⭐⭐): Use o middleware pre_find para filtrar automaticamente os usuários excluídos.
  5. 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.
Web-Tutorial.com

Equipe Técnica Web-Tutorial

Uma plataforma de tutoriais mantida por diversos desenvolvedores. Cada tutorial é escrito e revisado por profissionais da área correspondente. Trabalhamos para manter nosso conteúdo preciso e confiável — se encontrar algum problema, avise-nos.

100%