3.8 — Database Basics: MongoDB: Quick Revision
Episode 3 supplement -- print-friendly.
How to use
Skim -> drill weak spots in 3.8.a through 3.8.g -> 3.8-Exercise-Questions.md.
SQL vs MongoDB Terminology
| SQL | MongoDB |
|---|
| Database | Database |
| Table | Collection |
| Row | Document |
| Column | Field |
| Primary Key | _id (ObjectId) |
| JOIN | populate() / $lookup |
| Schema (DDL) | Schema (Mongoose, app-level) |
| INDEX | Index |
Connection
const mongoose = require('mongoose');
require('dotenv').config();
mongoose.connect(process.env.MONGODB_URI)
.then(() => console.log('MongoDB connected'))
.catch(err => { console.error(err); process.exit(1); });
MONGODB_URI=mongodb://localhost:27017/myapp
Schema Definition
const { Schema } = mongoose;
const userSchema = new Schema({
name: { type: String, required: true, trim: true, minlength: 2 },
email: { type: String, required: true, unique: true, lowercase: true },
age: { type: Number, min: 0, max: 150 },
role: { type: String, enum: ['user', 'admin'], default: 'user' },
isActive:{ type: Boolean, default: true },
hobbies: [String],
address: { street: String, city: String, zip: String },
author: { type: Schema.Types.ObjectId, ref: 'User' }
}, { timestamps: true, toJSON: { virtuals: true } });
const User = mongoose.model('User', userSchema);
Schema Types
| Type | Example |
|---|
String | 'Alice' |
Number | 25, 3.14 |
Boolean | true |
Date | new Date() |
ObjectId | Schema.Types.ObjectId |
Array | [String], [{ name: String }] |
Decimal128 | Schema.Types.Decimal128 |
Mixed | Schema.Types.Mixed (any, no validation) |
Buffer | Binary data |
Map | Dynamic key-value |
Validators
| Validator | Applies To | Example |
|---|
required | All | required: [true, 'Name required'] |
min / max | Number, Date | min: 0, max: 150 |
minlength / maxlength | String | minlength: 2 |
enum | String, Number | enum: ['a', 'b', 'c'] |
match | String | match: /^\S+@\S+\.\S+$/ |
validate | All (custom) | validate: { validator: fn, message: '...' } |
Note: unique is a MongoDB index, not a Mongoose validator. Catch with error code 11000.
CRUD Operations
Create
const user = await User.create({ name: 'Alice', email: 'alice@example.com' });
Read
const all = await User.find({});
const filtered = await User.find({ role: 'admin' });
const one = await User.findOne({ email: 'alice@example.com' });
const byId = await User.findById(id);
const count = await User.countDocuments({ role: 'user' });
Update
const updated = await User.findByIdAndUpdate(id,
{ name: 'New Name' },
{ new: true, runValidators: true }
);
Delete
const deleted = await User.findByIdAndDelete(id);
Query Operators
| Operator | Meaning | Example |
|---|
$gt | Greater than | { age: { $gt: 25 } } |
$gte | Greater or equal | { age: { $gte: 18 } } |
$lt | Less than | { age: { $lt: 30 } } |
$lte | Less or equal | { age: { $lte: 65 } } |
$ne | Not equal | { role: { $ne: 'banned' } } |
$in | In array | { role: { $in: ['admin', 'mod'] } } |
$nin | Not in array | { role: { $nin: ['banned'] } } |
$or | Logical OR | { $or: [{...}, {...}] } |
$and | Logical AND | { $and: [{...}, {...}] } |
$regex | Pattern match | { name: { $regex: /^A/i } } |
$exists | Field exists | { field: { $exists: true } } |
$elemMatch | Array element match | { arr: { $elemMatch: {...} } } |
Update Operators
| Operator | Description | Example |
|---|
$set | Set field | { $set: { name: 'New' } } |
$unset | Remove field | { $unset: { temp: '' } } |
$inc | Increment | { $inc: { count: 1 } } |
$push | Add to array | { $push: { tags: 'new' } } |
$pull | Remove from array | { $pull: { tags: 'old' } } |
$addToSet | Add if unique | { $addToSet: { tags: 'x' } } |
$pop | Remove first/last | { $pop: { arr: 1 } } |
Projection, Sorting, Pagination
await User.find({ role: 'user' })
.select('name email -_id')
.sort({ createdAt: -1 })
.skip((page - 1) * limit)
.limit(limit)
.lean();
lean()
| With lean | Without lean |
|---|
| Plain JS objects | Mongoose Documents |
| No virtuals, no methods, no save() | Full Mongoose features |
| 2-5x faster | Standard speed |
| Use for: read-only, API responses | Use for: modify + save |
Populate (Joins)
const post = await Post.findById(id).populate('author');
const post = await Post.findById(id).populate('author', 'name email');
const comment = await Comment.findById(id)
.populate('author', 'name')
.populate('post', 'title');
const comment = await Comment.findById(id)
.populate({ path: 'post', populate: { path: 'author', select: 'name' } });
await Author.findById(id).populate({
path: 'posts',
match: { isPublished: true },
options: { sort: { createdAt: -1 }, limit: 5 }
});
Virtual Populate
authorSchema.virtual('posts', {
ref: 'Post',
localField: '_id',
foreignField: 'author'
});
const author = await Author.findById(id).populate('posts');
Embed vs Reference
| Criteria | Embed | Reference |
|---|
| Relationship | 1:1, 1:few | 1:many, many:many |
| Accessed together? | Yes | Not always |
| Size growth | Bounded | Unbounded OK |
| Reads | Single query | Multiple (populate) |
| Writes | Risk of large updates | Small, targeted |
| Consistency | Atomic (one doc) | May need transactions |
1:1 → Embed (almost always)
1:Few → Embed (usually)
1:Many → Reference (child refs parent)
Many:Many → Reference (arrays or junction collection)
Middleware (Hooks)
userSchema.pre('save', async function(next) {
if (!this.isModified('password')) return next();
this.password = await bcrypt.hash(this.password, 10);
next();
});
userSchema.pre('find', function() {
this.where({ isDeleted: { $ne: true } });
});
Virtuals
userSchema.virtual('fullName').get(function() {
return `${this.firstName} ${this.lastName}`;
});
Error Handling
app.use((err, req, res, next) => {
if (err.name === 'ValidationError') {
const errors = Object.values(err.errors).map(e => e.message);
return res.status(400).json({ error: 'Validation failed', details: errors });
}
if (err.code === 11000) {
const field = Object.keys(err.keyValue)[0];
return res.status(409).json({ error: `Duplicate ${field}` });
}
if (err.name === 'CastError') {
return res.status(400).json({ error: `Invalid ${err.path}` });
}
res.status(500).json({ error: 'Internal server error' });
});
| Error | Status | Cause |
|---|
ValidationError | 400 | Required field missing, min/max fail |
code: 11000 | 409 | Duplicate unique key |
CastError | 400 | Invalid ObjectId format |
Model File Pattern
models/
├── User.js // Schema + validators + virtuals + methods + hooks + export
├── Post.js
└── Comment.js
const mongoose = require('mongoose');
const userSchema = new mongoose.Schema({ }, { timestamps: true });
module.exports = mongoose.model('User', userSchema);
One-Liners
- MongoDB = document database; flexible schema; stores BSON.
- Mongoose = ODM that adds schemas, validation, middleware, populate.
- ObjectId = 12-byte auto-generated _id; contains timestamp.
- Embed = nested data in one document. Reference = ObjectId pointer to another collection.
- populate() = application-level join; at least 2 queries.
- lean() = skip Mongoose wrapper; return plain objects; faster reads.
- pre('save') = middleware that runs before
.save(); use for password hashing.
- Virtuals = computed properties not stored in DB; need
toJSON: { virtuals: true }.
End of 3.8 quick revision.