MongoDB Basics

Understanding NoSQL Database Concepts and MongoDB Integration

Understanding MongoDB in the NoSQL Landscape

MongoDB is a popular document-oriented NoSQL database designed for scalability, flexibility, and performance. Unlike relational databases that store data in tables with rigid schemas, MongoDB stores data in flexible, JSON-like documents where fields can vary from document to document.

flowchart TD A[NoSQL Databases] --> B[Document Stores] A --> C[Key-Value Stores] A --> D[Column-Family Stores] A --> E[Graph Databases] B --> B1[MongoDB] B --> B2[CouchDB] B --> B3[Firestore] C --> C1[Redis] C --> C2[DynamoDB] D --> D1[Cassandra] D --> D2[HBase] E --> E1[Neo4j] E --> E2[ArangoDB]

Key MongoDB Concepts

Document

The basic unit of data in MongoDB, similar to a row in a relational database but with a flexible schema. Documents are stored in BSON (Binary JSON) format.


{
  "_id": ObjectId("60a91cd3b3e55a0015c0e980"),
  "name": "John Smith",
  "email": "john@example.com",
  "age": 30,
  "address": {
    "street": "123 Main St",
    "city": "Anytown",
    "state": "CA",
    "zip": "12345"
  },
  "interests": ["programming", "hiking", "reading"]
}
                    
Collection

A grouping of MongoDB documents, analogous to a table in relational databases. Collections do not enforce schema on documents.

users Collection Document 1 {"name": "John", ...} Document 2 {"name": "Jane", ...} Document 3 {"name": "Bob", ...}
Database

A container for collections, similar to a database in relational systems. A single MongoDB server can host multiple databases.

Field

A key-value pair within a document. Fields can contain various data types, including other documents and arrays.

_id

A unique identifier automatically added to each document, functioning as a primary key. Defaults to an ObjectId, but can be overridden.

MongoDB vs. Relational Databases

Relational (e.g., PostgreSQL) MongoDB
Tables Collections
Rows Documents
Columns Fields
Joins Embedded Documents & References
Fixed Schema Dynamic Schema
SQL JSON-based Query Language
ACID Transactions (full) ACID Transactions (since v4.0, with limitations)

When to Choose MongoDB

MongoDB excels in specific scenarios:

  • Rapid Development: When you need to iterate quickly and your schema may evolve
  • Semi-structured or Unstructured Data: When data doesn't fit neatly into tabular format
  • Hierarchical Data Storage: When you have nested, complex data structures
  • High Write Load: For applications with heavy write operations
  • Horizontal Scaling: When you need to scale across multiple servers easily
  • Content Management: For storing content with varying attributes
  • Real-time Analytics: For applications requiring fast, simple analytics
  • Prototyping: For quickly building MVPs or proof of concepts

Real-World Examples: Content management systems, mobile apps, IoT applications, gaming applications, event logging, and real-time analytics.

Installing and Setting Up MongoDB

Let's start by installing MongoDB on your development machine. MongoDB offers Community Edition (free) and Enterprise Edition (commercial, with additional features).

Windows Installation

  1. Download the MongoDB Community Server installer from MongoDB Download Center
  2. Run the MSI installer and follow the setup wizard
  3. Choose "Complete" installation type
  4. Optionally install MongoDB Compass (GUI tool)
  5. MongoDB will be installed as a Windows service by default

macOS Installation

Using Homebrew (Recommended)

# Install Homebrew if you don't already have it
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

# Install MongoDB Community Edition
brew tap mongodb/brew
brew install mongodb-community

# Start MongoDB service
brew services start mongodb-community
                    

Linux Installation (Ubuntu)


# Import MongoDB public GPG key
wget -qO - https://www.mongodb.org/static/pgp/server-6.0.asc | sudo apt-key add -

# Create a list file for MongoDB
echo "deb [ arch=amd64,arm64 ] https://repo.mongodb.org/apt/ubuntu focal/mongodb-org/6.0 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-6.0.list

# Update package database
sudo apt-get update

# Install MongoDB packages
sudo apt-get install -y mongodb-org

# Start MongoDB service
sudo systemctl start mongod

# Enable MongoDB to start on boot
sudo systemctl enable mongod
                    

Docker Installation

Using Docker is a clean, cross-platform way to run MongoDB:


# Pull the MongoDB image
docker pull mongo

# Run MongoDB container
docker run --name mongodb -d -p 27017:27017 mongo

# To run with authentication enabled
docker run --name mongodb -d -p 27017:27017 \
    -e MONGO_INITDB_ROOT_USERNAME=admin \
    -e MONGO_INITDB_ROOT_PASSWORD=password \
    mongo
                    

MongoDB Atlas: Cloud-Hosted Option

MongoDB Atlas is the official fully-managed cloud database service. It's an excellent option for development and production deployments without the overhead of managing infrastructure.

  1. Go to MongoDB Atlas and create an account
  2. Create a new project
  3. Build a cluster (the free tier is sufficient for development)
  4. Configure database access (create a user with password)
  5. Configure network access (whitelist your IP address)
  6. Get your connection string from the "Connect" button
⚠️ Security Note

For production use, always secure your MongoDB installation:

  • Enable authentication
  • Configure network security (firewalls)
  • Use TLS/SSL for encrypted connections
  • Follow the principle of least privilege for user permissions

MongoDB Compass: GUI Tool

MongoDB Compass is the official GUI for MongoDB, providing a visual interface to explore and manipulate your data.

  1. Download MongoDB Compass from MongoDB Compass Download
  2. Install and open the application
  3. Connect to your MongoDB instance using the connection string

MongoDB CRUD Operations

Let's explore the basic CRUD (Create, Read, Update, Delete) operations in MongoDB using the MongoDB Shell, a command-line interface for interacting with MongoDB.

Accessing the MongoDB Shell


# Connect to local MongoDB instance
mongosh

# Connect to a specific database
mongosh mydatabase

# Connect with authentication
mongosh --username myuser --password mypassword --authenticationDatabase admin
            

Creating and Switching Databases


// Show all databases
show dbs

// Switch to a database (creates it if it doesn't exist)
use ecommerce

// Check current database
db
            

Creating Collections


// Explicitly create a collection
db.createCollection("products")

// Show all collections
show collections

// Collections are also created implicitly when you insert documents
db.users.insertOne({ name: "John", email: "john@example.com" })
            

Create Operations


// Insert a single document
db.products.insertOne({
  name: "Smartphone",
  price: 699.99,
  category: "Electronics",
  specs: {
    brand: "TechCo",
    model: "X1",
    storage: "128GB",
    color: "Black"
  },
  inStock: true,
  tags: ["smartphone", "tech", "mobile"]
})

// Insert multiple documents
db.products.insertMany([
  {
    name: "Laptop",
    price: 1299.99,
    category: "Electronics",
    specs: {
      brand: "TechCo",
      model: "UltraBook",
      processor: "Core i7",
      ram: "16GB",
      storage: "512GB SSD"
    },
    inStock: true,
    tags: ["laptop", "computer", "tech"]
  },
  {
    name: "Headphones",
    price: 199.99,
    category: "Electronics",
    specs: {
      brand: "AudioPro",
      model: "SoundMax",
      type: "Over-ear",
      wireless: true
    },
    inStock: false,
    tags: ["audio", "headphones", "wireless"]
  }
])
            

Read Operations


// Find all documents in a collection
db.products.find()

// Pretty-print results
db.products.find().pretty()

// Find with a simple filter
db.products.find({ category: "Electronics" })

// Find with multiple conditions
db.products.find({
  category: "Electronics",
  price: { $lt: 1000 },
  inStock: true
})

// Find with nested fields
db.products.find({ "specs.brand": "TechCo" })

// Find with array conditions
db.products.find({ tags: "wireless" })

// Find one document
db.products.findOne({ name: "Laptop" })

// Find with projection (selecting fields)
db.products.find(
  { category: "Electronics" },
  { name: 1, price: 1, _id: 0 } // 1 to include, 0 to exclude
)
            

Query Operators


// Comparison operators
db.products.find({ price: { $gt: 500 } })     // Greater than
db.products.find({ price: { $gte: 500 } })    // Greater than or equal
db.products.find({ price: { $lt: 300 } })     // Less than
db.products.find({ price: { $lte: 300 } })    // Less than or equal
db.products.find({ price: { $ne: 699.99 } })  // Not equal
db.products.find({ price: { $in: [199.99, 699.99] } }) // In array
db.products.find({ price: { $nin: [199.99, 699.99] } }) // Not in array

// Logical operators
db.products.find({
  $and: [
    { price: { $gt: 500 } },
    { inStock: true }
  ]
})

db.products.find({
  $or: [
    { price: { $lt: 200 } },
    { price: { $gt: 1000 } }
  ]
})

db.products.find({
  price: { $not: { $gt: 1000 } }
})

// Element operators
db.products.find({ specs: { $exists: true } })
db.products.find({ price: { $type: "number" } })

// Array operators
db.products.find({ tags: { $all: ["tech", "mobile"] } })
db.products.find({ tags: { $size: 3 } })
            

Sorting, Limiting, and Skipping


// Sort results (1 for ascending, -1 for descending)
db.products.find().sort({ price: 1 })

// Limit results
db.products.find().limit(2)

// Skip results (for pagination)
db.products.find().skip(1).limit(2)

// Combining methods
db.products.find({ category: "Electronics" })
  .sort({ price: -1 })
  .skip(1)
  .limit(2)
            

Update Operations


// Update a single document
db.products.updateOne(
  { name: "Smartphone" },
  { $set: { price: 649.99, "specs.color": "Blue" } }
)

// Update multiple documents
db.products.updateMany(
  { category: "Electronics" },
  { $set: { onSale: true } }
)

// Increment values
db.products.updateOne(
  { name: "Laptop" },
  { $inc: { price: 100 } }
)

// Add to arrays
db.products.updateOne(
  { name: "Headphones" },
  { $push: { tags: "premium" } }
)

// Remove from arrays
db.products.updateOne(
  { name: "Headphones" },
  { $pull: { tags: "wireless" } }
)

// Update or insert (upsert)
db.products.updateOne(
  { name: "Tablet" },
  { 
    $set: { 
      price: 399.99,
      category: "Electronics",
      inStock: true
    }
  },
  { upsert: true }
)

// Replace a document
db.products.replaceOne(
  { name: "Smartphone" },
  {
    name: "Smartphone",
    price: 799.99,
    category: "Electronics",
    specs: {
      brand: "TechCo",
      model: "X2",
      storage: "256GB",
      color: "Silver"
    },
    inStock: true,
    tags: ["smartphone", "tech", "mobile", "flagship"]
  }
)
            

Delete Operations


// Delete a single document
db.products.deleteOne({ name: "Headphones" })

// Delete multiple documents
db.products.deleteMany({ inStock: false })

// Delete all documents in a collection
db.products.deleteMany({})

// Drop a collection
db.products.drop()

// Drop a database
db.dropDatabase()
            

Data Modeling in MongoDB

Data modeling in MongoDB is fundamentally different from relational databases. Instead of normalizing data across tables, MongoDB encourages embedding related data in a single document when appropriate.

Key Data Modeling Concepts

Embedding vs. Referencing

Embedding (Denormalized)

Include related data within the same document:


// User document with embedded addresses
{
  "_id": ObjectId("..."),
  "name": "John Smith",
  "email": "john@example.com",
  "addresses": [
    {
      "type": "home",
      "street": "123 Main St",
      "city": "Anytown",
      "state": "CA",
      "zip": "12345"
    },
    {
      "type": "work",
      "street": "456 Business Ave",
      "city": "Workville",
      "state": "CA",
      "zip": "67890"
    }
  ]
}
                    

Advantages:

  • Retrieves all related data in a single query
  • Better performance for read operations
  • Maintains data locality
  • Atomic updates

Disadvantages:

  • Documents can grow larger over time
  • Can't access embedded data independently
  • Duplication if data is needed in multiple places

When to use: For "contains" relationships, when data is always accessed together, for smaller data sets, for one-to-few relationships.

Referencing (Normalized)

Store references to documents in other collections:


// User document with references to addresses
{
  "_id": ObjectId("..."),
  "name": "John Smith",
  "email": "john@example.com",
  "address_ids": [
    ObjectId("address1..."),
    ObjectId("address2...")
  ]
}

// Address collection
{
  "_id": ObjectId("address1..."),
  "user_id": ObjectId("..."),
  "type": "home",
  "street": "123 Main St",
  "city": "Anytown",
  "state": "CA",
  "zip": "12345"
}
                    

Advantages:

  • Avoids data duplication
  • Keeps documents smaller
  • Better for many-to-many relationships
  • Allows independent access to referenced data

Disadvantages:

  • Requires multiple queries or complex aggregations
  • No atomic updates across multiple documents
  • Can be slower for reads

When to use: For one-to-many or many-to-many relationships, when data is frequently updated, for larger data sets, when data is accessed independently.

Common Data Modeling Patterns

1. One-to-Few: Embedded Documents

// Product with a few variants
{
  "_id": ObjectId("..."),
  "name": "T-Shirt",
  "base_price": 19.99,
  "variants": [
    {
      "size": "S",
      "color": "Blue",
      "price": 19.99,
      "stock": 25
    },
    {
      "size": "M",
      "color": "Blue",
      "price": 19.99,
      "stock": 30
    },
    {
      "size": "L",
      "color": "Blue",
      "price": 22.99,
      "stock": 15
    }
  ]
}
            
2. One-to-Many: Array of References

// Author with many books
{
  "_id": ObjectId("author1..."),
  "name": "Jane Author",
  "bio": "Bestselling author of...",
  "book_ids": [
    ObjectId("book1..."),
    ObjectId("book2..."),
    ObjectId("book3...")
  ]
}

// Books collection
{
  "_id": ObjectId("book1..."),
  "title": "My First Book",
  "author_id": ObjectId("author1..."),
  "published_date": ISODate("2020-01-15"),
  "pages": 320
}
            
3. One-to-Squillions: References in the "Many" Side

// User document (no references to logs)
{
  "_id": ObjectId("user1..."),
  "username": "jsmith",
  "email": "john@example.com"
}

// Log entries with references to user
{
  "_id": ObjectId("log1..."),
  "user_id": ObjectId("user1..."),
  "action": "login",
  "timestamp": ISODate("2023-04-15T08:30:00Z"),
  "ip": "192.168.1.1"
}
            
4. Many-to-Many: Array of References on Both Sides

// Student with references to courses
{
  "_id": ObjectId("student1..."),
  "name": "Alice Student",
  "course_ids": [
    ObjectId("course1..."),
    ObjectId("course2...")
  ]
}

// Course with references to students
{
  "_id": ObjectId("course1..."),
  "title": "Database Design",
  "student_ids": [
    ObjectId("student1..."),
    ObjectId("student2..."),
    ObjectId("student3...")
  ]
}
            
5. Subset Pattern: Embedding a Subset of Information

// Product with most important information embedded
{
  "_id": ObjectId("order1..."),
  "user_id": ObjectId("user1..."),
  "date": ISODate("2023-04-15"),
  "status": "shipped",
  "items": [
    {
      "product_id": ObjectId("product1..."),
      "product_name": "Smartphone X1", // Embed essential product info
      "price": 699.99,
      "quantity": 1
    },
    {
      "product_id": ObjectId("product2..."),
      "product_name": "Wireless Headphones",
      "price": 199.99,
      "quantity": 1
    }
  ],
  "total": 899.98
}
            

Schema Validation

MongoDB allows you to enforce a schema within a collection through validation rules:


db.createCollection("products", {
  validator: {
    $jsonSchema: {
      bsonType: "object",
      required: ["name", "price", "category"],
      properties: {
        name: {
          bsonType: "string",
          description: "must be a string and is required"
        },
        price: {
          bsonType: "number",
          minimum: 0,
          description: "must be a non-negative number and is required"
        },
        category: {
          bsonType: "string",
          description: "must be a string and is required"
        },
        description: {
          bsonType: "string",
          description: "must be a string if the field exists"
        },
        tags: {
          bsonType: "array",
          items: {
            bsonType: "string"
          }
        }
      }
    }
  },
  validationLevel: "moderate",
  validationAction: "error"
})
            

MongoDB with Node.js

Now let's learn how to integrate MongoDB with a Node.js application using Mongoose, a popular ODM (Object Data Modeling) library that provides a straightforward, schema-based solution.

Setting Up a Node.js Project with MongoDB


# Create project directory
mkdir mongodb-node-demo
cd mongodb-node-demo

# Initialize a new Node.js project
npm init -y

# Install required packages
npm install express mongoose dotenv
npm install --save-dev nodemon
            

Creating a Basic Express App with MongoDB Connection

First, create a .env file to store your MongoDB connection string:


# .env file
MONGODB_URI=mongodb://localhost:27017/mydatabase
PORT=3000
            

Now, let's create the main server file:


// server.js
const express = require('express');
const mongoose = require('mongoose');
require('dotenv').config();

// Initialize Express app
const app = express();
app.use(express.json());

// Connect to MongoDB
mongoose.connect(process.env.MONGODB_URI)
  .then(() => console.log('Connected to MongoDB'))
  .catch(err => {
    console.error('Failed to connect to MongoDB', err);
    process.exit(1);
  });

// Basic route
app.get('/', (req, res) => {
  res.send('MongoDB Node.js API is running');
});

// Start the server
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});
            

Defining Mongoose Schemas and Models

Mongoose allows you to define schemas for your MongoDB collections, providing structure and validation.


// models/Product.js
const mongoose = require('mongoose');

// Define a schema
const productSchema = new mongoose.Schema({
  name: {
    type: String,
    required: true,
    trim: true
  },
  price: {
    type: Number,
    required: true,
    min: 0
  },
  description: {
    type: String,
    trim: true
  },
  category: {
    type: String,
    required: true,
    enum: ['Electronics', 'Clothing', 'Books', 'Home', 'Sports']
  },
  inStock: {
    type: Boolean,
    default: true
  },
  specs: {
    type: Map,
    of: String
  },
  tags: [String],
  createdAt: {
    type: Date,
    default: Date.now
  },
  updatedAt: Date
});

// Add a pre-save hook
productSchema.pre('save', function(next) {
  this.updatedAt = new Date();
  next();
});

// Create a model
const Product = mongoose.model('Product', productSchema);

module.exports = Product;
            

Building RESTful API Routes


// routes/productRoutes.js
const express = require('express');
const router = express.Router();
const Product = require('../models/Product');

// GET all products
router.get('/', async (req, res) => {
  try {
    const products = await Product.find();
    res.json(products);
  } catch (err) {
    console.error('Error fetching products:', err);
    res.status(500).json({ message: 'Server error' });
  }
});

// GET a single product
router.get('/:id', async (req, res) => {
  try {
    const product = await Product.findById(req.params.id);
    
    if (!product) {
      return res.status(404).json({ message: 'Product not found' });
    }
    
    res.json(product);
  } catch (err) {
    console.error('Error fetching product:', err);
    
    if (err.kind === 'ObjectId') {
      return res.status(400).json({ message: 'Invalid ID format' });
    }
    
    res.status(500).json({ message: 'Server error' });
  }
});

// POST a new product
router.post('/', async (req, res) => {
  try {
    const newProduct = new Product(req.body);
    const savedProduct = await newProduct.save();
    res.status(201).json(savedProduct);
  } catch (err) {
    console.error('Error creating product:', err);
    
    if (err.name === 'ValidationError') {
      return res.status(400).json({ 
        message: 'Validation Error', 
        errors: Object.values(err.errors).map(e => e.message) 
      });
    }
    
    res.status(500).json({ message: 'Server error' });
  }
});

// PUT (update) a product
router.put('/:id', async (req, res) => {
  try {
    const updatedProduct = await Product.findByIdAndUpdate(
      req.params.id,
      req.body,
      { new: true, runValidators: true }
    );
    
    if (!updatedProduct) {
      return res.status(404).json({ message: 'Product not found' });
    }
    
    res.json(updatedProduct);
  } catch (err) {
    console.error('Error updating product:', err);
    
    if (err.kind === 'ObjectId') {
      return res.status(400).json({ message: 'Invalid ID format' });
    }
    
    if (err.name === 'ValidationError') {
      return res.status(400).json({ 
        message: 'Validation Error', 
        errors: Object.values(err.errors).map(e => e.message) 
      });
    }
    
    res.status(500).json({ message: 'Server error' });
  }
});

// DELETE a product
router.delete('/:id', async (req, res) => {
  try {
    const deletedProduct = await Product.findByIdAndDelete(req.params.id);
    
    if (!deletedProduct) {
      return res.status(404).json({ message: 'Product not found' });
    }
    
    res.json({ message: 'Product deleted successfully' });
  } catch (err) {
    console.error('Error deleting product:', err);
    
    if (err.kind === 'ObjectId') {
      return res.status(400).json({ message: 'Invalid ID format' });
    }
    
    res.status(500).json({ message: 'Server error' });
  }
});

module.exports = router;
            

Update your server.js to use the routes:


// server.js (updated)
// ... existing code ...

// Import routes
const productRoutes = require('./routes/productRoutes');

// Use routes
app.use('/api/products', productRoutes);

// ... rest of the code ...
            

MongoDB Relationships with Mongoose

Let's implement a relationship between Users and Orders in our e-commerce example:


// models/User.js
const mongoose = require('mongoose');

const userSchema = new mongoose.Schema({
  name: {
    type: String,
    required: true,
    trim: true
  },
  email: {
    type: String,
    required: true,
    unique: true,
    trim: true,
    lowercase: true
  },
  // Virtual reference to orders
});

// Add a virtual field for orders
userSchema.virtual('orders', {
  ref: 'Order',
  localField: '_id',
  foreignField: 'user'
});

// Set virtuals in JSON
userSchema.set('toJSON', { virtuals: true });
userSchema.set('toObject', { virtuals: true });

const User = mongoose.model('User', userSchema);
module.exports = User;

// models/Order.js
const mongoose = require('mongoose');

const orderItemSchema = new mongoose.Schema({
  product: {
    type: mongoose.Schema.Types.ObjectId,
    ref: 'Product',
    required: true
  },
  name: String,
  quantity: {
    type: Number,
    required: true,
    min: 1
  },
  price: {
    type: Number,
    required: true
  }
});

const orderSchema = new mongoose.Schema({
  user: {
    type: mongoose.Schema.Types.ObjectId,
    ref: 'User',
    required: true
  },
  items: [orderItemSchema],
  total: {
    type: Number,
    required: true
  },
  status: {
    type: String,
    enum: ['pending', 'processing', 'shipped', 'delivered', 'cancelled'],
    default: 'pending'
  },
  createdAt: {
    type: Date,
    default: Date.now
  }
});

const Order = mongoose.model('Order', orderSchema);
module.exports = Order;
            

Using Population to Fetch Related Data


// Get orders for a user with populated product details
router.get('/users/:userId/orders', async (req, res) => {
  try {
    const orders = await Order.find({ user: req.params.userId })
      .populate({
        path: 'items.product',
        select: 'name category'
      })
      .sort({ createdAt: -1 });
    
    res.json(orders);
  } catch (err) {
    console.error('Error fetching orders:', err);
    res.status(500).json({ message: 'Server error' });
  }
});

// Get a user with their orders
router.get('/users/:id/with-orders', async (req, res) => {
  try {
    const user = await User.findById(req.params.id)
      .populate({
        path: 'orders',
        options: { sort: { createdAt: -1 } }
      });
    
    if (!user) {
      return res.status(404).json({ message: 'User not found' });
    }
    
    res.json(user);
  } catch (err) {
    console.error('Error fetching user with orders:', err);
    res.status(500).json({ message: 'Server error' });
  }
});
            

Advanced Mongoose and MongoDB Features

Query Building and Middleware


// Query Building
const getProductsByCategory = async (category, options = {}) => {
  const query = Product.find({ category });
  
  // Apply filters
  if (options.minPrice !== undefined) {
    query.where('price').gte(options.minPrice);
  }
  
  if (options.maxPrice !== undefined) {
    query.where('price').lte(options.maxPrice);
  }
  
  if (options.inStock !== undefined) {
    query.where('inStock').equals(options.inStock);
  }
  
  // Apply sorting
  if (options.sortBy) {
    const sortOrder = options.sortDir === 'desc' ? -1 : 1;
    query.sort({ [options.sortBy]: sortOrder });
  } else {
    query.sort({ createdAt: -1 });
  }
  
  // Apply pagination
  const page = options.page || 1;
  const limit = options.limit || 10;
  const skip = (page - 1) * limit;
  
  query.skip(skip).limit(limit);
  
  // Execute query
  return query.exec();
};

// Query Middleware (Hooks)
// Add this to the productSchema
productSchema.pre('find', function() {
  console.log('Executing find operation on products');
  this.startTime = Date.now();
});

productSchema.post('find', function(result) {
  console.log(`Find operation completed in ${Date.now() - this.startTime}ms`);
});

// Static method on schema
productSchema.statics.findByCategory = function(category) {
  return this.find({ category });
};

// Instance method on schema
productSchema.methods.applyDiscount = function(percentage) {
  this.price = this.price * (1 - percentage / 100);
  return this.save();
};

// Using the methods
const electronicsProducts = await Product.findByCategory('Electronics');

const product = await Product.findById(productId);
await product.applyDiscount(10); // Apply 10% discount
            

Aggregation Framework

MongoDB's aggregation framework is a powerful tool for processing and analyzing data within the database.


// Calculate sales by category
router.get('/sales-by-category', async (req, res) => {
  try {
    const salesByCategory = await Order.aggregate([
      // Only include completed orders
      { $match: { status: { $in: ['shipped', 'delivered'] } } },
      
      // Unwind the items array
      { $unwind: '$items' },
      
      // Lookup to get product details
      {
        $lookup: {
          from: 'products',
          localField: 'items.product',
          foreignField: '_id',
          as: 'productDetails'
        }
      },
      
      // Unwind the product details
      { $unwind: '$productDetails' },
      
      // Group by category and sum sales
      {
        $group: {
          _id: '$productDetails.category',
          totalSales: { $sum: { $multiply: ['$items.quantity', '$items.price'] } },
          count: { $sum: 1 }
        }
      },
      
      // Sort by total sales (descending)
      { $sort: { totalSales: -1 } },
      
      // Project to rename fields
      {
        $project: {
          category: '$_id',
          totalSales: 1,
          count: 1,
          _id: 0
        }
      }
    ]);
    
    res.json(salesByCategory);
  } catch (err) {
    console.error('Error aggregating sales data:', err);
    res.status(500).json({ message: 'Server error' });
  }
});
            

Transactions

MongoDB supports multi-document transactions, allowing for atomic operations across multiple documents and collections.


// Function to process an order
const processOrder = async (userId, items) => {
  // Start a session
  const session = await mongoose.startSession();
  session.startTransaction();
  
  try {
    // Calculate total price and create order items
    let total = 0;
    const orderItems = [];
    
    // Process each item
    for (const item of items) {
      // Find the product and ensure it's in stock
      const product = await Product.findById(item.productId).session(session);
      
      if (!product) {
        throw new Error(`Product ${item.productId} not found`);
      }
      
      if (!product.inStock) {
        throw new Error(`Product ${product.name} is out of stock`);
      }
      
      // Create order item and add to total
      const orderItem = {
        product: product._id,
        name: product.name,
        quantity: item.quantity,
        price: product.price
      };
      
      orderItems.push(orderItem);
      total += product.price * item.quantity;
      
      // Update product stock
      await Product.findByIdAndUpdate(
        product._id,
        { inStock: product.quantity > 1 },
        { session }
      );
    }
    
    // Create the order
    const order = new Order({
      user: userId,
      items: orderItems,
      total
    });
    
    await order.save({ session });
    
    // Commit the transaction
    await session.commitTransaction();
    session.endSession();
    
    return order;
  } catch (error) {
    // If an error occurred, abort the transaction
    await session.abortTransaction();
    session.endSession();
    throw error;
  }
};
            

Indexing for Performance


// Define indexes in Mongoose schema
const productSchema = new mongoose.Schema({
  // ... fields ...
});

// Simple index
productSchema.index({ name: 1 });

// Compound index
productSchema.index({ category: 1, price: -1 });

// Text index for search
productSchema.index(
  { name: 'text', description: 'text' },
  { weights: { name: 10, description: 5 } }
);

// Using text search
router.get('/search', async (req, res) => {
  try {
    const { q } = req.query;
    
    if (!q) {
      return res.status(400).json({ message: 'Search query is required' });
    }
    
    const products = await Product.find(
      { $text: { $search: q } },
      { score: { $meta: 'textScore' } }
    ).sort({ score: { $meta: 'textScore' } });
    
    res.json(products);
  } catch (err) {
    console.error('Search error:', err);
    res.status(500).json({ message: 'Server error' });
  }
});
            

Practical Activities

Activity 1: Setting Up a MongoDB Database

  1. Install MongoDB locally or set up a MongoDB Atlas free tier cluster
  2. Connect to your MongoDB instance using MongoDB Compass or the mongo shell
  3. Create a database called "e-commerce"
  4. Create collections for users, products, and orders
  5. Insert sample documents into each collection
  6. Practice CRUD operations using the MongoDB shell

Activity 2: Building a RESTful API with MongoDB and Express

Create a complete REST API for a blog application with:

  • User authentication (signup, login)
  • Blog posts with CRUD operations
  • Comments as embedded documents within posts
  • Categories for posts
  • Ability to like posts
  • Search functionality for posts

Implement proper error handling, validation, and pagination.

Activity 3: Data Modeling Challenge

Design a MongoDB data model for an online learning platform with:

  • Users (students and instructors)
  • Courses (with multiple lessons and quizzes)
  • Enrollments
  • Progress tracking for students
  • Reviews and ratings for courses

Consider when to use embedded documents vs. references for optimal performance and flexibility.

MongoDB Best Practices

Schema Design Best Practices

Performance Best Practices

Security Best Practices

Mongoose Best Practices

MongoDB vs. PostgreSQL: When to Use Each

Factor MongoDB PostgreSQL
Data Structure Schema-flexible, document-oriented Schema-rigid, table-oriented
Complex Relationships Less natural, requires denormalization or references Very natural with joins and foreign keys
Querying Flexibility Rich document-oriented queries Powerful SQL with advanced functions
Transactions Supported since v4.0, with limitations Fully ACID compliant with robust transaction support
Schema Evolution Highly flexible, easy schema changes Less flexible, requires migrations
Performance for Read-Heavy Workloads Excellent for denormalized data Good with proper indexing
Scalability Designed for horizontal scaling Traditionally vertical, with horizontal options
Data Consistency Eventually consistent by default Strongly consistent
Write Performance Generally faster Can be slower due to ACID guarantees

Choose MongoDB When:

Choose PostgreSQL When:

Hybrid Approaches

Many modern applications use both types of databases in a polyglot persistence architecture:

Further Reading and Resources

Coming Up: MongoDB CRUD Operations

In our next session, we'll dive deeper into MongoDB CRUD operations, exploring advanced query techniques, aggregation framework, and more complex data manipulations.