Node.js: Mongoose ODM
Última atualização: 2026-08-26
O código de validação de dados do Bob, escrito usando o driver nativo do MongoDB, estava espalhado por várias rotas — o endpoint de registro verificava o formato do e-mail, o endpoint de publicação de artigo validava o comprimento do タイトル e o endpoint de alteração de senha verificava se a nova senha era diferente da antiga. Sempre que um novo campo era adicionado, ele precisava incluir um trecho de lógica if (!フィールド) no controlador correspondente, o que fazia com que a mesma expressão regular de validação de e-mail fosse repetida em três rotas. Depois de migrar para o Mongoose, Bob centralizou todas as regras de validação na definição do esquema — definindo-as uma única vez e aplicando-as em todos os lugares —, o que reduziu o código do controlador em 60%.
Você aprenderá:
- Relações e uso da arquitetura de três camadas: esquema, modelo e documento
- Definições declarativas de tipos de campo e validadores (obrigatório / enumerado / mínimo / máximo / correspondência)
- Padrões de campos calculados para o atributo “virtual”
- Interceptação do ciclo de vida dos ganchos pré e pós
- Chamadas encadeadas no construtor de consultas
- Índices (index / únicos) e otimização de desempenho
- Extensões personalizadas para métodos de instância e métodos estáticos
- Use
populatepara implementar consultas combinadas para referências entre documentos
1. Arquitetura central do Mongoose
O Mongoose é uma biblioteca de mapeamento objeto-documento (ODM) para o MongoDB que oferece uma camada de modelagem de dados orientada por esquema sobre o driver nativo. O conceito central está organizado em três camadas: o esquema define a estrutura → o modelo é compilado em um construtor → um documento é uma instância do modelo.
▶ Exemplo:(1) Relação entre esquema, modelo e documento
graph LR
A["Schema<br/>Defining Structures and Verification"] -->|mongoose.model() Compilation| B["Model<br/>Constructor + Query Interface"]
B -->|new Model() Instantiation| C["Document<br/>Examples of Verified Documents"]
C -->|.save() Persistence| D[("MongoDB<br/>Gathering")]
B -->|Model.find() etc.| D
D -->|Back| C
(2) Instalação e conexão
▶ Exemplo: Instalando o Mongoose e conectando-se ao MongoDB
npm install mongoose
const mongoose = require('mongoose');
mongoose.connect('mongodb://localhost:27017/myapp')
.then(() => console.log('MongoDB connected'))
.catch(err => console.error('Connection error:', err));
(3) Esquema e modelo mínimos
▶ Exemplo: Definindo um esquema de usuário e criando um modelo
const userSchema = new mongoose.Schema({
name: String,
email: String,
age: Number
});
const User = mongoose.model('User', userSchema);
| Conceito | Função | Analogia |
|---|---|---|
| Esquema | Planta / Definição estrutural | Desenhos arquitetônicos |
| Modelo | Construtor + Interface de operação do banco de dados | Equipe de construção |
| Documento | Exemplos de documentos verificados | Casas concluídas |
2. Tipos de campos do esquema
O Mongoose oferece mapeamentos de tipos abrangentes para cada campo, superando em muito a falta de restrições de tipo no driver nativo.
(1) Referência rápida aos tipos de campo
| Tipo Mongoose | Tipo JS correspondente | Exemplo | Descrição |
|---|---|---|---|
String |
String | name: String |
Corte automático (requer configuração) |
Number |
Número | age: Number |
Suporta mínimo/máximo |
Boolean |
Booleano | active: Boolean |
Converte automaticamente para 0/1/"true" |
Date |
Data | createdAt: Date |
Métodos de data integrados |
ObjectId |
ObjectId | author: mongoose.Schema.Types.ObjectId |
Referências a outros documentos |
Array |
Matriz | tags: [String] |
Matriz de documentos filhos ou matriz de tipos |
Mixed |
Objeto | meta: mongoose.Schema.Types.Mixed |
Qualquer tipo, sem validação |
Buffer |
Buffer | avatar: Buffer |
Dados binários |
Map |
Mapa | prefs: { type: Map, of: String } |
Estrutura do Mapa no ES6 |
Decimal128 |
Decimal128 | price: mongoose.Schema.Types.Decimal128 |
Decimal de alta precisão |
(2) A sintaxe completa para definições de campos
▶ Exemplo: Explicação detalhada das opções de campo
const productSchema = new mongoose.Schema({
name: {
type: String,
required: [true, 'The product name cannot be left blank.'],
trim: true,
minlength: 2,
maxlength: 100
},
price: {
type: Number,
required: true,
min: [0, 'Prices cannot be negative.'],
default: 0
},
category: {
type: String,
enum: ['electronics', 'books', 'clothing', 'food'],
lowercase: true
},
tags: [String],
metadata: {
type: mongoose.Schema.Types.Mixed,
default: {}
}
});
3. Validador
A validação é o cerne do Mongoose — ela libera os controladores da tarefa de validar dados e centraliza essa tarefa nas declarações de esquema.
(1) Referência rápida do validador
| Validador | Tipo aplicável | Descrição | Exemplo |
|---|---|---|---|
required |
Todos | Campos obrigatórios | required: [true, 'Cannot be empty'] |
enum |
String | Restrição de valor de enumeração | enum: ['A', 'B', 'C'] |
min |
Número / Data | Valor mínimo | min: 0 |
max |
Número / Data | Valor máximo | max: 150 |
minlength |
Sequência | Comprimento mínimo | minlength: 6 |
maxlength |
Sequência de caracteres | Comprimento máximo | maxlength: 200 |
match |
String | Correspondência de expressão regular | match: [/^\S+@\S+\.\S+$/, 'Invalid email format'] |
validate |
Todas | Funções de validação personalizadas | validate: v => v > 0 |
(2) Validadores personalizados
▶ Exemplo: Validadores personalizados e mensagens de erro
const userSchema = new mongoose.Schema({
password: {
type: String,
required: true,
validate: {
validator: function(v) {
return /^(?=.*[A-Z])(?=.*\d).{8,}$/.test(v);
},
message: props => `${props.value} Password does not meet requirements: at least 8 characters, includes uppercase and digits`
}
},
phone: {
type: String,
validate: {
validator: function(v) {
return /^1[3-9]\d{9}$/.test(v);
},
message: 'The phone number format is incorrect'
}
}
});
(3) Verifique o tempo de acionamento
A verificação é acionada automaticamente nos seguintes momentos: new Model().save() e Model.create(). Você pode acioná-la manualmente usando o método validate(). updateOne() / updateMany(), etc., não acionam automaticamente a verificação; é necessário configurar a opção runValidators: true.
▶ Exemplo: Ativando a validação para operações de atualização
User.updateOne(
{ email: 'bob@test.com' },
{ age: -5 },
{ runValidators: true }
);
4. Propriedades virtuais
Os atributos virtuais não são armazenados no banco de dados; eles são calculados dinamicamente apenas durante as consultas, o que os torna adequados para campos derivados.
(1) Definição e uso
▶ Exemplo: atributo virtual “Nome completo do usuário”
const userSchema = new mongoose.Schema({
firstName: String,
lastName: String,
email: String
});
userSchema.virtual('fullName')
.get(function() {
return `${this.firstName} ${this.lastName}`;
})
.set(function(v) {
const parts = v.split(' ');
this.firstName = parts[0];
this.lastName = parts[1] || '';
});
const User = mongoose.model('User', userSchema);
const user = new User({ firstName: 'Bob', lastName: 'Smith' });
console.log(user.fullName);
Bob Smith
(2) virtual e toJSON
Os atributos virtuais não são incluídos por padrão nas saídas toJSON() e toObject(). Eles devem ser ativados explicitamente nas opções do esquema:
const userSchema = new mongoose.Schema({
firstName: String,
lastName: String
}, {
toJSON: { virtuals: true },
toObject: { virtuals: true }
});
5. Hooks (Middleware)
Os hooks (middleware) são executados automaticamente em etapas específicas do ciclo de vida de um documento e são utilizados para pré-processamento de dados, registro em log, operações em cascata e muito mais.
(1) Tipos de hook e condições de acionamento
| Tipo de gancho | Condições de acionamento | Usos comuns |
|---|---|---|
pre('save') |
Antes de salvar | Hash da senha, formatação dos dados, carimbo de data/hora da atualização |
post('save') |
Após salvar | Enviar notificações, registrar entradas |
pre('remove') |
Antes da exclusão | Exclusão em cascata de documentos associados |
post('remove') |
Após a exclusão | Limpar recursos e registros |
pre('find') |
Antes da consulta | Critérios de filtragem padrão (por exemplo, exclusão temporária) |
post('find') |
Após a consulta | Anonimização de dados |
pre('updateOne') |
Antes da atualização | Data e hora da atualização |
post('aggregate') |
Após a agregação | Registro |
(2) para ganchos
▶ Exemplo: Criar hash das senhas automaticamente antes de salvá-las
const bcrypt = require('bcrypt');
userSchema.pre('save', async function(next) {
if (!this.isModified('password')) return next();
this.password = await bcrypt.hash(this.password, 10);
next();
});
(3) Ganchos de postagem
▶ Exemplo: Enviar um e-mail de boas-vindas após o salvamento
userSchema.post('save', function(doc, next) {
console.log(`User ${doc.email} Saved`);
next();
});
(4) Ganchos de consulta
▶ Exemplo: Filtrar automaticamente documentos excluídos durante uma consulta
userSchema.pre('find', function() {
this.where({ deletedAt: null });
});
6. Construtor de consultas
O construtor de consultas do Mongoose suporta chamadas encadeadas, que são mais intuitivas do que os parâmetros de objeto utilizados pelo driver nativo.
(1) Método de consulta em cadeia
▶ Exemplo: Chamadas encadeadas do Construtor de Consultas
const users = await User.find()
.where('age').gte(18).lte(65)
.where('role').equals('admin')
.sort({ createdAt: -1 })
.select('name email age')
.limit(10)
.skip(0);
console.log(users);
(2) Comparação de métodos comuns de consulta
| Implementação de driver nativo | Construtor de consultas do Mongoose | Descrição |
|---|---|---|
db.users.find({ age: { $gte: 18 } }) |
User.find().where('age').gte(18) |
Pesquisa por condição |
db.users.find().sort({ name: 1 }) |
User.find().sort({ name: 1 }) |
Ordenar |
db.users.find().limit(10) |
User.find().limit(10) |
Número de inscrições limitado a |
db.users.find().skip(20) |
User.find().skip(20) |
Pular |
db.users.find({}, { name: 1 }) |
User.find().select('name') |
Filtro de campo |
(3) Encapsulamento de consultas paginadas
▶ Exemplo: Funções auxiliares para consultas paginadas
async function paginate(Model, filter = {}, page = 1, limit = 10) {
const skip = (page - 1) * limit;
const [docs, total] = await Promise.all([
Model.find(filter).skip(skip).limit(limit).sort({ createdAt: -1 }),
Model.countDocuments(filter)
]);
return {
data: docs,
total,
page,
totalPages: Math.ceil(total / limit)
};
}
const result = await paginate(User, { role: 'user' }, 2, 10);
7. Índice
Os índices são fundamentais para o desempenho das consultas em bancos de dados. O Mongoose oferece suporte à definição declarativa de índices no esquema.
(1) Índices de coluna única e índices compostos
▶ Exemplo: Definindo um índice no esquema
const userSchema = new mongoose.Schema({
email: {
type: String,
required: true,
unique: true
},
username: {
type: String,
index: true
},
region: String,
status: String
});
userSchema.index({ region: 1, status: 1 });
| Tipo de índice | Definição | Descrição |
|---|---|---|
| Índice exclusivo | unique: true |
Os valores dos campos devem ser exclusivos |
| Índice normal | index: true |
Consulta acelerada |
| Índice Composto | schema.index({ a: 1, b: -1 }) |
Índice Composto Multidisciplinar |
| Índice de textos | schema.index({ title: 'text' }) |
Pesquisa de texto completo |
▶ Exemplo:(2) Criação automática de índices no ambiente de desenvolvimento
mongoose.connect(uri, { autoIndex: true });
Em um ambiente de produção, recomenda-se desativar autoIndex e criar índices manualmente usando o script de migração para evitar atrasos na inicialização.
8. Métodos de instância e métodos estáticos
O Mongoose permite que você amplie um esquema com métodos personalizados, que se dividem em duas categorias: métodos de instância e métodos estáticos.
(1) Métodos de instância
Os métodos de instância atuam sobre um único documento; use this para acessar o documento atual.
▶ Exemplo: Método de comparação de senhas
userSchema.methods.comparePassword = function(candidate) {
return bcrypt.compare(candidate, this.password);
};
const user = await User.findOne({ email: 'bob@test.com' });
const isMatch = await user.comparePassword('mypassword');
(2) Métodos estáticos
Os métodos estáticos são definidos no Modelo e não dependem de instâncias de documentos, o que os torna adequados para o suporte a consultas.
▶ Exemplo: Localizando métodos estáticos por função
userSchema.statics.findByRole = function(role) {
return this.find({ role }).sort({ createdAt: -1 });
};
const admins = await User.findByRole('admin');
| Tipo | Definição | Chamada | Esta referência |
|---|---|---|---|
| Métodos de instância | schema.methods.xxx = function |
doc.xxx() |
Instância de documento |
| Método estático | schema.statics.xxx = function |
Model.xxx() |
Modelo |
9. Preencher consultas com junções
O populate() do Mongoose implementa a resolução de referências entre documentos do MongoDB, de forma semelhante a um JOIN do SQL.
(1) Definições de referência e junções
▶ Exemplo: Artigos associados a autores
const postSchema = new mongoose.Schema({
title: String,
content: String,
author: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true
}
});
const Post = mongoose.model('Post', postSchema);
const posts = await Post.find().populate('author', 'firstName lastName email');
(2) Preenchimento em vários níveis e filtragem condicional
▶ Exemplo: Junções e filtros em vários níveis
const commentSchema = new mongoose.Schema({
content: String,
author: { type: mongoose.Schema.Types.ObjectId, ref: 'User' },
post: { type: mongoose.Schema.Types.ObjectId, ref: 'Post' }
});
const Comment = mongoose.model('Comment', commentSchema);
const comments = await Comment.find()
.populate('author', 'firstName lastName')
.populate({
path: 'post',
select: 'title content',
match: { status: 'published' }
});
(3) Considerações sobre desempenho para populate
populate() Essencialmente, isso envolve a emissão de consultas adicionais e, em seguida, a fusão dos resultados; não se trata de um JOIN propriamente dito. O problema N+1 ainda persiste — se 100 artigos estiverem associados a 100 autores diferentes, isso acionará 101 consultas. Para cenários com associações frequentes, considere a incorporação de documentos ou $lookup a agregação.
10. Comparação entre o Mongoose e o driver nativo
| Dimensão | Driver nativo do MongoDB | Mongoose ODM |
|---|---|---|
| Validação de dados | Instruções if/else codificadas manualmente espalhadas pelo controlador | Validação declarativa baseada em esquema, gerenciada centralmente |
| Restrições de tipo | Nenhuma; qualquer campo pode armazenar qualquer valor | Tipos impostos pelo esquema; conversão automática |
| Consultas combinadas | Agregação manual $lookup |
populate() Feito em uma linha |
| Ganchos de ciclo de vida | Nenhum | Ganchos pré/pós |
| Atributo virtual | Nenhum | Campo virtual calculado dinamicamente |
| Gerenciamento de índices | Manual createIndex() |
Declaração de esquema + Criação automática |
| API de consulta | Parâmetros de objeto find({ age: { $gte: 18 } }) |
Construtor encadeado + parâmetros de objeto |
| Curva de aprendizado | Baixa; se assemelha bastante à sintaxe nativa do MongoDB | Média; requer compreensão de esquema, modelo e documento |
| Flexibilidade | Alta, controle total | Média, os campos fora do esquema são ignorados por padrão |
| Desempenho | Ligeiramente melhor, sem camada intermediária | Ligeiramente menor, sobrecarga causada pela validação e pelos hooks |
11. Exemplo abrangente: modelos de dados de usuários e artigos
Combine esquemas, validação, hooks, atributos virtuais, métodos de instância e consultas de junção para formar um sistema completo de modelo de dados.
▶ Exemplo: models/User.js
const mongoose = require('mongoose');
const bcrypt = require('bcrypt');
const userSchema = new mongoose.Schema({
firstName: {
type: String,
required: [true, 'The last name cannot be left blank.'],
trim: true
},
lastName: {
type: String,
required: [true, 'The name cannot be empty'],
trim: true
},
email: {
type: String,
required: [true, 'The email address cannot be left blank.'],
unique: true,
lowercase: true,
match: [/^\S+@\S+\.\S+$/, 'The email address format is incorrect.']
},
password: {
type: String,
required: [true, 'The password cannot be empty.'],
minlength: 8,
validate: {
validator: function(v) {
return /^(?=.*[A-Z])(?=.*\d).{8,}$/.test(v);
},
message: 'Password must be at least 8 characters, must include uppercase and digits'
}
},
role: {
type: String,
enum: ['user', 'admin'],
default: 'user'
},
age: {
type: Number,
min: [0, 'Age cannot be a negative number.'],
max: [150, 'Age must not exceed150']
},
createdAt: {
type: Date,
default: Date.now
}
}, {
toJSON: { virtuals: true },
toObject: { virtuals: true }
});
userSchema.virtual('fullName').get(function() {
return `${this.firstName} ${this.lastName}`;
});
userSchema.pre('save', async function(next) {
if (!this.isModified('password')) return next();
this.password = await bcrypt.hash(this.password, 10);
next();
});
userSchema.methods.comparePassword = function(candidate) {
return bcrypt.compare(candidate, this.password);
};
userSchema.statics.findByRole = function(role) {
return this.find({ role }).sort({ createdAt: -1 });
};
module.exports = mongoose.model('User', userSchema);
▶ Exemplo: models/Post.js
const mongoose = require('mongoose');
const postSchema = new mongoose.Schema({
title: {
type: String,
required: [true, 'The title cannot be left blank.'],
trim: true,
minlength: [2, 'Title must be at least2characters'],
maxlength: [200, 'Most Titles200characters']
},
content: {
type: String,
required: [true, 'Content cannot be empty']
},
author: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true
},
status: {
type: String,
enum: ['draft', 'published', 'archived'],
default: 'draft'
},
tags: [{
type: String,
lowercase: true
}],
viewCount: {
type: Number,
default: 0,
min: 0
},
createdAt: {
type: Date,
default: Date.now
},
updatedAt: {
type: Date,
default: Date.now
}
});
postSchema.index({ status: 1, createdAt: -1 });
postSchema.index({ tags: 1 });
postSchema.virtual('excerpt').get(function() {
return this.content.substring(0, 100) + '...';
});
postSchema.pre('save', function(next) {
if (this.isModified('content')) {
this.updatedAt = new Date();
}
next();
});
postSchema.post('remove', async function(doc) {
await mongoose.model('Comment').deleteMany({ post: doc._id });
});
postSchema.statics.findPublished = function() {
return this.find({ status: 'published' })
.populate('author', 'firstName lastName email')
.sort({ createdAt: -1 });
};
module.exports = mongoose.model('Post', postSchema);
▶ Exemplo: Consultas e junções
const mongoose = require('mongoose');
const User = require('./models/User');
const Post = require('./models/Post');
async function main() {
await mongoose.connect('mongodb://localhost:27017/blog');
const user = await User.create({
firstName: 'Bob',
lastName: 'Smith',
email: 'bob@example.com',
password: 'Secure123',
role: 'admin',
age: 28
});
const post = await Post.create({
title: 'Mongoose Getting Started Guide',
content: 'Mongoose is MongoDB ODM library, providing schema-driven data modeling...',
author: user._id,
status: 'published',
tags: ['mongodb', 'mongoose', 'nodejs']
});
const published = await Post.findPublished();
console.log(published[0].excerpt);
console.log(published[0].author.fullName);
const match = await user.comparePassword('Secure123');
console.log('Password match:', match);
await mongoose.connection.close();
}
main();
node app.js
Mongoose is MongoDB ODM library, providing schema-driven data modeling......
Bob Smith
Password match: true
❓ Perguntas Frequentes
P: Como faço para escolher entre o Mongoose e o driver nativo? R: Escolha o driver nativo para projetos pequenos ou quando precisar de flexibilidade máxima; escolha o Mongoose para projetos de médio a grande porte, colaboração em equipe ou quando precisar de validação e hooks. Você também pode usar os dois juntos — o Mongoose oferece suporte ao acesso a coleções nativas por meio do
Model.collection.
P: Qual é a diferença entre um Esquema e um Modelo? R: Um Esquema é um modelo que define tipos de campos, regras de validação e hooks; um Modelo é um construtor gerado a partir de um Esquema que fornece uma interface para métodos CRUD. Um único Esquema pode gerar apenas um Modelo.
P: Os campos virtuais não são armazenados no banco de dados? R: Sim, os campos virtuais são calculados apenas na memória e não são gravados no MongoDB. Se você precisar mantê-los, use um campo comum e o hook
pre-savepara calcular e atribuir o valor. Por padrão, os campos virtuais não aparecem na saída JSON; é necessário definirtoJSON: { virtuals: true }.
P: Qual é o desempenho do
populate? R: Opopulatenão é um JOIN de SQL; ele basicamente envolve a execução de consultas adicionais e, em seguida, a fusão dos resultados. O desempenho é bom quando há poucos documentos relacionados; no entanto, um grande número de documentos relacionados diferentes pode causar um problema de consulta N+1. Para cenários de alta frequência, considere a incorporação de documentos, o uso manual do$lookupou o armazenamento em cache.
P: Como faço para lidar com erros de validação? R: O Mongoose lança
ValidationErrorquando a validação falha, e o objetoerrorscontém os detalhes do erro para cada campo. Você pode usarObject.values(err.errors).map(e => e.message)para extrair todas as mensagens e, em conjunto com o middleware de tratamento de erros do Express, retornar um código de status 400 uniforme.
P: A que se refere
thisno hookpre-save? R:thisrefere-se à instânciaDocumentque está prestes a ser salva. Observação: o uso de funções-seta fará com que a ligaçãothisseja perdida; a função do gancho deve ser uma função comum.this.isModified('field')pode ser usado para determinar se um campo foi modificado.
P: Posso adicionar campos depois que o esquema já tiver sido definido? R: Sim, você pode usar
schema.add({ newField: String })para adicioná-los dinamicamente. No entanto, os modelos compilados não serão atualizados automaticamente; você precisará recompilá-los ou usar a extensãoschema.plugin(). Recomendamos planejar cuidadosamente a estrutura dos campos no início do projeto.
📖 Resumo
- Conceitos-chave e uso da arquitetura central do Mongoose
- Conceitos básicos e uso dos tipos de campo do esquema
- Conceitos básicos e uso de validadores
- Conceitos básicos e uso de propriedades virtuais
- Conceitos básicos e uso de hooks (middleware)
- Conceitos básicos e uso do Construtor de Consultas
- Conceitos básicos e uso de índices
- Conceitos básicos e uso de métodos de instância e métodos estáticos
📝 Exercícios
- Conclua todos os exemplos de código desta lição e certifique-se de que cada um deles seja executado corretamente.
- Modifique o exemplo completo e adicione suas próprias extensões
- Analise a documentação oficial, identifique 1 ou 2 APIs que não foram abordadas nesta aula e escreva um código de teste para elas.
- Reflexão: Como você aplicaria o que aprendeu nesta aula a um projeto do mundo real?
- Tente combinar o que você aprendeu nesta aula com o conteúdo das aulas anteriores para criar um pequeno projeto.