Mongoose $lookup operator (with examples)

Updated: December 30, 2023 By: Guest Contributor Post a comment

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.