MongoDB e Mongoose: compatibilidade e comparação
Ado Kukic, Stanimira Vlaeva11 min read • Published Nov 25, 2021 • Updated Apr 02, 2024
Avalie esse Artigo
Neste artigo, exploraremos a biblioteca Mongoose do MongoDB. Mongoose é uma biblioteca de modelagem de dados de objetos (ODM) do MongoDB, distribuída como um pacote npm. Vamos comparar e contrastar o Mongoose com o uso do driver nativo do MongoDB Node.js juntamente com a validação de esquema do MongoDB.
Veremos como a validação de esquema do MongoDB nos ajuda a impor um esquema de banco de dados e, ao mesmo tempo, permite uma grande flexibilidade quando necessário. Por fim, veremos se os recursos adicionais que o Mongoose oferece valem a sobrecarga de introduzir uma biblioteca de terceiros em nossos aplicativos.
Mongoose é uma biblioteca de modelagem de dados de objetos (ODM) baseada em Node.js para MongoDB. É semelhante a um mapeador relacional de objetos (ORM) como o SQLAlchemy para bancos de dados SQL tradicionais. O problema que o Mongoose visa resolver é permitir que os desenvolvedores imponham um esquema específico na camada do aplicativo. Além de impor um esquema, o Mongoose também oferece uma variedade de ganchos, validação de modelos e outros recursos com o objetivo de facilitar o trabalho com o MongoDB.
A validação de esquema do MongoDB torna possível aplicar facilmente um esquema em seu MongoDB database, mantendo um alto grau de flexibilidade, oferecendo o melhor dos dois mundos. No passado, a única maneira de impor um esquema a uma coleção do MongoDB era fazê-lo no nível da aplicação usando um ODM como o Mongoose, mas isso representava desafios significativos para os desenvolvedores.
Se quiser seguir este tutorial e brincar com as validações de esquema, mas não tiver uma instância do MongoDB configurada, você poderá configurar um MongoDB Atlas cluster gratuito aqui.
Um grande benefício de usar um banco de dados NoSQL como o MongoDB é que você não fica limitado a um modelo de dados rígido. Você pode adicionar ou remover campos, agrupar dados com várias camadas de profundidade e ter um modelo de dados verdadeiramente flexível que atenda às suas necessidades atuais e possa se adaptar às suas necessidades em constante mudança no futuro. Mas ser muito flexível também pode ser um desafio. Se não houver consenso sobre como o modelo de dados deve ser e cada documento em uma coleção contiver campos muito diferentes, você terá dificuldades.
Em uma extremidade do esquema, temos ODMs como o Mongoose, que desde o início nos forçam a um esquema semirrígido. Com o Mongoose, você definiria um objeto
Schema
no código da aplicação que mapeia para uma coleção no MongoDB database. O objeto Schema
define a estrutura dos documentos na coleção. Em seguida, você precisa criar um objeto Model
fora do esquema. O modelo é usado para interagir com a coleção.Por exemplo, digamos que estamos criando um blog e queremos representar uma publicação no blog. Primeiro definimos um esquema e, em seguida, criamos um modelo Mongoose correspondente:
1 const blog = new Schema({ 2 title: String, 3 slug: String, 4 published: Boolean, 5 content: String, 6 tags: [String], 7 comments: [{ 8 user: String, 9 content: String, 10 votes: Number 11 }] 12 }); 13 14 const Blog = mongoose.model('Blog', blog);
Depois de definirmos um modelo do Mongoose, poderíamos executar queries para buscar, atualizar e excluir dados em uma coleção do MongoDB que se alinha ao modelo do Mongoose. Com o modelo acima, poderíamos fazer coisas como:
1 // Create a new blog post 2 const article = new Blog({ 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 // Insert the article in our MongoDB database 11 article.save(); 12 13 // Find a single blog post 14 Blog.findOne({}, (err, post) => { 15 console.log(post); 16 });
A vantagem de usar o Mongoose é que temos um esquema para trabalhar em nosso código de aplicativo e uma relação explícita entre os documentos do MongoDB e os modelos do Mongoose em nosso aplicativo. A desvantagem é que só podemos criar posts de blog e eles precisam seguir o esquema definido acima. Se mudarmos o esquema do Mongoose, estaremos mudando completamente o relacionamento e, se você estiver passando por um desenvolvimento rápido, isso poderá atrasá-lo bastante.
A outra desvantagem é que essa relação entre o esquema e o modelo só existe dentro dos limites de nosso aplicativo Node.js. Nosso MongoDB database não está ciente da relação, ele apenas insere ou recupera os dados solicitados sem qualquer tipo de validação. Caso usássemos uma linguagem de programação diferente para interagir com nosso banco de dados, todas as restrições e modelos que definimos no Mongoose seriam inúteis.
Por outro lado, se decidirmos usar apenas o driver MongoDB Node.js, poderíamos executar queries em qualquer coleção em nosso banco de dados ou criar novas rapidamente. O driver MongoDB Node.js não tem conceitos de modelagem ou mapeamento de dados de objeto.
Simplesmente escrevemos queries no banco de dados e na coleção com a qual desejamos trabalhar para atingir as metas de negócios. Se quiséssemos inserir uma nova postagem de blog em nossa coleção, poderíamos executar um comando como este:
1 db.collection('posts').insertOne({ 2 title: 'Better Post!', 3 slug: 'a-better-post', 4 published: true, 5 author: 'Ado Kukic', 6 content: 'This is an even better post', 7 tags: ['featured'], 8 });
Essa operação
insertOne()
seria executada sem problemas usando o driver Node.js. Se tentássemos salvar esses dados usando nosso modelo Mongoose Blog
, haveria falha, porque não temos uma propriedade author
definida em nosso modelo Blog Mongoose.Só porque o driver Node.js não tem o conceito de modelo, não significa que não podemos criar modelos para representar nossos dados do MongoDB no nível do aplicativo. Poderemos facilmente criar um modelo genérico ou usar uma biblioteca como objectmodel. Poderemos criar um modelo
Blog
como este:1 function Blog(post) { 2 this.title = post.title; 3 this.slug = post.slug; 4 ... 5 }
Poderíamos, então, usar esse modelo em conjunto com nosso driver Node.js do MongoDB, o que nos daria a flexibilidade de usar o modelo, mas sem sermos limitados por ele.
1 db.collection('posts').findOne({}).then((err, post) => { 2 let article = new Blog(post); 3 });
Nesse cenário, nosso banco de dados do MongoDB ainda desconhece o modelo de blog no nível do aplicativo, mas nossos desenvolvedores podem trabalhar com ele, adicionar métodos e auxiliares específicos ao modelo, e saberiam que esse modelo deve ser usado apenas dentro os limites do nosso aplicativo Node.js. Em seguida, vamos explorar a validação de esquema.
Podemos escolher entre duas maneiras diferentes de adicionar validação de esquema às nossas coleções do MongoDB. A primeira é usar validadores em nível de aplicativo, que são definidos nos esquemas do Mongoose. A segunda é usar a validação de esquema do MongoDB, que é definida na própria coleção do MongoDB. A grande diferença é que a validação nativa do esquema MongoDB é aplicada no nível do banco de dados. Vejamos por que isso é importante explorando ambos os métodos.
Quando se trata de validação de esquema, o Mongoose a aplica na camada do aplicativo, como vimos na seção anterior. Ele faz isso de duas maneiras.
Primeiro, ao definir nosso modelo, estamos informando explicitamente ao nosso aplicativo Node.js quais campos e tipos de dados permitiremos que sejam inseridos em uma coleção específica. Por exemplo, nosso esquema do Mongoose Blog define uma propriedade
title
do tipo String
. Se tentarmos inserir uma publicação no blog com uma propriedade title
que seja uma array, ela falhará. Tudo que estiver fora dos campos definidos também não será inserido no banco de dados.Em segundo lugar, validamos ainda mais se os dados nos campos definidos correspondem ao nosso conjunto definido de critérios. Por exemplo, podemos expandir nosso modelo de blog adicionando validadores específicos, como exigir determinados campos, garantir um tamanho mínimo ou máximo para um campo específico ou até mesmo criar nossa lógica personalizada. Vamos ver como isso fica com o Mongoose. Em nosso código, simplesmente expandiríamos a propriedade e adicionaríamos nossos validadores:
1 const blog = new Schema({ 2 title: { 3 type: String, 4 required: true, 5 }, 6 slug: { 7 type: String, 8 required: true, 9 }, 10 published: Boolean, 11 content: { 12 type: String, 13 required: true, 14 minlength: 250 15 }, 16 ... 17 }); 18 19 const Blog = mongoose.model('Blog', blog);
O Mongoose trata da definição do modelo e da validação do esquema de uma só vez. A desvantagem ainda é a mesma. Essas regras se aplicam apenas à camada do aplicativo e o próprio MongoDB fica sem saber de nada.
O driver do MongoDB Node.js em si não tem mecanismos para inserir ou gerenciar validações, e não deveria. Podemos definir regras de validação de esquema para nosso banco de dados do MongoDB usando o MongoDB Shell ou o Compass.
Podemos criar uma validação de esquema ao criar nossa coleção ou após o fato em uma coleção existente. Como estamos trabalhando com essa ideia de blog como exemplo, adicionaremos nossas validações de esquema a ela. Usarei Compass e MongoDB Atlas. Para obter um ótimo recurso sobre como adicionar validações de esquema programaticamente, confira esta série.
Se quiser seguir este tutorial e brincar com as validações de esquema, mas não tiver uma instância do MongoDB configurada, você poderá configurar um MongoDB Atlas cluster gratuito aqui.
Crie uma coleção chamada
posts
e vamos inserir os dois documento com os quais estamos trabalhando. Os documentos são:1 [{"title":"Better Post!","slug":"a-better-post","published":true,"author":"Ado Kukic","content":"This is an even better post","tags":["featured"]}, {"_id":{"$oid":"5e714da7f3a665d9804e6506"},"title":"Awesome Post","slug":"awesome-post","published":true,"content":"This is an awesome post","tags":["featured","announcement"]}]
Agora, na interface de usuário do Compass, navegarei até a aba Validação. Como esperado, não há regras de validação em vigor no momento, o que significa que nosso banco de dados aceitará qualquer documento, desde que seja um BSON válido. Clique no botãoAdicionar uma regra) e você verá uma interface de usuário para criar suas próprias regras de validação.
Por padrão, não há regras, portanto, qualquer documento será marcado como aprovado. Vamos adicionar uma regra para exigir a propriedade
author
. Ela terá a seguinte aparência:1 { 2 $jsonSchema: { 3 bsonType: "object", 4 required: [ "author" ] 5 } 6 }
Agora vamos ver que nossa postagem inicial, que não tem um campo
author
falhou na validação, enquanto a postagem que tem o campo author
está pronta.Também podemos ir além e adicionar validações a campos individuais. Digamos que, para fins de SEO, queremos que todos os títulos das postagens do blog tivessem no mínimo 20 caracteres e no máximo 80 caracteres. Podemos representar isso assim:
1 { 2 $jsonSchema: { 3 bsonType: "object", 4 required: [ "tags" ], 5 properties: { 6 title: { 7 type: "string", 8 minLength: 20, 9 maxLength: 80 10 } 11 } 12 } 13 }
Agora, se tentarmos inserir um documento em nossa coleção
posts
por meio do driver Node.js ou do Compass, obteremos um erro.Há muitas outras regras e validações que você pode adicionar. Confira a lista completa aqui. Para uma abordagem guiada mais avançada, confira os artigos sobre a validação de esquema com arrays e dependências.
Com o Mongoose, nosso modelo de dados e esquema são a base para nossas interações com o MongoDB. O MongoDB em si não está ciente de nenhuma dessas restrições. O Mongoose assume o papel de juiz, júri e executor sobre quais queries podem ser executadas e o que acontece com elas.
Mas com a validação de esquema nativo do MongoDB, temos mais flexibilidade. Quando implementamos um esquema, a validação em documentos existentes não ocorre automaticamente. A validação é feita somente em atualizações e inserções. No entanto, se quisermos deixar os documentos existentes como estão, podemos alterar o
validationLevel
para validar apenas os novos documentos inseridos no banco de dados.Além disso, com as validações de esquema feitas no nível do MongoDB database, podemos optar por ainda inserir documentos que falham na validação. A opção
validationAction
nos permite determinar o que acontece se uma query falhar na validação. Por padrão, ela é definida como error
, mas podemos alterá-la para warn
se quisermos que a inserção ainda ocorra. Agora, em vez de uma inserção ou atualização com erro, ela simplesmente avisaria o usuário que a operação falhou na validação.E, finalmente, se precisarmos, podemos ignorar completamente a validação do documento passando a opção
bypassDocumentValidation
com nossa query. Para mostrar como isso funciona, digamos que queremos inserir apenas um title
em nossa coleção posts
e não queremos nenhum outro dado. Se tentarmos fazer isso...1 db.collection('posts').insertOne({ title: 'Awesome' });
... receberíamos um erro informando que a validação do documento falhou. Mas se quisermos ignorar a validação do documento para essa inserção, basta fazer isso:
1 db.collection('posts').insertOne( 2 { title: 'Awesome' }, 3 { bypassDocumentValidation: true } 4 );
Isso não seria possível com o Mongoose. A validação do esquema do MongoDB está mais alinhada com toda a filosofia do MongoDB, em que o foco está em um esquema de design flexível que seja rápido e facilmente adaptável aos seus casos de uso.
A área final em que eu gostaria de comparar o Mongoose e o driver Node.js do MongoDB é no suporte para pseudojunções. Tanto o Mongoose quanto o driver nativo do Node.js oferecem a capacidade de combinar documentos de várias coleções no mesmo banco de dados, semelhante a uma junção em bancos de dados relacionais tradicionais.
A abordagem do Mongoose é chamada de Preencher. Ela permite que os desenvolvedores criem modelos de dados que podem fazer referência uns aos outros e, em seguida, com uma API simples, solicitar dados de várias coleções. Para o nosso exemplo, vamos expandir a publicação no blog e adicionar uma nova coleção para usuários.
1 const user = new Schema({ 2 name: String, 3 email: String 4 }); 5 6 const blog = new Schema({ 7 title: String, 8 slug: String, 9 published: Boolean, 10 content: String, 11 tags: [String], 12 comments: [{ 13 user: { Schema.Types.ObjectId, ref: 'User' }, 14 content: String, 15 votes: Number 16 }] 17 }); 18 19 const User = mongoose.model('User', user); 20 const Blog = mongoose.model('Blog', blog);
O que fizemos acima foi criar um novo modelo e esquema para representar os usuários que deixam comentários nos posts do blog. Quando um usuário deixa um comentário, em vez de armazenar informações sobre ele, armazenamos apenas o
_id
desse usuário. Portanto, uma operação de atualização para adicionar um novo comentário à nossa postagem pode ter a seguinte aparência:1 Blog.updateOne({ 2 comments: [{ user: "12345", content: "Great Post!!!" }] 3 });
Isso pressupõe que temos um usuário em nossa coleção
User
com o _id
de 12345
. Agora, se quisermos preencher nossa propriedade user
quando fizermos uma query – e em vez de apenas retornar _id
retornar o documento inteiro – poderíamos fazer:1 Blog. 2 findOne({}). 3 populate('comments.user'). 4 exec(function (err, post) { 5 console.log(post.comments[0].user.name) // Name of user for 1st comment 6 });
O preenchimento juntamente com a modelagem de dados Mongoose pode ser muito poderoso, especialmente se você vem de um background de banco de dados relacional. A desvantagem, porém, é a quantidade de "mágica" que acontece nos bastidores para que isso ocorra. O Mongoose faria duas queries separadas para realizar essa tarefa e, se você estiver unindo várias coleções, as operações poderiam rapidamente desacelerar.
O outro problema é que o conceito de preenchimento só existe na camada do aplicativo. Portanto, embora isso funcione, contar com ele para o gerenciamento do banco de dados pode prejudicá-lo no futuro.
O MongoDB, a partir da versão 3.2, introduziu uma nova operação chamada
$lookup
que permite aos desenvolvedores fazer um left outer join em uma coleção dentro de um único MongoDB database. Se quiséssemos preencher as informações do usuário usando o Node.js, poderíamos criar um pipeline de agregação para fazer isso. Nosso ponto de partida usando o operador $lookup
poderá ter a seguinte aparência:1 db.collection('posts').aggregate([ 2 { 3 '$lookup': { 4 'from': 'users', 5 'localField': 'comments.user', 6 'foreignField': '_id', 7 'as': 'users' 8 } 9 }, {} 10 ], (err, post) => { 11 console.log(post.users); //This would contain an array of users 12 });
Poderemos criar uma etapa adicional em nosso pipeline de agregação para substituir as informações do usuário no campo
comments
pelos dados do usuário, mas isso está um pouco fora do escopo deste artigo. Se você quiser saber mais sobre como os pipelines de agregação funcionam com o MongoDB, confira os docs de agregação.Tanto o driver Mongoose quanto o MongoDB Node.js oferecem suporte a uma funcionalidade semelhante. Embora o Mongoose torne o desenvolvimento do MongoDB familiar para alguém que pode ser completamente novo, ele realiza muitas coisas secretas que podem ter consequências não intencionais no futuro.
Pessoalmente, acredito que você não precisa de um ODM para ter sucesso com o MongoDB. Também não sou muito fã de ORMs no mundo dos bancos de dados relacionais. Embora eles façam o salto inicial em uma tecnologia parecer familiar, eles abstraem grande parte do poder de um banco de dados.
Os desenvolvedores têm muitas escolhas a fazer ao criar aplicativos. Neste artigo, analisamos as diferenças entre usar um ODM e o driver nativo e mostramos que a diferença entre os dois não é tão grande. Usar um ODM como o Mongoose pode fazer o desenvolvimento parecer familiar, mas força um design rígido, que é um antipadrão ao considerar o desenvolvimento com o MongoDB.
O driver MongoDB Node.js funciona nativamente com seu MongoDB database para oferecer a melhor e mais flexível experiência de desenvolvimento. Ele permite que o banco de dados faça o que faz de melhor e que seu aplicativo se concentre no que faz de melhor, que provavelmente não é gerenciar modelos de dados.