Understanding MongoDB CRUD Operations
In our previous lecture, we introduced MongoDB and its document-oriented approach to data storage. Today, we'll dive deep into the core database operations: Create, Read, Update, and Delete (CRUD). These fundamental operations form the backbone of any application's interaction with MongoDB.
The beauty of MongoDB's CRUD operations lies in their intuitive, JavaScript-like syntax, which allows developers to work with data in a way that feels natural and consistent with their application code. Unlike SQL's declarative approach, MongoDB uses method-based operations that map closely to programming concepts.
Why Master CRUD Operations?
Understanding MongoDB CRUD operations thoroughly enables you to:
- Build Efficient Data Access Patterns: Design your application to interact with the database optimally
- Optimize Performance: Craft queries that utilize MongoDB's indexing and query engine effectively
- Handle Complex Data Requirements: Address sophisticated business needs with MongoDB's flexible query capabilities
- Maintain Data Integrity: Ensure your application's data remains consistent and valid
- Scale Your Application: Implement database operations that perform well as your dataset grows
MongoDB Shell vs. MongoDB Node.js Driver
Throughout this lecture, we'll present examples in both the MongoDB Shell (mongosh) and the MongoDB Node.js driver. The syntax is similar but has some differences:
MongoDB Shell
// Finding documents
db.users.find({ age: { $gt: 25 } })
// Inserting a document
db.users.insertOne({
name: "John",
email: "john@example.com"
})
Node.js Driver
// Finding documents
await db.collection('users').find({
age: { $gt: 25 }
}).toArray()
// Inserting a document
await db.collection('users').insertOne({
name: "John",
email: "john@example.com"
})
Creating Data in MongoDB
MongoDB provides two primary methods for creating (inserting) documents into a collection: insertOne() for single documents and insertMany() for multiple documents.
Inserting Single Documents
Use insertOne() to add a single document to a collection:
// MongoDB Shell
db.products.insertOne({
name: "Smartphone X1",
manufacturer: "TechCorp",
price: 699.99,
specs: {
screen: "6.5 inch",
processor: "OctaCore 2.4GHz",
storage: "128GB",
camera: "48MP"
},
colors: ["Black", "Silver", "Blue"],
available: true,
releaseDate: new Date("2023-04-15")
})
// Result
{
acknowledged: true,
insertedId: ObjectId("6176f67c8baf56a52bd8f3e2")
}
// Node.js Driver
const result = await db.collection('products').insertOne({
name: "Smartphone X1",
manufacturer: "TechCorp",
price: 699.99,
specs: {
screen: "6.5 inch",
processor: "OctaCore 2.4GHz",
storage: "128GB",
camera: "48MP"
},
colors: ["Black", "Silver", "Blue"],
available: true,
releaseDate: new Date("2023-04-15")
});
console.log(`New product inserted with id: ${result.insertedId}`);
Key points about insertOne():
- MongoDB automatically generates a unique
_idfield as the primary key if not provided - The operation returns the ID of the newly inserted document
- Documents don't need to conform to a predefined schema (unless schema validation is configured)
- The operation is atomic – it either succeeds completely or fails completely
Inserting Multiple Documents
Use insertMany() to add multiple documents in a single operation:
// MongoDB Shell
db.products.insertMany([
{
name: "Laptop Pro",
manufacturer: "TechCorp",
price: 1299.99,
specs: {
screen: "15.6 inch",
processor: "QuadCore 3.2GHz",
storage: "512GB SSD",
memory: "16GB"
},
colors: ["Silver", "Space Gray"],
available: true,
releaseDate: new Date("2023-02-20")
},
{
name: "Wireless Earbuds",
manufacturer: "AudioTech",
price: 149.99,
specs: {
batteryLife: "8 hours",
bluetoothVersion: "5.2",
noiseReduction: true
},
colors: ["White", "Black", "Red"],
available: true,
releaseDate: new Date("2023-03-15")
},
{
name: "Smart Watch",
manufacturer: "TechCorp",
price: 299.99,
specs: {
screen: "1.5 inch",
batteryLife: "48 hours",
sensors: ["heart rate", "oxygen", "accelerometer"]
},
colors: ["Black", "Silver"],
available: false,
releaseDate: new Date("2023-06-10")
}
])
// Result
{
acknowledged: true,
insertedCount: 3,
insertedIds: {
'0': ObjectId("6176f7d08baf56a52bd8f3e3"),
'1': ObjectId("6176f7d08baf56a52bd8f3e4"),
'2': ObjectId("6176f7d08baf56a52bd8f3e5")
}
}
Insert Options
Both insertOne() and insertMany() accept an options object as the second parameter:
// MongoDB Shell
db.products.insertMany(
[/* documents */],
{
ordered: false,
writeConcern: { w: "majority", wtimeout: 5000 }
}
)
// Node.js Driver
await db.collection('products').insertMany(
[/* documents */],
{
ordered: false,
writeConcern: { w: "majority", wtimeout: 5000 }
}
);
| Option | Description | Default |
|---|---|---|
ordered |
If true, MongoDB will stop processing documents after the first error. If false, MongoDB will continue processing remaining documents even if one fails. |
true |
writeConcern |
Controls the acknowledgment of write operations.w: 1 - Wait for acknowledgment from primaryw: "majority" - Wait for acknowledgment from a majority of replica set memberswtimeout - Time limit for the write concern
|
w: 1 (no wtimeout) |
Handling Duplicate Keys
By default, attempting to insert a document with a duplicate _id will result in an error. You can use ordered: false to continue processing other documents in a batch insert:
// MongoDB Shell - With duplicate key
try {
db.products.insertMany([
{ _id: 1, name: "Product A" },
{ _id: 1, name: "Duplicate ID" }, // This will cause an error
{ _id: 2, name: "Product B" }
], { ordered: false });
} catch (e) {
print(e);
}
// Result: First and third documents will be inserted, second will fail
Using Transactions for Multi-Document Inserts
For operations that must be atomic across multiple documents or collections, use transactions:
// Node.js Driver with transactions
const session = client.startSession();
try {
session.startTransaction();
// Insert product
await db.collection('products').insertOne({
_id: new ObjectId(),
name: "New Product",
price: 199.99
}, { session });
// Insert initial inventory
await db.collection('inventory').insertOne({
productId: productId,
quantity: 100,
location: "Warehouse A"
}, { session });
// Commit the transaction
await session.commitTransaction();
console.log("Transaction committed successfully");
} catch (error) {
// If an error occurred, abort the transaction
await session.abortTransaction();
console.error("Transaction aborted:", error);
} finally {
// End the session
session.endSession();
}
Best Practices for Insert Operations
- Batch Inserts: Use
insertMany()for better performance when inserting multiple documents - Predefined ObjectIDs: Consider generating and specifying your own
_idvalues for better control - Document Validation: Implement schema validation to ensure document structure
- Error Handling: Always implement proper error handling for insert operations
- Bulk Writes: For complex mixed operations, consider using the
bulkWrite()method
Reading Data from MongoDB
MongoDB's read operations allow you to query and retrieve documents from collections. The main methods are find() for multiple documents and findOne() for a single document.
Basic Find Operations
// MongoDB Shell - Find all documents in a collection
db.products.find()
// Find with a simple equality condition
db.products.find({ manufacturer: "TechCorp" })
// Find a single document
db.products.findOne({ name: "Smartphone X1" })
// Node.js Driver - Find all documents
const allProducts = await db.collection('products').find().toArray();
// Find with a condition
const techProducts = await db.collection('products')
.find({ manufacturer: "TechCorp" })
.toArray();
// Find a single document
const smartphone = await db.collection('products')
.findOne({ name: "Smartphone X1" });
Query Operators
MongoDB provides rich query operators for creating complex conditions:
Comparison Operators
// Greater than
db.products.find({ price: { $gt: 500 } })
// Less than or equal
db.products.find({ price: { $lte: 200 } })
// Equal to one of the values
db.products.find({ manufacturer: { $in: ["TechCorp", "AudioTech"] } })
// Not equal
db.products.find({ available: { $ne: false } })
// Between range (greater than or equal AND less than)
db.products.find({ price: { $gte: 100, $lt: 300 } })
Logical Operators
// AND - Multiple conditions (implicit)
db.products.find({ manufacturer: "TechCorp", available: true })
// AND - Explicit operator
db.products.find({
$and: [
{ price: { $gt: 500 } },
{ available: true }
]
})
// OR
db.products.find({
$or: [
{ manufacturer: "TechCorp" },
{ price: { $lt: 200 } }
]
})
// NOT
db.products.find({
manufacturer: { $not: { $eq: "TechCorp" } }
})
// NOR - Neither condition is true
db.products.find({
$nor: [
{ price: { $gt: 1000 } },
{ available: false }
]
})
Element Operators
// Field exists
db.products.find({ specs: { $exists: true } })
// Field is of a specific type
db.products.find({ price: { $type: "number" } })
Array Operators
// Document has an array field containing the element
db.products.find({ colors: "Black" })
// Array contains all the specified elements
db.products.find({ colors: { $all: ["Black", "Silver"] } })
// Array has exactly the specified size
db.products.find({ colors: { $size: 3 } })
// Array element matches a condition
db.products.find({ "specs.sensors": "heart rate" })
// Use $elemMatch for complex array element conditions
db.inventory.find({
items: {
$elemMatch: {
name: "Widget",
quantity: { $gt: 10 }
}
}
})
Evaluation Operators
// Regular expression match
db.products.find({ name: { $regex: /phone/i } })
// JavaScript expression evaluation (use sparingly - performance impact)
db.products.find({
$expr: { $gt: [{ $multiply: ["$price", 0.8] }, 500] }
})
Querying Embedded Documents
MongoDB allows you to query fields inside nested documents using dot notation:
// Query on a field in an embedded document
db.products.find({ "specs.storage": "128GB" })
// Query on multiple fields in an embedded document
db.products.find({
"specs.screen": "6.5 inch",
"specs.processor": "OctaCore 2.4GHz"
})
Projection - Selecting Fields
Use projection to specify which fields to include or exclude from the results:
// Include only certain fields (1 = include)
db.products.find(
{ manufacturer: "TechCorp" },
{ name: 1, price: 1, available: 1, _id: 0 }
)
// Exclude certain fields (0 = exclude)
db.products.find(
{ manufacturer: "TechCorp" },
{ specs: 0, colors: 0 }
)
// Project fields in embedded documents
db.products.find(
{ manufacturer: "TechCorp" },
{ name: 1, "specs.storage": 1, "specs.processor": 1 }
)
Important: You cannot mix inclusion and exclusion in the same projection, with the exception of the _id field. Either specify fields to include or fields to exclude.
Sorting, Limiting, and Skipping
Control the order and number of documents returned:
// Sort by price (1 = ascending, -1 = descending)
db.products.find().sort({ price: -1 })
// Sort by multiple fields
db.products.find().sort({ manufacturer: 1, price: -1 })
// Limit the number of results
db.products.find().limit(5)
// Skip the first N results (useful for pagination)
db.products.find().skip(10).limit(5)
// Combining all modifiers
db.products.find({ available: true })
.sort({ price: -1 })
.skip(20)
.limit(10)
Counting Documents
// Count all documents in a collection
db.products.countDocuments()
// Count documents matching a query
db.products.countDocuments({ manufacturer: "TechCorp" })
// Count with more complex conditions
db.products.countDocuments({
price: { $gt: 300 },
available: true
})
Using Cursors in Node.js
The Node.js driver returns a cursor object from find(), allowing for efficient iteration:
// Process a large result set one document at a time
const cursor = db.collection('products').find({ available: true });
await cursor.forEach(product => {
console.log(`${product.name}: $${product.price}`);
// Process each document individually
});
// Alternatively, convert to array for smaller result sets
const products = await cursor.toArray();
// Using async iterator (modern approach)
const cursor = db.collection('products').find({ available: true });
for await (const product of cursor) {
console.log(`${product.name}: $${product.price}`);
}
Advanced Find Options
// Node.js Driver - Advanced options
const cursor = db.collection('products').find(
{ price: { $gt: 500 } },
{
// Projection
projection: { name: 1, price: 1, _id: 0 },
// Sort
sort: { price: -1 },
// Limit
limit: 10,
// Skip
skip: 20,
// Hint (use a specific index)
hint: { price: 1 },
// Max time MS (timeout for query execution)
maxTimeMS: 5000,
// Comment (useful for logging and debugging)
comment: "Query for expensive products"
}
);
Best Practices for Find Operations
- Use Projections: Select only the fields you need to reduce network traffic and memory usage
- Create Indexes: Ensure proper indexes exist for frequently queried fields
- Cursor Management: Use cursor methods for large result sets to avoid loading everything into memory
- Limit Results: Always limit the number of returned documents in production applications
- Prefer Equality Filters: Queries with equality conditions are typically faster than range queries
- Avoid Regex with Leading Wildcards: Regular expressions with leading wildcards (e.g.,
/^text/) cannot use indexes effectively
Updating Data in MongoDB
MongoDB provides several methods for updating documents: updateOne(), updateMany(), replaceOne(), and various specialized update methods.
Basic Update Operations
// MongoDB Shell - Update a single document
db.products.updateOne(
{ name: "Smartphone X1" }, // filter
{ $set: { price: 649.99, available: false } } // update
)
// Update multiple documents
db.products.updateMany(
{ manufacturer: "TechCorp" }, // filter
{ $set: { manufacturer: "TechCorp Inc." } } // update
)
// Replace an entire document (except _id)
db.products.replaceOne(
{ name: "Smartphone X1" }, // filter
{ // complete replacement document
name: "Smartphone X1 Pro",
manufacturer: "TechCorp Inc.",
price: 799.99,
specs: {
screen: "6.7 inch",
processor: "OctaCore 3.0GHz",
storage: "256GB",
camera: "64MP"
},
colors: ["Black", "Silver", "Gold"],
available: true,
releaseDate: new Date("2023-09-15")
}
)
Update Operators
MongoDB provides various update operators to perform different kinds of modifications:
Field Update Operators
// Set field values
db.products.updateOne(
{ name: "Wireless Earbuds" },
{ $set: { price: 129.99, "specs.batteryLife": "10 hours" } }
)
// Increment numeric fields
db.products.updateOne(
{ name: "Smartphone X1" },
{ $inc: { price: -50 } } // Decrease price by 50
)
// Multiply numeric fields
db.products.updateOne(
{ name: "Laptop Pro" },
{ $mul: { price: 0.9 } } // 10% discount
)
// Remove fields
db.products.updateOne(
{ name: "Smart Watch" },
{ $unset: { "specs.waterResistant": "" } }
)
// Rename fields
db.products.updateMany(
{},
{ $rename: { "releaseDate": "launchDate" } }
)
// Set values only if the field doesn't exist
db.products.updateMany(
{},
{ $setOnInsert: { lastModified: new Date() } }
)
// Current date
db.products.updateOne(
{ name: "Smartphone X1" },
{ $currentDate: { lastModified: true } }
)
// Min/Max (only update if the new value is less/greater)
db.products.updateOne(
{ name: "Laptop Pro" },
{
$min: { price: 1199.99 }, // Only set if new value is less than current
$max: { "specs.storage": "1TB" } // Only set if new value is greater than current
}
)
Array Update Operators
// Add elements to array (if not already present)
db.products.updateOne(
{ name: "Smart Watch" },
{ $addToSet: { colors: "Gold" } }
)
// Add multiple elements to array (if not already present)
db.products.updateOne(
{ name: "Smart Watch" },
{ $addToSet: { colors: { $each: ["Red", "Green"] } } }
)
// Push elements to array (allows duplicates)
db.products.updateOne(
{ name: "Smartphone X1" },
{ $push: { colors: "Rose Gold" } }
)
// Push with modifiers
db.products.updateOne(
{ name: "Smartphone X1" },
{
$push: {
priceHistory: {
$each: [{ date: new Date(), price: 649.99 }],
$sort: { date: -1 },
$slice: 5 // Keep only the most recent 5 entries
}
}
}
)
// Remove elements from array
db.products.updateOne(
{ name: "Smart Watch" },
{ $pull: { colors: "Black" } }
)
// Remove multiple elements matching a condition
db.products.updateOne(
{ name: "Smart Watch" },
{ $pull: { colors: { $in: ["Black", "Silver"] } } }
)
// Remove by position (first element)
db.products.updateOne(
{ name: "Smart Watch" },
{ $pop: { colors: -1 } } // -1 first element, 1 last element
)
Update Specific Array Elements
// Update the first array element that matches
db.inventory.updateOne(
{ _id: 1 },
{ $set: { "items.$[elem].quantity": 50 } },
{ arrayFilters: [{ "elem.name": "Widget" }] }
)
// Update all array elements that match
db.inventory.updateOne(
{ _id: 1 },
{ $set: { "items.$[elem].price": 19.99 } },
{ arrayFilters: [{ "elem.quantity": { $lt: 20 } }] }
)
// Update by position
db.products.updateOne(
{ name: "Smartphone X1" },
{ $set: { "colors.0": "Midnight Black" } } // Update first element
)
Upsert - Insert if Not Exists
The upsert option creates a new document if no document matches the filter:
// MongoDB Shell - Upsert example
db.products.updateOne(
{ name: "Tablet Mini" }, // This document doesn't exist yet
{
$set: {
name: "Tablet Mini",
manufacturer: "TechCorp Inc.",
price: 349.99,
available: true,
releaseDate: new Date("2023-11-01")
}
},
{ upsert: true } // Create if not exists
)
Return Modified Document
In the Node.js driver, you can return the modified document as part of the update operation:
// Node.js Driver - Return modified document
const result = await db.collection('products').findOneAndUpdate(
{ name: "Smartphone X1" },
{ $set: { price: 599.99 } },
{
returnDocument: 'after', // Return the document after the update
projection: { name: 1, price: 1, _id: 0 } // Only return specific fields
}
);
console.log("Updated product:", result.value);
Updates with Aggregation Pipeline
MongoDB 4.2+ supports using aggregation pipelines in update operations:
// MongoDB Shell - Update with aggregation pipeline
db.products.updateMany(
{ manufacturer: "TechCorp Inc." },
[
// Use the current price to calculate a new field
{ $set: { discountedPrice: { $multiply: ["$price", 0.9] } } },
// Use the newly calculated field in another calculation
{ $set: { savings: { $subtract: ["$price", "$discountedPrice"] } } },
// Conditionally set the "onSale" field
{ $set: { onSale: { $gt: ["$savings", 100] } } }
]
)
Best Practices for Update Operations
- Use Specific Filters: Target updates as precisely as possible
- Update Only Necessary Fields: Minimize the amount of data modified
- Use Atomic Operators: Prefer using atomic update operators over replaceOne when possible
- Consider Performance: Large updates can impact database performance
- Validate Before Update: Implement pre-update validation in your application
- Track Modifications: Consider adding lastModified timestamps to documents
Deleting Data from MongoDB
MongoDB provides deleteOne() and deleteMany() methods for removing documents from collections.
Basic Delete Operations
// MongoDB Shell - Delete a single document
db.products.deleteOne({ name: "Smartphone X1" })
// Delete multiple documents
db.products.deleteMany({ manufacturer: "TechCorp Inc." })
// Delete all documents in a collection
db.products.deleteMany({})
// Delete documents matching complex criteria
db.products.deleteMany({
price: { $lt: 200 },
available: false
})
// Node.js Driver - Delete operations
const result = await db.collection('products').deleteOne({
name: "Smartphone X1"
});
console.log(`${result.deletedCount} document deleted`);
const manyResult = await db.collection('products').deleteMany({
manufacturer: "TechCorp Inc."
});
console.log(`${manyResult.deletedCount} documents deleted`);
Finding and Deleting
To retrieve a document before deleting it, use findOneAndDelete():
// Node.js Driver - Find and delete
const result = await db.collection('products').findOneAndDelete(
{ name: "Smartphone X1" },
{ sort: { price: 1 }, projection: { name: 1, price: 1, _id: 0 } }
);
if (result.value) {
console.log(`Deleted product: ${result.value.name}, price: ${result.value.price}`);
} else {
console.log("No matching product found");
}
Soft Deletes
Instead of actually removing documents, consider implementing soft deletes:
// MongoDB Shell - Soft delete (mark as deleted)
db.products.updateOne(
{ name: "Smartphone X1" },
{
$set: {
deleted: true,
deletedAt: new Date()
}
}
)
// Query excluding soft-deleted documents
db.products.find({ deleted: { $ne: true } })
// Restore soft-deleted document
db.products.updateOne(
{ name: "Smartphone X1", deleted: true },
{
$set: { deleted: false },
$unset: { deletedAt: "" }
}
)
Best Practices for Delete Operations
- Double-Check Filters: Be absolutely sure your filter matches only the documents you intend to delete
- Use Limit for deleteOne: Set
limit: 1for deleting single documents to ensure consistency - Consider Soft Deletes: For important data, consider marking as deleted rather than actually removing
- Handle Referential Integrity: Update or delete related documents in other collections
- Use Transactions: For operations that need to delete across multiple collections atomically
Bulk Operations
For efficiency when performing multiple operations, MongoDB provides bulk operations that allow you to batch multiple write operations together.
Unordered Bulk Operations
// MongoDB Shell - Unordered bulk operations
const bulk = db.products.initializeUnorderedBulkOp();
// Add operations to the bulk
bulk.find({ price: { $gt: 1000 } }).update({ $set: { category: "premium" } });
bulk.find({ price: { $lt: 100 } }).update({ $set: { category: "budget" } });
bulk.find({ name: "Obsolete Product" }).remove();
bulk.insert({ name: "New Product", price: 499.99 });
// Execute all operations
const result = bulk.execute();
// Result contains information about all operations
printjson(result);
// Node.js Driver - Bulk operations
const bulkOperations = [
{
updateMany: {
filter: { price: { $gt: 1000 } },
update: { $set: { category: "premium" } }
}
},
{
updateMany: {
filter: { price: { $lt: 100 } },
update: { $set: { category: "budget" } }
}
},
{
deleteOne: {
filter: { name: "Obsolete Product" }
}
},
{
insertOne: {
document: { name: "New Product", price: 499.99 }
}
}
];
const result = await db.collection('products').bulkWrite(bulkOperations, {
ordered: false,
writeConcern: { w: "majority" }
});
console.log(`
Inserted: ${result.insertedCount}
Updated: ${result.modifiedCount}
Deleted: ${result.deletedCount}
`);
Ordered vs. Unordered Bulk Operations
| Ordered | Unordered |
|---|---|
| Operations are executed in sequence | Operations can be executed in parallel |
| Stops processing after first error | Continues processing after errors |
| Guarantees execution order | No guaranteed execution order |
| Generally slower, especially for many operations | Generally faster for many operations |
// Node.js Driver - Ordered bulk operations
const result = await db.collection('products').bulkWrite(
bulkOperations,
{ ordered: true } // Stop on first error
);
CRUD Operations with Mongoose
Mongoose is an Object Data Modeling (ODM) library for MongoDB and Node.js that provides a higher-level abstraction with schemas, validation, and more.
Defining a Schema
// Node.js with Mongoose - Define a schema
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const productSchema = new Schema({
name: {
type: String,
required: true,
trim: true,
unique: true
},
manufacturer: {
type: String,
required: true,
trim: true
},
price: {
type: Number,
required: true,
min: 0
},
specs: {
screen: String,
processor: String,
storage: String,
camera: String
},
colors: [String],
available: {
type: Boolean,
default: true
},
releaseDate: {
type: Date,
default: Date.now
},
category: {
type: String,
enum: ['budget', 'standard', 'premium'],
default: 'standard'
},
reviews: [{
user: String,
rating: {
type: Number,
min: 1,
max: 5
},
comment: String,
date: {
type: Date,
default: Date.now
}
}]
}, {
timestamps: true // Adds createdAt and updatedAt fields
});
// Add instance methods
productSchema.methods.applyDiscount = function(percentage) {
this.price = this.price * (1 - percentage / 100);
return this.save();
};
// Add static methods
productSchema.statics.findByManufacturer = function(manufacturer) {
return this.find({ manufacturer: new RegExp(manufacturer, 'i') });
};
// Add virtual property (not stored in the database)
productSchema.virtual('summary').get(function() {
return `${this.name} by ${this.manufacturer} - $${this.price}`;
});
// Create model
const Product = mongoose.model('Product', productSchema);
module.exports = Product;
Creating Data with Mongoose
// Create a single document
const smartphone = new Product({
name: "Smartphone X1",
manufacturer: "TechCorp",
price: 699.99,
specs: {
screen: "6.5 inch",
processor: "OctaCore 2.4GHz",
storage: "128GB",
camera: "48MP"
},
colors: ["Black", "Silver", "Blue"]
});
// Save to database
await smartphone.save();
// Alternative creation method
const laptop = await Product.create({
name: "Laptop Pro",
manufacturer: "TechCorp",
price: 1299.99,
specs: {
screen: "15.6 inch",
processor: "QuadCore 3.2GHz",
storage: "512GB SSD"
},
colors: ["Silver", "Space Gray"]
});
// Create multiple documents
await Product.insertMany([
{
name: "Wireless Earbuds",
manufacturer: "AudioTech",
price: 149.99,
specs: {
batteryLife: "8 hours",
bluetoothVersion: "5.2"
},
colors: ["White", "Black"]
},
{
name: "Smart Watch",
manufacturer: "TechCorp",
price: 299.99,
specs: {
screen: "1.5 inch",
batteryLife: "48 hours"
},
colors: ["Black", "Silver"]
}
]);
Reading Data with Mongoose
// Find all documents
const allProducts = await Product.find();
// Find with conditions
const techCorpProducts = await Product.find({ manufacturer: "TechCorp" });
// Find one document
const smartphone = await Product.findOne({ name: "Smartphone X1" });
// Find by ID
const product = await Product.findById("6176f67c8baf56a52bd8f3e2");
// Query with chaining
const expensiveProducts = await Product.find()
.where('price').gt(500)
.where('available').equals(true)
.select('name price')
.sort('-price')
.limit(5);
// Using static methods
const techProducts = await Product.findByManufacturer("Tech");
// Using a virtual property
const product = await Product.findOne({ name: "Smartphone X1" });
console.log(product.summary); // "Smartphone X1 by TechCorp - $699.99"
Updating Data with Mongoose
// Find and update directly with the model
const updatedProduct = await Product.findByIdAndUpdate(
"6176f67c8baf56a52bd8f3e2",
{ price: 649.99, available: false },
{ new: true } // Return the updated document
);
// Update multiple documents
const result = await Product.updateMany(
{ manufacturer: "TechCorp" },
{ $set: { manufacturer: "TechCorp Inc." } }
);
console.log(`${result.modifiedCount} products updated`);
// Update with validate/save pattern
const product = await Product.findById("6176f67c8baf56a52bd8f3e2");
if (product) {
product.price = 629.99;
product.colors.push("Rose Gold");
await product.save(); // Validation runs before saving
}
// Using instance methods
const product = await Product.findOne({ name: "Laptop Pro" });
await product.applyDiscount(10); // Apply 10% discount
Deleting Data with Mongoose
// Delete a single document
await Product.deleteOne({ name: "Obsolete Product" });
// Delete multiple documents
const result = await Product.deleteMany({ available: false });
console.log(`${result.deletedCount} products deleted`);
// Find and delete
const deletedProduct = await Product.findByIdAndDelete("6176f67c8baf56a52bd8f3e2");
if (deletedProduct) {
console.log(`Deleted ${deletedProduct.name}`);
}
// Delete with the document instance
const product = await Product.findOne({ name: "Discontinued Item" });
if (product) {
await product.deleteOne();
console.log("Product deleted");
}
Validation with Mongoose
// Define a schema with advanced validation
const productSchema = new Schema({
name: {
type: String,
required: [true, 'Product name is required'],
trim: true,
minlength: [3, 'Name must be at least 3 characters'],
maxlength: [100, 'Name cannot exceed 100 characters']
},
price: {
type: Number,
required: true,
min: [0, 'Price cannot be negative'],
validate: {
validator: function(v) {
return v % 0.01 === 0; // Ensure price has at most 2 decimal places
},
message: props => `${props.value} is not a valid price format`
}
},
sku: {
type: String,
required: true,
unique: true,
validate: {
validator: function(v) {
return /^[A-Z]{2}-\d{4}$/.test(v); // Format: XX-0000
},
message: props => `${props.value} is not a valid SKU format`
}
}
});
// Handling validation errors
try {
const product = new Product({
name: "AB", // Too short
price: -10, // Negative
sku: "invalid" // Wrong format
});
await product.save();
} catch (error) {
if (error.name === 'ValidationError') {
for (const field in error.errors) {
console.log(`${field}: ${error.errors[field].message}`);
}
} else {
console.error("Error:", error);
}
}
Best Practices for Mongoose
- Define Schemas with Validation: Take advantage of Mongoose's built-in validation
- Use Middleware: Implement pre/post hooks for common operations
- Lean Queries: Use
.lean()for read-only operations to improve performance - Populate with Caution: Be mindful of the performance impact of
populate() - Handle Errors Properly: Implement specific handling for different types of Mongoose errors
- Understand Promises: Mongoose methods return promises or support callbacks
Practical Activities
Activity 1: E-commerce Product Management
Create a complete CRUD API for managing products in an e-commerce platform:
- Set up a MongoDB database (local or Atlas)
- Create a Node.js/Express application with the necessary routes:
- GET /api/products - List all products with optional filtering
- GET /api/products/:id - Get a single product
- POST /api/products - Create a new product
- PUT /api/products/:id - Update a product
- DELETE /api/products/:id - Delete a product
- Implement query parameters for filtering, sorting, and pagination
- Add validation for product data
- Implement error handling for all operations
Activity 2: Bulk Operations and Data Migration
Create a script that migrates a set of data from a file into MongoDB:
- Download a sample CSV or JSON dataset (you can find free datasets on Kaggle)
- Create a Node.js script that:
- Parses the file data
- Transforms the data into the right format for MongoDB
- Uses bulk operations to efficiently insert the data
- Logs progress and handles errors
- Implement a validation step before insertion
- Add a feature to update existing records instead of creating duplicates
Activity 3: Advanced Querying and Aggregation
Build a reporting module for an e-commerce system:
- Create a collection of sample order data with the following structure:
{ orderId: "ORD123", customer: { id: "CUST456", name: "John Doe", email: "john@example.com" }, products: [ { id: "PROD789", name: "Product Name", price: 29.99, quantity: 2 }, // More products... ], total: 59.98, paymentMethod: "Credit Card", status: "Shipped", orderDate: ISODate("2023-04-15"), shippingAddress: { street: "123 Main St", city: "Anytown", state: "CA", zip: "12345" } } - Implement the following reports using MongoDB queries:
- Total sales by day/week/month
- Top selling products
- Sales by payment method
- Customer purchase frequency
- Create an API endpoint to access these reports
- Implement caching for report results
Further Reading and Resources
- MongoDB CRUD Operations Documentation
- MongoDB Node.js Driver Documentation
- Mongoose Documentation
- "MongoDB: The Definitive Guide" by Shannon Bradshaw, Eoin Brazil, and Kristina Chodorow
- "MongoDB in Action" by Kyle Banker, Peter Bakkum, Shaun Verch, Doug Garrett, and Tim Hawkins
- "Practical MongoDB Aggregations" (E-book by MongoDB)