logo
eng-flag

Mongoose Cheatsheet

Table of Contents

  1. Installation
  2. Connecting to MongoDB
  3. Defining Schemas
  4. Creating Models
  5. CRUD Operations
  6. Querying
  7. Population
  8. Middleware
  9. Validation
  10. Indexing
  11. Virtuals
  12. Plugins
  13. Transactions
  14. Error Handling
  15. Best Practices

Installation

Install Mongoose in your project:

npm install mongoose

Connecting to MongoDB

Install Mongoose in your project:

const mongoose = require("mongoose");

// Connect to local MongoDB
mongoose.connect("mongodb://localhost/mydatabase", {
  useNewUrlParser: true,
  useUnifiedTopology: true,
});

// Connect to MongoDB Atlas
mongoose.connect(
  "mongodb+srv://<username>:<password>@cluster0.mongodb.net/mydatabase",
  {
    useNewUrlParser: true,
    useUnifiedTopology: true,
  }
);

// Handle connection events
const db = mongoose.connection;
db.on("error", console.error.bind(console, "connection error:"));
db.once("open", function () {
  console.log("Connected to MongoDB");
});

Defining Schemas

Schemas define the structure of documents in a collection.

const mongoose = require("mongoose");
const Schema = mongoose.Schema;

// Basic schema
const userSchema = new Schema({
  name: String,
  email: String,
  age: Number,
});

// Schema with more options
const productSchema = new Schema({
  name: {
    type: String,
    required: true,
    trim: true,
  },
  price: {
    type: Number,
    min: 0,
  },
  category: {
    type: String,
    enum: ["Electronics", "Books", "Clothing"],
  },
  inStock: {
    type: Boolean,
    default: true,
  },
  createdAt: {
    type: Date,
    default: Date.now,
  },
});

Creating Models

Models are fancy constructors compiled from Schema definitions.

const User = mongoose.model("User", userSchema);
const Product = mongoose.model("Product", productSchema);

CRUD Operations

Create

// Create a single document
const newUser = new User({
  name: "John Doe",
  email: "john@example.com",
  age: 30,
});

newUser.save((err, user) => {
  if (err) return console.error(err);
  console.log("User saved:", user);
});

// Create multiple documents
User.create(
  [
    { name: "Jane Doe", email: "jane@example.com", age: 25 },
    { name: "Bob Smith", email: "bob@example.com", age: 35 },
  ],
  (err, users) => {
    if (err) return console.error(err);
    console.log("Users created:", users);
  }
);

Read

// Find all documents
User.find({}, (err, users) => {
  if (err) return console.error(err);
  console.log("All users:", users);
});

// Find documents with specific criteria
User.find({ age: { $gte: 18 } }, (err, users) => {
  if (err) return console.error(err);
  console.log("Adult users:", users);
});

// Find one document
User.findOne({ email: "john@example.com" }, (err, user) => {
  if (err) return console.error(err);
  console.log("Found user:", user);
});

// Find by ID
User.findById("5f7c3b3f9d3e2a1234567890", (err, user) => {
  if (err) return console.error(err);
  console.log("User by ID:", user);
});

Update

// Update one document
User.updateOne({ name: "John Doe" }, { age: 31 }, (err, result) => {
  if (err) return console.error(err);
  console.log("Update result:", result);
});

// Update multiple documents
User.updateMany({ age: { $lt: 18 } }, { isMinor: true }, (err, result) => {
  if (err) return console.error(err);
  console.log("Update result:", result);
});

// Find and update
User.findOneAndUpdate(
  { email: "john@example.com" },
  { $inc: { age: 1 } },
  { new: true },
  (err, updatedUser) => {
    if (err) return console.error(err);
    console.log("Updated user:", updatedUser);
  }
);

Delete

// Delete one document
User.deleteOne({ name: "John Doe" }, (err) => {
  if (err) return console.error(err);
  console.log("User deleted");
});

// Delete multiple documents
User.deleteMany({ age: { $lt: 18 } }, (err) => {
  if (err) return console.error(err);
  console.log("Minor users deleted");
});

// Find and delete
User.findOneAndDelete({ email: "john@example.com" }, (err, deletedUser) => {
  if (err) return console.error(err);
  console.log("Deleted user:", deletedUser);
});

Querying

Mongoose provides a rich query API.

// Basic querying
User.find({ age: { $gte: 18 } })
  .sort({ name: 1 })
  .limit(10)
  .select("name email")
  .exec((err, users) => {
    if (err) return console.error(err);
    console.log("Adult users:", users);
  });

// Chaining queries
User.find({ isActive: true })
  .where("age")
  .gte(18)
  .lte(65)
  .where("email")
  .ne(null)
  .limit(50)
  .sort("-lastLogin")
  .select("name email")
  .exec((err, users) => {
    if (err) return console.error(err);
    console.log("Active adult users:", users);
  });

// Using query builders
const query = User.find({ isActive: true });
query.where("age").gte(18).lte(65);
query.where("email").ne(null);
query.limit(50).sort("-lastLogin").select("name email");
query.exec((err, users) => {
  if (err) return console.error(err);
  console.log("Active adult users:", users);
});

// Using $or operator
User.find(
  {
    $or: [{ age: { $lt: 18 } }, { age: { $gt: 65 } }],
  },
  (err, users) => {
    if (err) return console.error(err);
    console.log("Users not of working age:", users);
  }
);

// Using regex
User.find({ name: /^John/ }, (err, users) => {
  if (err) return console.error(err);
  console.log("Users with names starting with John:", users);
});

Population

Population is the process of automatically replacing specified paths in the document with document(s) from other collection(s).

// Define schemas with references
const authorSchema = new Schema({
  name: String,
  bio: String,
});

const bookSchema = new Schema({
  title: String,
  author: { type: Schema.Types.ObjectId, ref: "Author" },
});

const Author = mongoose.model("Author", authorSchema);
const Book = mongoose.model("Book", bookSchema);

// Create an author and a book
const author = new Author({ name: "John Doe", bio: "A prolific writer" });
author.save((err, savedAuthor) => {
  if (err) return console.error(err);

  const book = new Book({ title: "My Great Novel", author: savedAuthor._id });
  book.save((err, savedBook) => {
    if (err) return console.error(err);
    console.log("Book saved:", savedBook);
  });
});

// Populate the author when querying for books
Book.findOne({ title: "My Great Novel" })
  .populate("author")
  .exec((err, book) => {
    if (err) return console.error(err);
    console.log("Book with author details:", book);
  });

// Populate specific fields
Book.findOne({ title: "My Great Novel" })
  .populate("author", "name")
  .exec((err, book) => {
    if (err) return console.error(err);
    console.log("Book with author name:", book);
  });

// Nested population
const publisherSchema = new Schema({
  name: String,
  location: String,
});

const bookSchema = new Schema({
  title: String,
  author: { type: Schema.Types.ObjectId, ref: "Author" },
  publisher: { type: Schema.Types.ObjectId, ref: "Publisher" },
});

const Publisher = mongoose.model("Publisher", publisherSchema);
const Book = mongoose.model("Book", bookSchema);

Book.findOne({ title: "My Great Novel" })
  .populate({
    path: "author",
    populate: { path: "publisher" },
  })
  .exec((err, book) => {
    if (err) return console.error(err);
    console.log("Book with author and publisher details:", book);
  });

Middleware

Middleware (pre and post hooks) are functions which are passed control during execution of asynchronous functions.

// Pre-save middleware
userSchema.pre("save", function (next) {
  // 'this' refers to the document being saved
  if (this.isModified("password")) {
    this.password = hashPassword(this.password);
  }
  next();
});

// Post-save middleware
userSchema.post("save", function (doc, next) {
  console.log("%s has been saved", doc._id);
  next();
});

// Pre-find middleware
userSchema.pre("find", function () {
  // 'this' refers to the query
  this.start = Date.now();
});

userSchema.post("find", function (result) {
  console.log("Query took %d milliseconds", Date.now() - this.start);
});

// Error handling middleware
userSchema.post("save", function (error, doc, next) {
  if (error.name === "MongoError" && error.code === 11000) {
    next(new Error("There was a duplicate key error"));
  } else {
    next(error);
  }
});

Validation

Mongoose provides built-in and custom validators.

const userSchema = new Schema({
  name: {
    type: String,
    required: true,
    minlength: 2,
    maxlength: 50,
  },
  email: {
    type: String,
    required: true,
    unique: true,
    lowercase: true,
    validate: {
      validator: function (v) {
        return /^w+([.-]?w+)*@w+([.-]?w+)*(.w{2,3})+$/.test(v);
      },
      message: (props) => `${props.value} is not a valid email address!`,
    },
  },
  age: {
    type: Number,
    min: [18, "Must be at least 18 years old"],
    max: [120, "Age seems unrealistic"],
  },
  interests: {
    type: [String],
    validate: {
      validator: function (v) {
        return v && v.length > 0;
      },
      message: "A user must have at least one interest",
    },
  },
});

// Custom async validator
userSchema.path("email").validate(async function (value) {
  const emailCount = await mongoose.models.User.countDocuments({
    email: value,
  });
  return !emailCount;
}, "Email already exists");

// Using validate()
const user = new User({
  name: "J",
  email: "invalid-email",
  age: 15,
  interests: [],
});

user.validate((err) => {
  if (err) console.log(err.message);
});

Indexing

Indexes support the efficient execution of queries in MongoDB.

const userSchema = new Schema({
  name: String,
  email: { type: String, unique: true },
  createdAt: Date,
});

// Single field index
userSchema.index({ name: 1 });

// Compound index
userSchema.index({ name: 1, createdAt: -1 });

// Text index
userSchema.index({ name: "text", email: "text" });

// Geospatial index
const locationSchema = new Schema({
  name: String,
  location: {
    type: { type: String, default: "Point" },
    coordinates: [Number],
  },
});

locationSchema.index({ location: "2dsphere" });

// Creating indexes
mongoose.connect("mongodb://localhost/test", function (error) {
  if (error) console.error(error);
  else console.log("connected");

  User.createIndexes(function (error) {
    if (error) console.error(error);
    else console.log("indexes created");
  });
});

Virtuals

Virtuals are document properties that you can get and set but that do not get persisted to MongoDB.

const personSchema = new Schema({
  firstName: String,
  lastName: String,
});

// Virtual for full name
personSchema
  .virtual("fullName")
  .get(function () {
    return this.firstName + " " + this.lastName;
  })
  .set(function (v) {
    this.firstName = v.substr(0, v.indexOf(" "));
    this.lastName = v.substr(v.indexOf(" ") + 1);
  });

const Person = mongoose.model("Person", personSchema);

const person = new Person({
  firstName: "John",
  lastName: "Doe",
});

console.log(person.fullName); // 'John Doe'

person.fullName = "Jane Smith";
console.log(person.firstName); // 'Jane'
console.log(person.lastName); // 'Smith'

Plugins

Plugins are a tool for reusing logic in multiple schemas.

// Define a plugin
const lastModifiedPlugin = function (schema, options) {
  schema.add({ lastMod: Date });

  schema.pre("save", function (next) {
    this.lastMod = new Date();
    next();
  });

  if (options && options.index) {
    schema.path("lastMod").index(options.index);
  }
};

// Use the plugin
const userSchema = new Schema({ name: String, email: String });
userSchema.plugin(lastModifiedPlugin, { index: true });

// Alternatively, apply the plugin to all schemas
mongoose.plugin(lastModifiedPlugin);

Transactions

Transactions let you execute multiple operations in isolation and potentially undo all the operations if one of them fails.

const session = await mongoose.startSession();
session.startTransaction();

try {
  const opts = { session };
  const A = await Account.findOne({ name: "A" }, null, opts);
  const B = await Account.findOne({ name: "B" }, null, opts);

  A.balance -= 100;
  B.balance += 100;

  await A.save(opts);
  await B.save(opts);

  await session.commitTransaction();
  session.endSession();
} catch (error) {
  await session.abortTransaction();
  session.endSession();
  throw error;
}

Error Handling

Proper error handling is crucial for robust applications. Here are some ways to handle errors in Mongoose:

// Using callbacks
User.findById(id, (err, user) => {
  if (err) {
    if (err.name === "CastError") {
      return console.error("Invalid ID");
    }
    return console.error(err);
  }
  console.log(user);
});

// Using promises
User.findById(id)
  .then((user) => {
    console.log(user);
  })
  .catch((err) => {
    if (err.name === "CastError") {
      console.error("Invalid ID");
    } else {
      console.error(err);
    }
  });

// Using async/await
async function findUser(id) {
  try {
    const user = await User.findById(id);
    console.log(user);
  } catch (err) {
    if (err.name === "CastError") {
      console.error("Invalid ID");
    } else {
      console.error(err);
    }
  }
}

// Global error handler for Mongoose
mongoose.connection.on("error", (err) => {
  console.error("MongoDB connection error:", err);
});

// Custom error handling middleware (for Express.js)
app.use((err, req, res, next) => {
  if (err instanceof mongoose.Error.ValidationError) {
    return res.status(400).json({ error: err.message });
  }
  if (err.name === "MongoError" && err.code === 11000) {
    return res.status(409).json({ error: "Duplicate key error" });
  }
  next(err);
});

Best Practices

Here are some best practices to follow when using Mongoose:

  1. Use Schemas: Always define schemas for your models. This ensures data consistency and allows for validation.
const userSchema = new Schema({
  name: { type: String, required: true },
  email: { type: String, required: true, unique: true },
  age: { type: Number, min: 18 },
});
  1. Validation: Use built-in and custom validators to ensure data integrity.
userSchema.path("email").validate(function (email) {
  return /^[^s@]+@[^s@]+.[^s@]+$/.test(email);
}, "Invalid email format");
  1. Middleware: Use middleware for repetitive tasks like data transformation or logging.
userSchema.pre("save", function (next) {
  this.updatedAt = Date.now();
  next();
});
  1. Indexing: Create indexes for frequently queried fields to improve performance.
userSchema.index({ email: 1 }, { unique: true });
  1. Lean Queries: Use .lean() for read-only operations to get plain JavaScript objects instead of Mongoose documents.
User.find()
  .lean()
  .exec((err, users) => {
    console.log(users); // Plain JavaScript objects
  });
  1. Projections: Use projections to select only the fields you need.
User.find({}, "name email", (err, users) => {
  console.log(users); // Only name and email fields
});
  1. Population: Use population to work with references efficiently.
Post.find()
  .populate("author")
  .exec((err, posts) => {
    console.log(posts); // Posts with author details
  });
  1. Error Handling: Always handle potential errors, especially in production environments.
User.findById(id)
  .then((user) => {
    // Handle success
  })
  .catch((err) => {
    // Handle error
  });
  1. Connections: Manage database connections properly.
mongoose
  .connect(uri, { useNewUrlParser: true, useUnifiedTopology: true })
  .then(() => console.log("Connected to MongoDB"))
  .catch((err) => console.error("Could not connect to MongoDB", err));
  1. Transactions: Use transactions for operations that require atomicity.
const session = await mongoose.startSession();
session.startTransaction();
try {
  // Perform operations
  await session.commitTransaction();
} catch (error) {
  await session.abortTransaction();
} finally {
  session.endSession();
}

2024 © All rights reserved - buraxta.com