There's some confusion on the internet about what happens when you call Model.find()
in Mongoose. Make no mistake, Model.find()
does what you expect: find all documents that match a query. But there's some confusion about Model.find()
vs Query#find()
, setting options, promise support. In this article, I'll provide a conceptual overview of what happens when you call Model.find()
so you can answer similar questions for yourself.
Setup
For the purposes of this article, I'll assume you already have a MongoDB instance
running on localhost:27017
. If you don't, check out
run-rs, it downloads and runs MongoDB
for you with no dependencies beyond Node.js. Here's a standalone script that
demonstrates creating some documents and using find()
:
const mongoose = require('mongoose');
run().catch(error => console.log(error.stack));
async function run() {
await mongoose.connect('mongodb://localhost:27017/test', { useNewUrlParser: true });
// Clear the database every time. This is for the sake of example only,
// don't do this in 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' });
// Find all customers
const docs = await Customer.find();
console.log(docs);
}
Models and Queries
Mongoose actually has two find()
functions. The above standalone example uses
Model.find()
, but
there's also Query#find()
.
Query#find()
is shorthand for Query.prototype.find()
, find()
is an instance
method on the Query
class.
The Model.find()
function returns an instance of Mongoose's Query
class. The
Query
class represents a raw CRUD operation that you may send to MongoDB.
It provides a chainable
interface for building up more sophisticated queries. You don't instantiate a
Query
directly, Customer.find()
instantiates one for you.
const query = Customer.find();
query instanceof mongoose.Query; // true
const docs = await query; // Get the documents
So Model.find()
returns an instance of the Query
class. You can chain find()
calls to add additional query operators, also known as filters, to the query.
For example, both of the following queries will find all customers whose
email
contains 'foo.bar' and whose age
is at least 30.
// First parameter to `find()` is an object that contains query operators, see:
// https://docs.mongodb.com/manual/reference/operator/query/
Customer.find({ email: /foo\.bar/, age: { $gte: 30 } });
// Equivalent:
Customer.find({ email: /foo\.bar/ }).find({ age: { $gte: 30 } });
Query objects have numerous helpers
for building up sophisticated CRUD operations. The most commonly used ones are
Query#sort()
,
Query#limit()
, and
Query#skip()
.
// Find the customer whose name comes first in alphabetical order, in
// this case 'A'. Use `{ name: -1 }` to sort by name in reverse order.
const res = await Customer.find({}).sort({ name: 1 }).limit(1);
// Find the customer whose name comes _second_ in alphabetical order, in
// this case 'B'
const res2 = await Customer.find({}).sort({ name: 1 }).skip(1).limit(1);
One major advantage of using Mongoose is that Mongoose casts queries to match the model's schema. This means you don't explicitly need to convert strings to ObjectIds or worry about the nuances of converting strings to numbers.
// Mongoose will convert `_id` from a string to an ObjectId, and `age.$gte`
// into a number, or throw an error if it failed to convert these values.
Customer.find({ _id: res[0]._id.toHexString(), age: { $gte: '25' } });
Setting Options
The sort()
, limit()
, and skip()
helpers modify the query's
options. For
example, query.getOptions()
below will return an object that contains sort
and limit
properties.
const query = Customer.find().sort({ name: 1 }).limit(1);
query.getOptions(); // { sort: { name: 1 }, limit: 1 }
The Model.find()
function takes 3 arguments that help you initialize a query
without chaining. The first argument is the query filter (also known as conditions
).
The 2nd argument is the query projection,
which defines what fields to include or exclude from the query. For example, if
you want to exclude the customer's email
for privacy concerns, you can use
either of the below syntaxes.
// Explicitly exclude `email` using the 2nd argument. Use `email: 1` to
// include _only_ the `email` property.
Customer.find({}, { email: 0 });
// Equivalent approach using chaining
Customer.find().select({ email: 0 });
The 3rd argument to Model.find()
is the general query options. Here's a
full list of options.
For example, you can set limit
and skip
in the 3rd argument.
const res = await Customer.find({}, null, { sort: { name: 1 }, limit: 1 });
res[0].name; // 'A'
Note that Mongoose's Model.find()
has a different function signature than the
MongoDB driver's collection.find()
function.
The MongoDB driver only takes 2 arguments, filter
and options
. To convert
a MongoDB driver find()
call to a Mongoose Model.find()
call without chaining,
add null
as the 2nd argument.
// MongoDB driver query
client.db().collection('customers').find({ email: /foo\.bar/ }, { limit: 1 });
// Equivalent Mongoose query
Customer.find({ email: /foo\.bar/ }, null, { limit: 1 });
// Equivalent Mongoose query using chaining
Customer.find({ email: /foo\.bar/ }).limit(1);
Promises and Async/Await
Model.find()
returns a query instance, so why can you do await Model.find()
?
That's because a Mongoose query is a
thenable,
meaning they have a then()
function.
That means you can use queries in the same way you use promises, including with
promise chaining
as shown below.
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'
});
Queries also have a catch()
function.
In general, a thenable doesn't need to have a catch()
function, but Mongoose
added one for your convenience. Below is an example of using catch()
to handle
a malformed number
in your query.
// Caught: Cast to number failed for value "not a number" at path "age" for
// model "Customer"
Customer.find({ age: 'not a number' }).
catch(err => console.log('Caught:', err.message));
Queries are thenables, but queries are not promises.
In some cases, you might need a promise, not just a thenable. For example, you
may have strict TypeScript bindings or you may be using the cls-hooked
module. The Query#exec()
function
returns a fully fledged promise.
const q = Customer.find();
q instanceof Promise; // false
q.exec() instanceof Promise; // true
Moving On
Finding all documents that match a query in Mongoose is intuitive, but there's
nuances that pop up once you go beyond the most basic queries. Mongoose lets you
structure queries using chaining or, equivalently, using POJOs in a single
function call. Model.find()
returns a query, which has a separate find()
method that lets you attach additional filters. Queries are not promises,
but close enough for most practical uses. Remember these 3 concepts and you'll
know enough to address most common Mongoose issues.
Want to become your team's MongoDB expert? "Mastering Mongoose" distills 8 years of hard-earned lessons building Mongoose apps at scale into 153 pages. That means you can learn what you need to know to build production-ready full-stack apps with Node.js and MongoDB in a few days. Get your copy!