Mongoose MongoDB ODM

repository·master·Indexed 12 days ago

https://github.com/automattic/mongoose

An object modeling tool for MongoDB designed for asynchronous environments. Mongoose provides a schema-based solution to model application data, including built-in type validation, default values, and middleware/plugin support. Version 9.9.2 supports Node.js and alpha support for Deno.

Tokens
113K
Snippets
392
Records
460
Agent score
97%

What's inside Mongoose

  1. Understand Mongoose version support and lifecycles

    master

    Mongoose follows a major version lifecycle. Choosing the right version depends on whether you need new features, security patches, or are maintaining legacy systems:

    • Mongoose 9 (Current): The latest major version. Use this for the best experience, including all new features and improvements.
    • Mongoose 8 (Prior): Receives new features, improvements, and bug fixes until at least February 1, 2026.
    • Mongoose 7 (Legacy): Receives important fixes only; not the primary development focus.
    • Mongoose 6 (Limited Maintenance): Receives only security patches and requested bug fixes. End of Life is February 1, 2027, after which no updates (including security) will be provided.
    • Mongoose 5 (End of Life): No longer maintained or updated as of March 1, 2024.
  2. What is a Mongoose Document?

    master

    A Mongoose document is an instance of a Model class that is backed by MongoDB data. The class hierarchy is Document < Model < User (your specific model). Documents provide built-in support for change tracking, casting, validation, middleware, and persistence via the save() method.

    const User = mongoose.model('User', new Schema({ name: String }));
    
    // `doc` is a document instance
    const doc = new User({ name: 'John Smith' });
    
    doc instanceof User; // true
    doc instanceof mongoose.Model; // true
    doc instanceof mongoose.Document; // true
  3. What is a SchemaType?

    master

    A SchemaType is a configuration object for an individual property within a Mongoose schema. It defines the data type for a path, validation rules, getters/setters, and default values.

    Important Distinction: A SchemaType is a configuration object, not the data itself. For example, mongoose.Schema.Types.ObjectId is a configuration for a schema path, whereas mongoose.Types.ObjectId is the actual constructor used to create a MongoDB ObjectId instance.

    const schema = new Schema({ name: String });
    schema.path('name') instanceof mongoose.SchemaType; // true
    schema.path('name') instanceof mongoose.Schema.Types.String; // true
  4. Understand `minimize` behavior on `save()` for existing documents

    master

    In Mongoose 7, the minimize option (which strips empty objects) only applied when saving a new document. In Mongoose 8, minimize is also applied when save() is called on an existing document.

    const schema = new Schema({
      nested: {
        field1: Number
      }
    });
    const Test = mongoose.model('Test', schema);
    
    // Both Mongoose 7 and Mongoose 8 strip out empty objects when saving
    // a new document in MongoDB by default
    const { _id } = await Test.create({ nested: {} });
    let rawDoc = await Test.findById(_id).lean();
    rawDoc.nested; // undefined
    
    // Mongoose 8 will also strip out empty objects when saving
    // an existing document in MongoDB
    const doc = await Test.findById(_id);
    doc.nested = {};
    doc.markModified('nested');
    await doc.save();
    
    let rawDoc = await Test.findById(_id).lean();
    rawDoc.nested; // undefined in Mongoose 8, {} in Mongoose 7
  5. Manage one-to-many relationships with child refs

    master

    While one-to-many relationships are typically handled by a parent pointer on the 'many' side, you can maintain an array of child pointers on the 'one' side. To make these available for population, you must explicitly push() the child document into the parent's array and save() the parent.

    Alternatively, instead of maintaining two sets of pointers (which can get out of sync), you can simply query the child collection directly using the parent's ID.

    // Option 1: Pushing refs to children to allow .populate('stories')
    author.stories.push(story1);
    await author.save();
    
    // Option 2: Direct query (often safer to avoid sync issues)
    const stories = await Story.find({ author: author._id }).exec();
  6. Understand the difference between Query and Document middleware for updates

    master

    When using update hooks like updateOne() or findOneAndUpdate():

    • Query Middleware: this refers to the Query object. You can modify the query using this.set(), but you cannot access the document being updated directly. To access the document, you must perform an explicit query (e.g., this.model.findOne(this.getQuery())).
    • Document Middleware: this refers to the Document being updated. This is triggered when calling doc.updateOne() rather than Model.updateOne().

    Important: pre('save') and post('save') hooks are not executed during update() or findOneAndUpdate() calls.

    // Query middleware: 'this' is the Query
    schema.pre('updateOne', function() {
      this.set({ updatedAt: new Date() });
    });
    
    // Query middleware: Accessing the document via explicit query
    schema.pre('findOneAndUpdate', async function() {
      const docToUpdate = await this.model.findOne(this.getQuery());
      console.log(docToUpdate);
    });
    
    // Document middleware: 'this' is the Document
    schema.pre('updateOne', { document: true, query: false }, function() {
      console.log('Updating document:', this);
    });
  7. Manage document IDs (_id)

    master

    Mongoose automatically adds an _id property of type ObjectId to every schema.

    Customizing _id:

    • Overwriting: You can define your own _id path (e.g., _id: Number). If you do this, you are responsible for setting the _id before saving, otherwise Mongoose will throw an error: "document must have an _id before saving".
    • Disabling for Subdocuments: You can disable the automatic _id on subdocuments by passing { _id: false } in the subdocument's schema options or by setting _id: false within the subdocument's schema definition.
    // Overwriting default _id
    const schema = new Schema({
      _id: Number
    });
    const Model = mongoose.model('Test', schema);
    const doc = new Model();
    doc._id = 1;
    await doc.save(); // works
    
    // Disabling _id on a subdocument
    const nestedSchema = new Schema(
      { name: String },
      { _id: false }
    );
  8. Handle missing foreign documents during population

    master

    Mongoose population behaves like a SQL left join.

    • If a single reference path (e.g., author) finds no matching document, the field will be null.
    • If an array reference path (e.g., fans) finds no matching documents, the field will be an empty array [].
    // Single ref: returns null if not found
    const story = await Story.findOne({ title: 'Casino Royale' }).populate('author');
    story.author; // null
    
    // Array ref: returns [] if nothing matches
    const storyWithAuthors = await Story.findOne({ title: 'Casino Royale' }).populate('authors');
    storyWithAuthors.authors; // []
  9. Virtuals and Lean queries

    master

    When using .lean(), Mongoose returns Plain Old JavaScript Objects (POJOs) instead of full Mongoose documents. Because virtuals are properties of Mongoose documents, they are not available on lean documents.

    If you need virtuals while using lean() for performance, use the mongoose-lean-virtuals plugin.

    const fullDoc = await User.findOne();
    fullDoc.domain; // 'gmail.com' (Full Mongoose document)
    
    const leanDoc = await User.findOne().lean();
    leanDoc.domain; // undefined (POJO)
  10. Note on Timezones and MongoDB storage

    master
    MongoDB stores dates as 64-bit integers. Consequently, Mongoose does not store timezone information by default. When you retrieve a date and call Date#toString(), the JavaScript runtime will display the time using your local operating system's timezone.
  11. Use Getters to transform path values

    master

    Getters allow you to transform a value when it is accessed from a document. They act similarly to virtuals but are defined directly on a path.

    Best Practices:

    • Use getters on primitive paths (like Strings or Numbers).
    • Avoid declaring getters on arrays or subdocuments. Declaring a getter on an array or object can break Mongoose's change tracking or cause unexpected behavior (e.g., returning undefined when trying to push new elements) because the getter returns a new instance every time the path is accessed.
    • If you need to transform a nested value within an array, declare the getter on the specific nested path (e.g., arr.0.url) rather than the array itself.
    const root = 'https://s3.amazonaws.com/mybucket';
    
    const userSchema = new Schema({
      name: String,
      picture: {
        type: String,
        get: v => `${root}${v}`
      }
    });
    
    const User = mongoose.model('User', userSchema);
    
    const doc = new User({ name: 'Val', picture: '/123.png' });
    doc.picture; // 'https://s3.amazonaws.com/mybucket/123.png'
    
    // To bypass getters when converting to an object:
    doc.toObject({ getters: false }).picture; // '/123.png'
  12. Understand Date casting edge cases

    master

    Mongoose's date casting has specific behaviors that differ from native JavaScript:

    1. Object Casting: Mongoose calls the .valueOf() method on objects before casting. This allows libraries like moment to be cast to dates automatically.
    2. Numeric Strings: While native JavaScript new Date('1552261496289') results in an Invalid Date, Mongoose detects if a numeric string falls outside the representable JavaScript date range and converts it to a number before passing it to the constructor. This allows numeric strings representing timestamps to be cast correctly.
    // Casting a moment object automatically
    const moment = require('moment');
    const user = new User({
      name: 'Jean-Luc Picard',
      lastActiveAt: moment.utc('2002-12-09')
    });
    
    // Casting a numeric string timestamp
    const userWithTimestamp = new User({
      name: 'Jean-Luc Picard',
      lastActiveAt: '1552261496289'
    });