Introduction
Mongoose is a MongoDB object modeling tool designed to work in an asynchronous environment such as Node.js. The $lookup
stage in MongoDB’s aggregation pipeline is akin to a SQL JOIN, allowing documents from one collection to be joined with documents from another based on a specified condition. Here, we embark on a journey through MongoDB’s $lookup
operator, exploring its power and versatility through practical examples using Mongoose.
Basic Usage
To start with $lookup
, imagine you have two collections: orders
and products
. You want to enrich order documents with the corresponding product information:
const orderSchema = new mongoose.Schema({ productId: String, quantity: Number });
const Order = mongoose.model('Order', orderSchema);
const productSchema = new mongoose.Schema({ name: String, price: Number });
const Product = mongoose.model('Product', productSchema);
async function enrichOrdersWithProducts() {
return Order.aggregate([
{
$lookup: {
from: 'products',
localField: 'productId',
foreignField: '_id',
as: 'productDetails'
}
}
]);
}
await enrichOrdersWithProducts();
This simple example showcases the most straightforward use of the $lookup
operator to merge documents across collections.
Handling Many-to-Many Relationships
Sometimes you may face situations where an order may contain multiple products, and a product can also be part of multiple orders. Managing many-to-many relationships is a step further in complex querying:
const orderProductSchema = new mongoose.Schema({ orderId: mongoose.SchemaTypes.ObjectId, productId: mongoose.SchemaTypes.ObjectId });
const OrderProduct = mongoose.model('OrderProduct', orderProductSchema);
async function getOrderDetailsWithProducts() {
return Order.aggregate([
{
$lookup: {
from: 'orderproducts',
localField: '_id',
foreignField: 'orderId',
as: 'productConnections'
}
},
{
$unwind: '$productConnections'
},
{
$lookup: {
from: 'products',
localField: 'productConnections.productId',
foreignField: '_id',
as: 'productDetails'
}
},
{
$group: {
_id: '$_id',
products: { $push: '$productDetails' }
}
}
]);
}
await getOrderDetailsWithProducts();
In the above code, we first join the orders
with orderproducts
and then join the resulting documents with products
. We also make use of $unwind
and $group
to properly structure the resulting documents.
Pipeline inside $lookup
MongoDB allows for the nesting of aggregation pipelines within $lookup
for more refined querying:
async function getOrdersWithTotal() {
return Order.aggregate([
{ $match: { status: 'complete' } },
{
$lookup: {
from: 'orderproducts',
let: { orderId: '$_id' },
pipeline: [
{ $match: { $expr: { $eq: ['$orderId', '$orderId'] } } },
{ $lookup: {
from: 'products',
localField: 'productId',
foreignField: '_id',
as: 'product'
}
},
{ $unwind: '$product' },
{ $group: {
_id: '$orderId',
total: { $sum: { $multiply: ['$product.price', '$quantity'] } }
}
}
],
as: 'orderTotal'
}
}
]);
}
await getOrdersWithTotal();
This advanced usage of the $lookup
stage demonstrates nesting of the full-fledged pipelines inside it. It involves looking up order products from a filtered set of orders and calculating the total order amount.
Conclusion
Throughout this article, we have delved into the mechanics of MongoDB’s $lookup
via Mongoose, ranging from basic one-to-one joins, dealing with many-to-many relationships, to utilizing nested aggregation pipelines to perform complex queries. Tools like $lookup
significantly broaden the query capabilities of MongoDB, allowing us to perform joins and resemble relational data behaviors in a non-relational database, echoing the benefits of SQL without leaving behind NoSQL’s strengths and flexibility. By mastering these examples, you can enrich your models in rich and performant ways.