loading...
Cover image for Mongoose: Como find() funciona

Mongoose: Como find() funciona

oieduardorabelo profile image Eduardo Rabelo ・6 min read

alguma confusão na internet sobre o que acontece quando você chama Model.find() no Mongoose. Não se engane, Model.find() faz o que você espera: encontra todos os documentos que correspondem a uma consulta. Mas há alguma confusão entre Model.find() e Query#find(), opções de configuração, suporte a Promises. Neste artigo, fornecerei uma visão geral conceitual do que acontece quando você chama Model.find() para poder responder a perguntas semelhantes.

Configuração

Para os fins deste artigo, presumo que você já tenha uma instância do MongoDB em execução no localhost:27017. Caso contrário, confira o run-rs, ele baixa e executa o MongoDB para você, sem dependências além do Node.js. Aqui está um script independente que demonstra a criação de alguns documentos e o uso de .find():

const mongoose = require('mongoose');

run().catch(error => console.log(error.stack));

async function run() {
  await mongoose.connect('mongodb://localhost:27017/test', { useNewUrlParser: true });

  // Limpa o banco de dados todas as vezes.
  // Apenas para nosso exemplo.
  // Não faça isso em prod! :)
  await mongoose.connection.dropDatabase();

  const customerSchema = new mongoose.Schema({ name: String, age: Number, email: String });
  const Customer = mongoose.model('Customer', customerSchema);

  await Customer.create({ name: 'A', age: 30, email: 'a@foo.bar' });
  await Customer.create({ name: 'B', age: 28, email: 'b@foo.bar' });

  // Acha todos os Customers
  const docs = await Customer.find();
  console.log(docs);
}

Modelos e Consultas

O Mongoose realmente tem duas funções find(). O exemplo acima usa Model.find(), mas também existe o Query#find(). O Query#find() é abreviado para Query.prototype.find(), o .find() é um método de instância da classe Query.

Já o Model.find() retorna uma instância da classe Query do Mongoose. A classe Query representa uma operação CRUD crua (raw) que você pode enviar para o MongoDB. Ele fornece uma interface encadeada para criar consultas mais sofisticadas. Você não instancia uma Query diretamente, o Customer.find() instancia um para você.

const query = Customer.find();
query instanceof mongoose.Query; // true
const docs = await query; // Pega os documentos

Então, Model.find() retorna uma instância de Query. Você pode encadear chamadas .find() para adicionar operadores de consulta adicionais, também conhecidos como filtros. Por exemplo, as duas consultas a seguir encontrarão todos os clientes que o email contenha 'foo.bar' e cujo age mínimo seja 30.

// Primeiro parâmetro do "find()" é um objeto que contem os operadores da consulta, veja:
// https://docs.mongodb.com/manual/reference/operator/query/
Customer.find({ email: /foo\.bar/, age: { $gte: 30 } });

// Equivalente:
Customer.find({ email: /foo\.bar/ }).find({ age: { $gte: 30 } });

Os objetos de consulta têm vários auxiliares para criar operações sofisticadas de CRUD. Os mais utilizados são Query#sort(), Query#limit() e Query#skip().

// Encontre o primeiro customer em ordem alfabética, nesse caso "A".
// Você pode usar `{ name: -1 }` para sortear em ordem reversa.
const res = await Customer.find({}).sort({ name: 1 }).limit(1);

// Encontre o segundo customer em ordem alfabética, nesse caso "B"
const res2 = await Customer.find({}).sort({ name: 1 }).skip(1).limit(1);

Uma grande vantagem do Mongoose é que o Mongoose lança consultas para corresponder ao esquema do modelo. Isso significa que você não precisa converter explicitamente cadeias de caracteres para ObjectIds ou se preocupar com as nuances de converter cadeias de caracteres em números.

// Mongoose irá converter `_id` de string para ObjectID e `age.$gte` para número, ou irá jogar um error se ele falhar em qualquer um desses valores
Customer.find({ _id: res[0]._id.toHexString(), age: { $gte: '25' } });

Opções de configuração

Os ajudantes sort(), limit() e skip() modificam as opções de consulta. Por exemplo, o query.getOptions() abaixo retornará um objeto que contém sort e limit propriedades.

const query = Customer.find().sort({ name: 1 }).limit(1);
query.getOptions(); // { sort: { name: 1 }, limit: 1 }

A função Model.find() usa três argumentos que ajudam a inicializar uma consulta sem encadeamento. O primeiro argumento é o filtro de consulta (também conhecido como conditions). O segundo argumento é a projeção da consulta, que define quais campos incluir ou excluir da consulta. Por exemplo, se você deseja excluir o campo email por questões de privacidade, pode usar uma das sintaxes abaixo.

// Explicitamente remove o `email` usando o 2º argumento. Use `email: 1` para incluir _apenas_ a propriedade `email`
Customer.find({}, { email: 0 });

// Abordagem equivalente usando encadeamento
Customer.find().select({ email: 0 });

O terceiro argumento de Model.find() é as opções gerais de consulta. Aqui está uma lista completa de opções . Por exemplo, você pode definir limit e skip no terceiro argumento.

const res = await Customer.find({}, null, { sort: { name: 1 }, limit: 1 });
res[0].name; // 'A'

Perceba que o Model.find() tem uma assinatura de função diferente do que a função collection.find() do driver do MongoDB. O driver do MongoDB leva apenas 2 argumentos, filter e options. Para converter uma chamada find() de driver do MongoDB em uma chamada Model.find() do Mongoose, sem encadeamento, adicione null como o segundo argumento.

// consulta com driver do MongoDB
client.db().collection('customers').find({ email: /foo\.bar/ }, { limit: 1 });

// Equivalente em Mongoose
Customer.find({ email: /foo\.bar/ }, null, { limit: 1 });

// Equivalente em Mongoose usando encadeamento
Customer.find({ email: /foo\.bar/ }).limit(1);

Promises e Async/Await

Model.find() retorna uma instância de consulta, então por que você pode fazer await Model.find()? Isso ocorre porque uma consulta do Mongoose é um thenable, o que significa que eles têm uma função .then(). Isso significa que você pode usar as consultas da mesma maneira que Promises, inclusive com o encadeamento de delas, como mostrado abaixo.

Customer.find({ name: 'A' }).
  then(customers => {              
    console.log(customers[0].name); // 'A'
    return Customer.find({ name: 'B' });
  }).
  then(customers => {
    console.log(customers[0].name); // 'B'
  });

As consultas também têm uma função .catch(). Em geral, um thenable não precisa ter uma função .catch(), mas o Mongoose adicionou uma para sua conveniência. Abaixo está um exemplo de .catch() e como manipular um número incorreto na sua consulta.

Customer.find({ age: 'not a number' }).
  catch(err => console.log('Caught:', err.message));
// Caught: Cast to number failed for value "not a number" at path "age" for model "Customer"

As consultas são editáveis, mas as consultas não são Promises. Em alguns casos, você pode precisar de uma Promise, não apenas de um thenable. Por exemplo, você pode estar usando TypeScript em modo strict ou pode estar usando o módulo cls-hooked. A função Query#exec() retorna uma Promise completa.

const q = Customer.find();
q instanceof Promise; // false
q.exec() instanceof Promise; // true

Continuando

Encontrar todos os documentos que correspondem a uma consulta no Mongoose é intuitivo, mas há nuances que aparecem quando você vai além das consultas mais básicas. O Mongoose permite estruturar consultas usando encadeamento ou, equivalentemente, usando POJOs em uma única chamada de função. Model.find() retorna uma consulta, que possui um método .find() separado que permite anexar filtros adicionais. As consultas não são Promises, mas são próximas o suficiente para a maioria dos usos práticos. Lembre-se desses três conceitos e você saberá o suficiente para resolver os problemas mais comuns do Mongoose.

Créditos

Discussion

pic
Editor guide