🏠 Home / Hub

📄 MongoDB — Lesson 2: CRUD Operations

CRUD stands for Create, Read, Update, Delete — the four fundamental database operations. This lesson covers every method in depth with real examples using a blog posts collection.

1. Create — Inserting Documents

insertOne()

Inserts a single document. Returns an object with acknowledged and insertedId.

use blog

// Insert one document
const result = db.posts.insertOne({
  title: "Getting Started with MongoDB",
  slug: "getting-started-mongodb",
  content: "MongoDB is a document-oriented NoSQL database...",
  author: "alice",
  tags: ["mongodb", "nosql", "database"],
  published: true,
  views: 0,
  createdAt: new Date(),
  updatedAt: new Date()
})

// result:
// { acknowledged: true, insertedId: ObjectId("64a1b2c3d4e5f60718293a4b") }

// You can also specify your own _id
db.posts.insertOne({
  _id: "custom-id-001",
  title: "Custom ID Post"
})

insertMany()

Inserts an array of documents in a single operation. More efficient than multiple insertOne calls.

const result = db.posts.insertMany([
  {
    title: "MongoDB Aggregation",
    author: "bob",
    tags: ["mongodb", "aggregation"],
    published: true,
    views: 150,
    createdAt: new Date("2024-01-10")
  },
  {
    title: "Indexing Strategies",
    author: "carol",
    tags: ["mongodb", "performance"],
    published: false,
    views: 0,
    createdAt: new Date("2024-01-20")
  },
  {
    title: "Schema Design Patterns",
    author: "alice",
    tags: ["mongodb", "design"],
    published: true,
    views: 320,
    createdAt: new Date("2024-02-01")
  }
])

// result:
// { acknowledged: true, insertedIds: { '0': ObjectId(...), '1': ObjectId(...), '2': ObjectId(...) } }

// By default, insertMany stops on first error.
// Use ordered: false to continue inserting despite errors:
db.posts.insertMany(docs, { ordered: false })

2. Read — Finding Documents

find() and findOne()

// Find ALL documents (returns a cursor)
db.posts.find()

// find() with a filter object
db.posts.find({ author: "alice" })

// findOne() returns a single document (not a cursor)
db.posts.findOne({ slug: "getting-started-mongodb" })

// Find by _id
db.posts.findOne({ _id: ObjectId("64a1b2c3d4e5f60718293a4b") })

// Multiple conditions (implicit AND)
db.posts.find({ author: "alice", published: true })

// Comparison operators
db.posts.find({ views: { $gt: 100 } })           // views > 100
db.posts.find({ views: { $gte: 100 } })          // views >= 100
db.posts.find({ views: { $lt: 50 } })            // views < 50
db.posts.find({ views: { $gte: 50, $lte: 200 } }) // 50 <= views <= 200

Projection — Selecting Fields

// Include only specific fields (1 = include, _id included by default)
db.posts.find({}, { title: 1, author: 1 })

// Exclude _id explicitly
db.posts.find({}, { title: 1, author: 1, _id: 0 })

// Exclude specific fields (0 = exclude)
db.posts.find({}, { content: 0, __v: 0 })

// WARNING: You cannot mix include and exclude
// INVALID: db.posts.find({}, { title: 1, content: 0 })  // error!
// VALID: _id is the only exception to this rule
db.posts.find({}, { title: 1, _id: 0 })

Cursor Methods

// .sort() — 1 ascending, -1 descending
db.posts.find().sort({ views: -1 })          // most viewed first
db.posts.find().sort({ createdAt: -1 })       // newest first
db.posts.find().sort({ author: 1, views: -1 }) // multi-field sort

// .limit() — restrict number of results
db.posts.find().limit(10)

// .skip() — offset (for pagination)
db.posts.find().skip(20).limit(10)  // page 3 (10 per page)

// .count() / countDocuments()
db.posts.find({ published: true }).count()       // deprecated in newer drivers
db.posts.countDocuments({ published: true })      // preferred
db.posts.estimatedDocumentCount()                  // fastest (uses metadata)

// .toArray() — convert cursor to array
const posts = db.posts.find({ published: true }).toArray()

// Pagination helper function
function paginate(collection, filter, page, perPage) {
  return collection
    .find(filter)
    .sort({ createdAt: -1 })
    .skip((page - 1) * perPage)
    .limit(perPage)
    .toArray()
}
// paginate(db.posts, { published: true }, 2, 10) → page 2

3. Update — Modifying Documents

updateOne() — Update First Matching Document

// $set — set (or add) specific fields
db.posts.updateOne(
  { slug: "getting-started-mongodb" },   // filter
  { $set: { views: 500, updatedAt: new Date() } }  // update
)

// Result:
// { acknowledged: true, matchedCount: 1, modifiedCount: 1 }

// $inc — increment (or decrement) a numeric field
db.posts.updateOne(
  { slug: "getting-started-mongodb" },
  { $inc: { views: 1 } }   // add 1 to views
)
db.posts.updateOne(
  { slug: "getting-started-mongodb" },
  { $inc: { views: -5 } }  // subtract 5 from views
)

// $push — add an element to an array
db.posts.updateOne(
  { slug: "getting-started-mongodb" },
  { $push: { tags: "tutorial" } }
)

// $pull — remove an element from an array
db.posts.updateOne(
  { slug: "getting-started-mongodb" },
  { $pull: { tags: "nosql" } }
)

// $unset — remove a field entirely
db.posts.updateOne(
  { slug: "getting-started-mongodb" },
  { $unset: { legacyField: "" } }  // value doesn't matter
)

// $rename — rename a field
db.posts.updateOne(
  { slug: "getting-started-mongodb" },
  { $rename: { "oldFieldName": "newFieldName" } }
)

updateMany() — Update All Matching Documents

// Add a new field to ALL documents
db.posts.updateMany(
  {},  // empty filter = match all
  { $set: { featured: false } }
)

// Set all unpublished posts to archived
db.posts.updateMany(
  { published: false },
  { $set: { status: "archived", archivedAt: new Date() } }
)

// Increment views for all posts by a specific author
db.posts.updateMany(
  { author: "alice" },
  { $inc: { views: 10 } }
)

// $addToSet — like $push but prevents duplicates
db.posts.updateMany(
  { author: "alice" },
  { $addToSet: { tags: "featured" } }  // only adds if not already in array
)

replaceOne() vs updateOne()

// updateOne() — MODIFIES specific fields, keeps the rest
db.posts.updateOne(
  { _id: someId },
  { $set: { title: "New Title" } }
)
// Result: only title changes, all other fields remain

// replaceOne() — REPLACES the entire document (except _id)
db.posts.replaceOne(
  { _id: someId },
  {
    title: "Completely New Document",
    author: "dave",
    createdAt: new Date()
    // ALL previous fields are gone (views, tags, etc.)
  }
)
Rule of thumb: Use updateOne with operators ($set, etc.) when you want to change specific fields. Use replaceOne only when you want to completely overwrite a document.

4. Upsert — Update or Insert

The upsert: true option creates the document if it doesn't exist, or updates it if it does. This is a powerful pattern for "ensure this document exists" scenarios.

// Upsert: update if exists, insert if not
db.pageStats.updateOne(
  { slug: "getting-started-mongodb" },
  {
    $inc: { views: 1 },
    $setOnInsert: {           // only set on INSERT, not UPDATE
      createdAt: new Date(),
      slug: "getting-started-mongodb"
    }
  },
  { upsert: true }
)

// First call: creates { slug, views:1, createdAt }
// Subsequent calls: increments views only

// Upsert a user by email
db.users.updateOne(
  { email: "newuser@example.com" },
  {
    $set: { lastLoginAt: new Date() },
    $setOnInsert: {
      email: "newuser@example.com",
      role: "user",
      createdAt: new Date()
    }
  },
  { upsert: true }
)

5. findOneAndUpdate / findOneAndDelete

These methods atomically find and modify a document, returning either the original or updated version in a single operation — useful for implementing queues, counters, or atomic state changes.

// findOneAndUpdate — returns the ORIGINAL doc by default
const original = db.posts.findOneAndUpdate(
  { slug: "getting-started-mongodb" },
  { $inc: { views: 1 } }
)
// original.views is the OLD value

// returnDocument: "after" to get the UPDATED doc
const updated = db.posts.findOneAndUpdate(
  { slug: "getting-started-mongodb" },
  { $inc: { views: 1 } },
  { returnDocument: "after" }
)
// updated.views is the NEW value

// findOneAndUpdate with upsert
db.counters.findOneAndUpdate(
  { name: "postId" },
  { $inc: { seq: 1 } },
  { upsert: true, returnDocument: "after" }
)

// findOneAndDelete — returns the deleted document
const deleted = db.tasks.findOneAndDelete(
  { status: "pending" },
  { sort: { priority: -1 } }  // delete highest priority pending task
)
// deleted contains the task data for processing

6. Delete — Removing Documents

// deleteOne — remove first matching document
db.posts.deleteOne({ slug: "draft-post" })

// Result:
// { acknowledged: true, deletedCount: 1 }

// deleteMany — remove all matching documents
db.posts.deleteMany({ published: false })

// Delete all documents (DANGEROUS — collection still exists)
db.posts.deleteMany({})

// Drop the entire collection (removes collection + indexes)
db.posts.drop()

// Delete by _id
db.posts.deleteOne({ _id: ObjectId("64a1b2c3d4e5f60718293a4b") })

// Delete old records (older than 30 days)
const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000)
db.logs.deleteMany({ createdAt: { $lt: thirtyDaysAgo } })
Safety tip: Always run a find() with the same filter first to preview what will be deleted before running deleteMany().

7. Update Operators — Reference Table

OperatorCategoryDescriptionExample
$setFieldSet field value (adds if missing){$set: {name: "Bob"}}
$unsetFieldRemove a field from document{$unset: {oldField: ""}}
$renameFieldRename a field{$rename: {old: "new"}}
$incNumericIncrement by n (negative = decrement){$inc: {count: 1}}
$mulNumericMultiply field value by n{$mul: {price: 1.1}}
$minComparisonUpdate only if new value is less{$min: {score: 50}}
$maxComparisonUpdate only if new value is greater{$max: {score: 100}}
$currentDateDateSet field to current date/timestamp{$currentDate: {updatedAt: true}}
$setOnInsertFieldSet fields only during upsert insert{$setOnInsert: {createdAt: new Date()}}
$pushArrayAdd element to array{$push: {tags: "new"}}
$pullArrayRemove matching elements from array{$pull: {tags: "old"}}
$popArrayRemove first (-1) or last (1) element{$pop: {items: 1}}
$addToSetArrayAdd to array only if unique{$addToSet: {tags: "unique"}}
$pullAllArrayRemove all matching values{$pullAll: {scores: [1,2,3]}}
$eachArray modifierPush multiple values{$push: {tags: {$each: ["a","b"]}}}
$sortArray modifierSort array during $push{$push: {scores: {$each:[], $sort:-1}}}
$sliceArray modifierLimit array size during $push{$push: {log: {$each:[], $slice:-10}}}
$bitBitwiseBitwise AND / OR / XOR{$bit: {flags: {and: 5}}}

8. Full CRUD Exercise — Blog Posts Collection

// ---- SETUP ----
use blog_exercise

// ---- CREATE ----
db.posts.insertMany([
  {
    title: "First Post",
    author: "alice",
    body: "Hello World!",
    tags: ["general"],
    likes: 0,
    published: false,
    createdAt: new Date()
  },
  {
    title: "MongoDB Tips",
    author: "bob",
    body: "Use indexes on frequently queried fields.",
    tags: ["mongodb", "tips"],
    likes: 42,
    published: true,
    createdAt: new Date()
  }
])

// ---- READ ----
// All published posts, sorted newest first
db.posts.find({ published: true }).sort({ createdAt: -1 })

// ---- UPDATE ----
// Publish alice's first post and set updated time
db.posts.updateOne(
  { author: "alice", title: "First Post" },
  {
    $set: { published: true, updatedAt: new Date() },
    $addToSet: { tags: "featured" }
  }
)

// Simulate a user liking bob's post
db.posts.updateOne(
  { title: "MongoDB Tips" },
  { $inc: { likes: 1 } }
)

// ---- DELETE ----
// Remove all unpublished posts older than 7 days
const cutoff = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000)
db.posts.deleteMany({ published: false, createdAt: { $lt: cutoff } })

// ---- VERIFY ----
db.posts.find().sort({ createdAt: -1 })

📌 Study Checklist