Introdução ao MongoDB e Mongoose
Avalie esse Início rápido
Neste artigo, aprenderemos como o Mongoose, uma biblioteca de terceiros para o MongoDB, pode ajudar você a estruturar e acessar seus dados com facilidade.
Muitos que aprendem MongoDB são apresentados a ele por meio da biblioteca muito popular Mongoose, que é descrita como “elegant MongoDB object modeling for Node.js.”
Mongoose é uma biblioteca ODM (Object Data Modeling) para MongoDB. Embora você não precise usar uma ferramenta ODM ou ORM (Object Relational Mapping) para ter uma ótima experiência com o MongoDB, alguns desenvolvedores as preferem. Muitos desenvolvedores Node.js optam por trabalhar com o Mongoose para ajudar com modelagem de dados, imposição de esquema, validação de modelo e manipulação de dados em geral. E o Mongoose simplifica essas tarefas.
Por padrão, o MongoDB tem um modelo de dados flexível. Isso torna os bancos de dados do MongoDB muito fáceis de alterar e atualizar no futuro. Mas muitos desenvolvedores estão acostumados a ter esquemas rígidos.
O Mongoose força um esquema semirrígido desde o início. Com o Mongoose, os desenvolvedores devem definir um esquema e um modelo.
Um esquema define a estrutura dos documentos da sua coleção. Um esquema Mongoose é mapeado diretamente para uma coleção MongoDB.
1 const blog = new Schema({ 2 title: String, 3 slug: String, 4 published: Boolean, 5 author: String, 6 content: String, 7 tags: [String], 8 createdAt: Date, 9 updatedAt: Date, 10 comments: [{ 11 user: String, 12 content: String, 13 votes: Number 14 }] 15 });
Com esquemas, definimos cada campo e seu tipo de dados. Tipos permitidos são:
- String
- Número
- Data
- Buffer
- Boolean
- Misto
- ObjectId
- Array
- Decimal128
- Map
Os modelos pegam seu esquema e o aplicam a cada documento em sua coleção.
Os modelos são responsáveis por todas as interações de documentos, como criação, leitura, atualização e exclusão (CRUD).
Uma observação importante: o primeiro argumento passado para o modelo deve ser a forma singular do nome da sua coleção. O Mongoose altera automaticamente isso para o plural, transforma em minúsculas e usa para o nome da coleção de banco de dados.
1 const Blog = mongoose.model('Blog', blog);
Neste exemplo,
Blog
se traduz na coleção blogs
.Vamos executar os seguintes comandos do terminal para começar:
1 mkdir mongodb-mongoose 2 cd mongodb-mongoose 3 npm init -y 4 npm i mongoose 5 npm i -D nodemon 6 code .
Isso cria o diretório do projeto, inicializa, instala os pacotes necessários e abre o projeto no VS Code.
Vamos adicionar um script ao nosso arquivo
package.json
para executar nosso projeto. Também usaremos ES Modules em vez de Common JS, portanto, adicionaremos o módulo type
também. Isso também nos permitirá usarawait
de nível superior .1 ... 2 "scripts": { 3 "dev": "nodemon index.js" 4 }, 5 "type": "module", 6 ...
Agora criaremos o arquivo
index.js
e usaremos o Mongoose para nos conectar ao MongoDB.1 import mongoose from 'mongoose' 2 3 mongoose.connect("mongodb+srv://<username>:<password>@cluster0.eyhty.mongodb.net/myFirstDatabase?retryWrites=true&w=majority")
Você pode se conectar a uma instância local do MongoDB, mas, neste artigo, usaremos um cluster MongoDB Atlas gratuito. Se você ainda não tiver uma conta, é fácil se cadastrar para um cluster MongoDB Atlas gratuito aqui.
Depois de criar seu cluster, você deve substituir a string de conexão acima pela sua string de conexão, incluindo seu nome de usuário e senha.
A string de conexão que você copia do dashboard do MongoDB Atlas fará referência ao banco de dados do
myFirstDatabase
. Mude isso para o que você gostaria de chamar de banco de dados.Antes de fazermos qualquer coisa com nossa conexão, precisaremos criar um esquema e um modelo.
O ideal é criar um arquivo de esquema/modelo para cada esquema necessário. Então, criaremos uma nova estrutura de pasta/arquivo:
model/Blog.js
.1 import mongoose from 'mongoose'; 2 const { Schema, model } = mongoose; 3 4 const blogSchema = new Schema({ 5 title: String, 6 slug: String, 7 published: Boolean, 8 author: String, 9 content: String, 10 tags: [String], 11 createdAt: Date, 12 updatedAt: Date, 13 comments: [{ 14 user: String, 15 content: String, 16 votes: Number 17 }] 18 }); 19 20 const Blog = model('Blog', blogSchema); 21 export default Blog;
Agora que temos nosso primeiro modelo e esquema configurados, podemos começar a inserir dados em nosso banco de dados.
De volta ao arquivo
index.js
, vamos inserir um novo artigo no blog.1 import mongoose from 'mongoose'; 2 import Blog from './model/Blog'; 3 4 mongoose.connect("mongodb+srv://mongo:mongo@cluster0.eyhty.mongodb.net/myFirstDatabase?retryWrites=true&w=majority") 5 6 // Create a new blog post object 7 const article = new Blog({ 8 title: 'Awesome Post!', 9 slug: 'awesome-post', 10 published: true, 11 content: 'This is the best post ever', 12 tags: ['featured', 'announcement'], 13 }); 14 15 // Insert the article in our MongoDB database 16 await article.save();
Primeiro precisamos importar o modelo
Blog
que criamos. Em seguida, criamos um novo objeto e usamos o método save()
para inseri-lo em nosso MongoDB database.Vamos adicionar um pouco mais depois disso para registrar o que está atualmente no banco de dados. Usaremos o método
findOne()
para isso.1 // Find a single blog post 2 const firstArticle = await Blog.findOne({}); 3 console.log(firstArticle);
Vamos executar o código!
1 npm run dev
Você deverá ver o documento inserido registrado em seu terminal.
Como estamos usando
nodemon
neste projeto, toda vez que você salvar um arquivo, o código será executado novamente. Se você quiser inserir vários artigos, continue salvando. 😄No exemplo anterior, usamos o método
save()
Mongoose para inserir o documento em nosso banco de dados. Isso requer duas ações: instanciar o objeto e salvá-lo.Como alternativa, podemos fazer isso em uma ação usando o método
create()
do Mongoose.1 // Create a new blog post and insert into database 2 const article = await Blog.create({ 3 title: 'Awesome Post!', 4 slug: 'awesome-post', 5 published: true, 6 content: 'This is the best post ever', 7 tags: ['featured', 'announcement'], 8 }); 9 10 console.log(article);
Este método é muito melhor! Além de podermos inserir nosso documento, também retornamos o documento junto com seu
_id
quando o registramos no console.O Mongoose também torna a atualização de dados muito conveniente. Expandindo o exemplo anterior, vamos alterar o
title
do nosso artigo.1 article.title = "The Most Awesomest Post!!"; 2 await article.save(); 3 console.log(article);
Podemos editar diretamente o objeto local e, em seguida, usar o método
save()
para gravar a atualização de volta no banco de dados. Acho que não há nada mais fácil do que isso!Vamos garantir que estamos atualizando o documento correto. Usaremos um método especial do Mongoose,
findById()
, para obter nosso documento pelo seu ObjectId.1 const article = await Blog.findById("62472b6ce09e8b77266d6b1b").exec(); 2 console.log(article);
Observe que usamos a função
exec()
do Mongoose. Isso é opcional e retorna uma promessa. Na minha experiência, é melhor usar essa função, pois ela evitará alguns problemas de coçadura. Se quiser ler mais sobre isso, confira esta nota nos documentos do Mongoose sobre promessas.Assim como no driver padrão do MongoDB Node.js, podemos projetar apenas os campos necessários. Vamos obter somente os campos
title
, slug
e content
.1 const article = await Blog.findById("62472b6ce09e8b77266d6b1b", "title slug content").exec(); 2 console.log(article);
O segundo parâmetro pode ser do tipo
Object|String|Array<String>
para especificar quais campos queremos projetar. Neste caso, usamos um String
.Assim como no driver padrão Node.js do MongoDB, temos os métodos
deleteOne()
e deleteMany()
.1 const blog = await Blog.deleteOne({ author: "Jesse Hall" }) 2 console.log(blog) 3 4 const blog = await Blog.deleteMany({ author: "Jesse Hall" }) 5 console.log(blog)
Observe que os documentos que inserimos até agora não continham
author
, datas ou comments
. Até agora, definimos como deve ser a estrutura de nosso documento, mas não definimos quais campos são realmente obrigatórios. Neste ponto, qualquer campo pode ser omitido.Vamos definir alguns campos obrigatórios em nosso esquema
Blog.js
.1 const blogSchema = new Schema({ 2 title: { 3 type: String, 4 required: true, 5 }, 6 slug: { 7 type: String, 8 required: true, 9 lowercase: true, 10 }, 11 published: { 12 type: Boolean, 13 default: false, 14 }, 15 author: { 16 type: String, 17 required: true, 18 }, 19 content: String, 20 tags: [String], 21 createdAt: { 22 type: Date, 23 default: () => Date.now(), 24 immutable: true, 25 }, 26 updatedAt: Date, 27 comments: [{ 28 user: String, 29 content: String, 30 votes: Number 31 }] 32 });
Ao incluir validação em um campo, passamos um objeto como seu valor.
value: String
é o mesmo que value: {type: String}
.Há vários métodos de validação que podem ser usados.
Podemos definir
required
como verdadeiro em quaisquer campos que gostaríamos que fossem necessários.Para o
slug
, queremos que a string esteja sempre em letras minúsculas. Para isso, podemos definir lowercase
como verdadeiro. Isso pegará a entrada do slug e a converterá em minúsculas antes de salvar o documento no banco de dados.Para nossa data
created
, podemos definir a compra padrão usando uma função de seta. Também queremos que essa data não possa ser alterada posteriormente. Podemos fazer isso definindo immutable
como true.Os validadores são executados somente nos métodos de criação ou gravação.
O Mongoose usa muitos métodos padrão do MongoDB e introduz muitos métodos auxiliares extras que são abstraídos dos métodos regulares do MongoDB. A seguir, vamos falar sobre alguns deles.
O método
exists()
retorna null
ou o ObjectId de um documento que corresponde à query fornecida.1 const blog = await Blog.exists({ author: "Jesse Hall" }) 2 console.log(blog)
O Mongoose também tem seu próprio estilo de consultar dados. O método
where()
nos permite encadear e construir queries.1 // Instead of using a standard find method 2 const blogFind = await Blog.findOne({ author: "Jesse Hall" }); 3 4 // Use the equivalent where() method 5 const blogWhere = await Blog.where("author").equals("Jesse Hall"); 6 console.log(blogWhere)
Qualquer um desses métodos funciona. Use o que lhe parecer mais natural.
Você também pode encadear vários métodos
where()
para incluir até mesmo a query mais complicada.Para incluir projeção ao usar o método
where()
, encadeie o método select()
após sua query.1 const blog = await Blog.where("author").equals("Jesse Hall").select("title author") 2 console.log(blog)
É importante entender suas opções ao modelar dados.
Se você vem de um banco de dados relacional, está acostumado a ter tabelas separadas para todos os seus dados relacionados.
Geralmente, no MongoDB os dados acessados juntos devem ser armazenados juntos.
Você deve planejar isso com antecedência, se possível. Aninhe os dados no mesmo esquema quando fizer sentido.
Se você precisar de esquemas separados, o Mongoose facilita muito.
Vamos criar outro esquema para podermos ver como vários esquemas podem ser usados juntos.
Criaremos um novo arquivo,
User.js
, na pasta modelo.1 import mongoose from 'mongoose'; 2 const {Schema, model} = mongoose; 3 4 const userSchema = new Schema({ 5 name: { 6 type: String, 7 required: true, 8 }, 9 email: { 10 type: String, 11 minLength: 10, 12 required: true, 13 lowercase: true 14 }, 15 }); 16 17 const User = model('User', userSchema); 18 export default User;
Para
email
, estamos usando uma nova propriedade, minLength
, para exigir um comprimento mínimo de caracteres para essa string.Agora, faremos referência a esse novo modelo de usuário em nosso esquema de blog para
author
e comments.user
.1 import mongoose from 'mongoose'; 2 const { Schema, SchemaTypes, model } = mongoose; 3 4 const blogSchema = new Schema({ 5 ..., 6 author: { 7 type: SchemaTypes.ObjectId, 8 ref: 'User', 9 required: true, 10 }, 11 ..., 12 comments: [{ 13 user: { 14 type: SchemaTypes.ObjectId, 15 ref: 'User', 16 required: true, 17 }, 18 content: String, 19 votes: Number 20 }]; 21 }); 22 ...
Aqui, definimos
author
e comments.user
como SchemaTypes.ObjectId
e adicionamos um ref
, ou referência, ao modelo de usuário.Isso nos permitirá "join" nossos dados um pouco mais tarde.
E não se esqueça de desestruturar
SchemaTypes
de mongoose
na parte superior do arquivo.Por último, vamos atualizar o arquivo
index.js
. Precisamos importar nosso novo modelo de usuário, criar um novo usuário e criar um novo artigo com o _id
do novo usuário.1 ... 2 import User from './model/User.js'; 3 4 ... 5 6 const user = await User.create({ 7 name: 'Jesse Hall', 8 email: 'jesse@email.com', 9 }); 10 11 const article = await Blog.create({ 12 title: 'Awesome Post!', 13 slug: 'Awesome-Post', 14 author: user._id, 15 content: 'This is the best post ever', 16 tags: ['featured', 'announcement'], 17 }); 18 19 console.log(article);
Observe agora que há uma coleção
users
junto com a coleção blogs
no MongoDB database.Agora você verá apenas o usuário
_id
no campo do autor. Então, como obtemos todas as informações para o autor junto com o artigo?Podemos usar o método
populate()
do Mongoose.1 const article = await Blog.findOne({ title: "Awesome Post!" }).populate("author"); 2 console.log(article);
Agora os dados do
author
estão preenchidos, ou "joined," nos dados article
. O Mongoose realmente usa o método $lookup
nos bastidores.No Mongoose, middleware são funções executadas antes e/ou durante a execução de funções assíncronas no nível do esquema.
Este é um exemplo. Vamos atualizar a data
updated
sempre que um artigo for salvo ou atualizado. Adicionaremos isso ao nosso modelo Blog.js
.1 blogSchema.pre('save', function(next) { 2 this.updated = Date.now(); // update the date every time a blog post is saved 3 next(); 4 });
Em seguida, no arquivo
index.js
, encontraremos um artigo, atualizaremos o título e o salvaremos.1 const article = await Blog.findById("6247589060c9b6abfa1ef530").exec(); 2 article.title = "Updated Title"; 3 await article.save(); 4 console.log(article);
Observe que agora temos uma data
updated
!Além de
pre()
, há também uma função de middleware mongoose post()
.Acho que nosso exemplo aqui poderia usar outro esquema para
comments
. Tente criar esse esquema e testá-lo adicionando alguns usuários e comentários.Há muitos outros métodos auxiliares excelentes do Mongoose que não são abordados aqui. Certifique-se de verificar a documentação oficial para obter referências e mais exemplos.
Acho ótimo que os desenvolvedores tenham muitas opções para conectar e manipular dados no MongoDB. Independentemente de você preferir o Mongoose ou os drivers padrão do MongoDB, no final, o que importa são os dados e o que é melhor para sua aplicação e para o seu caso de uso.
Entendo por que o Mongoose é popular para muitos desenvolvedores e acho que o usarei mais no futuro.