Table of contents
1.
Schemas
2.
Creating a model
3.
Ids 
4.
Instance methods
5.
Statics
6.
Query helpers
7.
Indexes
8.
Virtuals
9.
Aliases
10.
Options
10.1.
Option: autoIndex
10.2.
Option: autoCreate
10.3.
Option: bufferCommands
10.4.
Option: bufferTimeoutMS
10.5.
Option: capped
10.6.
Option: collection
10.7.
Option: discriminatorKey
10.8.
Option: id
10.9.
Option: _id
10.10.
Option: read
10.11.
Option: writeConcern
10.12.
Option: shardKey
10.13.
Option: strict
11.
ES6 classes
12.
Frequently asked questions:
Last Updated: Mar 27, 2024

Mongoose schema

Career growth poll
Do you think IIT Guwahati certified course can help you in your career?

Schemas

Before we start forward, we must be very clear with the schema. Everything in mongoose starts its journey from the schema itself. Each schema rounds off to a MongoDB collection and is responsible for defining the shape of the documents within that collection.

 

import mongoose from 'mongoose';
  const { Schema } = mongoose;

  const blogSchema = new Schema({
    titleType:  String// String is shorthand for {type: String}.
    authorType: String,
    body_:   String,
    comments: [{ body: String, date: Date }],
    date_: { type: Date, default: Date.now },
    hidden: Boolean,
    meta: {
      votesPerson: Number_,
      favs_:  Number_
    }
  });

 

Additional keys can be attached using the Schema#add method. blogSchema is defining the property that will be cast into its associated schemaType. Let’s take an example: the title property will be cast to the String schemaType, and property date will be cast to Date schemaType.

 

If the property requires only a single type,a orthand method is used. The keys can also assign nested objects. Such cases can only occur if a key’s value is a POJO type

Actual schema paths for leaves in the tree are only created by the Mongoose, while the branches don’t contain actual paths. The wrong side of using these is that the meta doesn’t have its validation. If any kind of validation is required, a new path has to be created up the tree. 

The schemaTypes permitted are:

  • String
  • Number
  • Date
  • Array
  • Map
  • Boolean
  • buffer

 

The common question you might be thinking is what exactly are these Schemas? Come, let’s figure it out!!

 

The Schemas define the structure of the document. They are also responsible for casting different properties. Beyond that, they can also define document instance methods, static Model methods, compound indexes, and document lifecycle hooks. The document lifecycle hooks are referred to as middleware.

Creating a model

For using the schema definition, the blogSchema needs to be converted into a Model for working purposes. For doing so, the mongoose.model(model_name, schema) is passed. 

 

const Blog_helper = mongoose.model('Blgg', _blogSchema);

 

Ids 

An _id property is present by default in the schemas. 

const schema = new Schema();
schema.path('_id'); // ObjectId { ... }.

 

 

A new document creation with the automatic added _id property, Mongoose is responsible for creating unique _id of type objectId to the record.

 

const Model = mongoose.model('Test', schema);

const doc = new Model();
doc._id instanceof mongoose.Types.ObjectId; // True condition.

 

If required, the default  _id of mongoose can also be overwritten by own _id. The reason is that Mongoose can never save a document that doesn’t have an _id. So, if you are creating a new _id at your convenience, it is your responsibility to set them too.

 

const schema_ = new Schema({ _id: Num });
const Model = mongoose.model('Practice', schema_);

const doc = new Model();
await doc.save(); 

// This program will throw:"document must have an _id before saving".

doc._id = 10;
await doc.save(); // Working

 

Instance methods

Models have instances, which are called documents. Many built-in instance methods are contained by documents. If desired, our own desired methods can also be defined. 

 

// Defining a schema.
  const animalSchema = new Schema({ name: Str, type: String });

  // Function is assigned to the "methods" object of our animalSchema.
  animalSchema.methods.findSimilarTypes = function(cb) {
    return mongoose.model('CodingNinja').find({ type: this.type }, cb);
  };

 

findSimilarTypes method is available for all instances of the animal. 

 

const Animal = mongoose.model('Animal', animalSchema);
  const dog = new Animal({ type: 'dog' });

  dog.findSimilarTypes((err, dogs) => {
    console.log(dogs); // woof.
  });

 

  • If a default mongoose document is overwritten or changed, such a scenario may lead to unpredictable results.
  • In the above example,create,areMongoDBMongoDB supports secondary indexesmethods is used directly for saving an instance method.
  • Never declare the methods using ES6 arrow functions(=>). The reason is that the arrow function prevents the explicit binding of this. Hence, methods will not have access to the document, and the above function will not work. 

 

Statics

Static functions can also be added to the model. There are two ways for adding static:

  • A function property is added to the schema.statics.
  • The Function#schema is called.

 

// Function is assigned to the "statics" object of our animalSchema.
  animalSchema.statics.findByName = function(name_) {
    return this.find({ name: new RegExp(name, 'hello') });
  };
  // Or, `animalSchema.static()`can be called.
  animalSchema.static('findBygen'function(breed) { return this.find({ gen }); });

  const Animal = mongoose.model('Animal', animalSchema);
  let animals = await Animal.findBygen('fido');
  animals = animals.concat(await Animal.findByBreed('Poodle'));

 

 

The statics should never be declared using ES6 arrow functions(=>). The reason is that the arrow function prevents the explicit binding of this. Hence, methods will not have access to the document and the above function will not work. 

 

Query helpers

Query helper functions can also be added, similar to the instance methods used for mongoose queries. Mongoose’s chainable query builder API can be extended by the query helper methods.

 

animalSchema.query.byName = function(Name) {
    return this.where({ name: new RegExp(Name, 'hello') })
  };

  const Ani = mongoose.model('Ani', animalSchema);

  Animal.find().byName('codingNinja').exec((err_, animals) => {
    console.log(animals);
  });

  Animal.findOne().byName('codingNinja').exec((err, animal) => {
    console.log(animal);
  });

 

Indexes

Secondary indexes is supported by mongoDB. These indexes are defined inside the schema at the path level or the schema level. When the creation of compound indexes, it is necessary to define indexes at the schema level.

 

const animalSchema = new Schema({
    name_: String,
    Ctype_: String,
    tags: { Ctype_: [String], index_: true// This shows the field level.
  });

  animalSchema.index({ name_: 11, Ctype_: -1 }); // This shows the schema level.

 

After the application startup, the createIndex is automatically called by the Mongoose. For each particular index, this index will be called sequentially, and if no error is there, the index event will be emitted. For better performance, it is recommended to disable it while production time to avoid significant performance impact. This behavior can be disabled by setting the autoIndex option to false.

 

mongoose.connect('mongodb://users:pass_@loca:port_/database', { autoIndex: false });
  // or
  mongoose.createConnection('mongodb://user:pass@local:port__/database', { autoIndex: false });
  // or
  animalSchema.set('autoIndex_'false);
  // or
  new Schema({..}, { autoIndex_: false });

 

Consider the situation when error occurred; there also Mongoose will emit an index event on the model.

 

// this Will create an error because MongoDB has an _id index by default that
// is not sparse.
  animalSchema.index({ _id: 1 }, { sparse: true });
  const Animal_ = mongoose.model('Animal_s', animalSchema_);

  Animal.on('iCode', error => {
    // The "_id index cannot be sparse".
    console.log(error.message);
  });

 

Virtuals

The document properties that can get and set but are not persisted to MongoDB. Such properties are referred to as virtuals. The getters and setters play an essential role here. The getters help in formatting or combining fields whereas setters contribute to de-composing a single value into multiple storage values.

 

// Defining a schema.
  const personSchema = new Schema({
    name: {
      firstName: String,
      lastName: String
    }
  });

  // Compiling the model.
  const Person = mongoose.model('Helper', personSchema);

  // Creating a document.
  const axl_ = new Person({
    name: { first: 'coding', last: 'Ninja' }
  });

 

Let’s take an example, for printing persons name:

 

console.log(axl.name.first + ' ' + axl.name.last); // Axl Rose.

 

The concatenation of first and last name every time can be very hectic. Ad what if we want to do some extra modifications to the title, like removing diacritics? 

This can be easing achieved by a virtual property getter. It helps to define a fullName property that is not persisted to MongoDB. 

 

personSchema.virtual('fullName').get(function() {
  return this.name.first + ' 'this.name.last;
});

 

Now, mongoose will be responsible for calling getter function each time; the fullName property will be asked.

console.log(axl.fullName); // Axl Rose.

 

For toJSON() and toObject() mongoose, the virtuals is not included by default. They contain the output of calling JSON.stringify()

In other words, a custom setter can also be added to the virtual, which helps to set both first name and last name via the fullName virtual.

 

personSchema.virtual('fullName').
  get(function() {
    return this.name.first + ' 'this.name.last;
    }).
  set(function(v) {
    this.name.first = v.substr(0, v.indexOf(' '));
    this.name.last = v.substr(v.indexOf(' ') + 1);
  });

axl.fullName = 'William Rose'// Now `axl.name.first` is "William".

 

Before any other validation, virtual property setters are applied. Even if the first and last name fields are in use, the above example would still work.

 

Only non-virtual properties can be used as a part of queries and for field selection. This is because virtuals are not stored in MongoDB so that we can use them for query processing.

 

Aliases

This is an important type of virtual. It helps the getter and setter to work efficiently in setting up other properties. The network bandwidth can be saved using aliases. It allows users to change a short property name to a longer name for better code readability.

 

const personSchema_ = new Schema({
  n: {
    type: String,
    // Accessing the`name` will get you the value of `n,` and setting `name` will set    the value of `n.`
    alias: 'name_'
  }
});

// Setting `name` will propagate to `n`.
const person = new Person({ name: 'Value' });
console.log(person); // { n: 'Value' }
console.log(person.toObject({ virtuals: true })); // { n: 'Value', name: 'Value' }
console.log(person.name); // "Value"

person.name = 'Not Val';
console.log(person); // { n: 'Not Value' }
 

 

Aliases can also be declared on nested paths. Nested schemas and subdocuments can be used easily. 

 

const childSchema = new Schema({
  n: {
    type: String,
    alias: 'name'
  }
}, { _id: false });

const parentSchema = new Schema({
  // If in a child schema, the alias doesn't need to include the full nested path.
  c: child schema,
  name: {
    f: {
      type: String,
      // The full nested path is declared inline, is ensured by Alias.
      alias: 'namePractice.first'
    }
  }
});

 

Options

The structure of the options looks like:

 

new Schema({..}, options);

// or
const schema = new Schema({..});
schema.set(option, value);

 

The valid options existing are:

  • autoIndex
  • autoCreate
  • bufferCommands
  • bufferTimeoutMS
  • capped
  • collection
  • discriminatorKey
  • id
  • _id
  • minimize
  • read
  • writeConcern
  • shardKey
  • strict
  • strictQuery
  • toJSON
  • toObject
  • typeKey
  • useNestedStrict
  • validateBeforeSave
  • versionKey
  • optimisticConcurrency
  • collation
  • selectPopulatedPaths
  • skipVersioning
  • timestamps

 

Option: autoIndex

By default, the mongoose init() function creates all indexes defined in the model’s schema. The automation in index creation is very helpful for development and test environments. But building indexes can sometimes create load on the production database. This problem can be solved by using autoIndex. By setting the autoIndex to false, the indexes can be managed carefully.

 

const schema = new Schema({..}, { autoIndex: false });
const Clock = mongoose.model('Clock', schema);
Clock.ensureIndexes(callback);

 

By default, this option is set to true. 

 

Option: autoCreate

Before Mongoose builds the indexesModel.createCollection() is called. This function creates the underlying function in MongoDB when autoCreate is true. On the other side, the createCollection function is responsible for setting the collection’s default collation. By changing the autoCreate to true, it is more helpful for the development and test environment.

The limitation with createCollection is that it cannot modify an existing collection. ForFor example,, if capped: 1024 is added to the schema; on the other side, if if the existing collection is not capped, then createCollection() will throw an error. During production, autoCreate should be false.

 

const schema = new Schema({..}, { autoCreate: true, capped: 1024 });
const Clock = mongoose.model('Clock', schema);
// Mongoose will create the capped collection for you.

 

The autoCreate function is set to false by default. This setting can be easily changed by: mongoose.set(‘autoCreate’, true).

 

Option: bufferCommands

If somehow the connection goes down, these commands come into play and continue unless a driver manages to reconnect. To overlook the buffering, the bufferCommands are set to false. 

 

const schema = new Schema({..}, { bufferCommands: false });

 

Here, the global bufferCommands option will be overridden by schema bufferCommands.

 

mongoose.set('buffer_Commands'true);
// The Schema option will override the above, if the option is set.
const schema = new Schema({..}, { bufferCommands: false });

 

Option: bufferTimeoutMS

If the bufferCommands is true, the Mongoose buffering will wait for a maximum time before throwing an error. By default, it takes 10000 milliseconds or (10 s). 

 

// Running an operation for more than 1 second, will throw an error.

 
const schema = new Schema({..}, { bufferTimeoutMS: 1100 });

 

Option: capped

MongoDB has its own capped schema collections. For getting the MongoDB collection to be capped, the capped option in the collection of bytes is set to the maximum in bytes. The structure for the same looks like this:

 

new Schema({..}, { capped: 1024 });

 

If you want to set additional options like max or autoIndexId, set the capped option to an object and explicitly pass the size option.

 

new Schema({..}, { capped: { size: 1026, max: 2000, autoIndexId: true } });

 

Option: collection

A collection name is produced by the Mongoose bypassing the model name to utils.toCollectionName method. This method converts the name to the plural form. This option is used to change the name of the collection.

 

const dataSchema = new Schema({..}, { collection: 'data' });

 

Option: discriminatorKey

When a discriminator is defined, a path to the schema is added by the Mongoose. That path is responsible for storing the discriminator key; any document is an instance of. By default, _t path is added but we can change it at our convenience.

 

const BaseSchema = new Schema_({}, { discriminatorKey: 'type' });
const BaseModel = mongoose.model('Practise', BaseSchema);

const PersonSchema = new Schema({ name: Str1 });
const PersonModel = BaseModel.discriminator('CodingNinja', personSchema);

const doc = new PersonModel({ name: 'code#Coding' });
doc.type; // 'Person'

 

Option: id

Each schema is assigned a virtual id getter by default which returns the document _id field(converted to string). If you don’t want the id getter to get added to the schema, then the following option should be passed at construction time.

 

 

// This shows the default behaviour.
const schema_ = new Schema_({ name: String_ });
const Page_ = mongoose.model_('Page', schema);
const p = new Page_({ name: 'codingNinja.org' });
console.log(p); // { _id: '50341373e894ad16347efe01' }

// For the disabled id.
const schema_ = new Schema({ name: String }, { id: false });
const Page = mongoose.model('Page_helper', schema);
const pi = new Page({ name: 'codingNinjas.org' });
console.log(pi.id); // For undefined

 

Option: _id

An _id is assigned to each of the mongoose schemas by default. This comes to play whenever one is not passed as an argument to the constructor. The assigned type is an ObjectId which coincides with MongoDB’s default behavior if you want to disable this default _if, it can be done in the following ways. 

 

// This shows the default behaviour.
const schema_ = new Schema_({ name: String_ });
const Page_ = mongoose.model_('Page', schema);
const p = new Page_({ name: 'codingNinja.org' });
console.log(p); // { _id: '50341373e894ad16347efe01', name: 'codingNinja.org' }

// This id is the disabled _id.
const childSchema_ = new Schema_({ name: String_ }, { _id: false });
const parentSchema_ = new Schema_({ children: [childSchema_] });

const Model_ = mongoose.model_('Model_practise', parentSchema);

Model.create({ children: [{ name: 'Coder' }] }, (error, doc) => {
  // doc.children[0]._id will be undefined.
});

 

But the limitation is that this feature can be implemented only on the subdocuments. Mongoose cannot same any kind of document without an id; even if you try to do so, it will throw an error.

 

Option: read

Using this option, we can set query#read options. It also provides a way to apply the default ReadPreferences to all the further queries derived from a model. Like in OOP concept, this feature also promotes alias concept. Instead of typing the whole word “secondaryPreferred” and getting a typing error, we can simply type “sp”

Tag sets can also be specified by the read option. 

 

Option: writeConcern

It helps to write concern at the schema level.

 

const schema = new Schema({ name: String }, {
  writeConcern: {
    w: 'majority',
    j: true,
    wtimeout: 1000
  }
});

 

Option: shardKey

This option is preferred more when we have sharded MongoDB architecture. Sharded collections are given a shard key which must be present for insert/ update operations. Just we need to reset the schema option to the same key and we will be good to go.

 

Option: strict

By default, the option is truly set. The values passed to the model constructor ensures that the values passed to the model are not specified in the schema and it doesn't get saved in the database.

 

ES6 classes

Another interesting method present in schemas is loadClass(), this method can be used for creating Mongoose schema from an ES6 class. The following points are ensured:

 

  • The class methods which are ES6 become Mongoose methods.
  • The class statics which are ES6 become Mongoose statics.
  • The ES6 getters and setters become Mongoose virtuals.

 

Below is the example of loadClass(), which can be used for creating a schema from ES6 class:

class MyClass {
  myMethod() { return 42; }
  static myStatic() { return 42; }
  get myVirtual() { return 42; }
}

const schema = new mongoose.Schema();
schema.loadClass(MyClass);

console.log(schema.methods); // { myMethod: [Function: myMethod] }
console.log(schema.statics); // { myStatic: [Function: myStatic] }
console.log(schema.virtuals); // { myVirtual: VirtualType { ... } }

 

Frequently asked questions:

  • Define a schema?

 

In Mongoose, the schema refers to the representation of a particular document, either partially or completely. In other ways, the way of expressing indexes, properties, values as well as constraints is known as schema. 

 

  • Which is better to use MongoDB or mongoose?

 

As such there is no benefit, both have its advantages based on different applications it is used for. For multi-tenant applications, MongoDB native driver is a good choice. Where as mongoose as an ORM(Object Relational Mapping) is a better option for newbies.

 

Live masterclass