Categories
MongoDB

Using MongoDB with Mongoose — Populate

To make MongoDB database manipulation easy, we can use the Mongoose NPM package to make working with MongoDB databases easier.

In this article, we’ll look at how to use Mongoose to manipulate our MongoDB database.

Populate

We can use the populate method to join 2 models together.

For example, we can write:

async function run() {
  const { createConnection, Types, Schema } = require('mongoose');
  const connection = createConnection('mongodb://localhost:27017/test');
  const personSchema = Schema({
    _id: Schema.Types.ObjectId,
    name: String,
    age: Number,
    stories: [{ type: Schema.Types.ObjectId, ref: 'Story' }]
  });

  const storySchema = Schema({
    author: { type: Schema.Types.ObjectId, ref: 'Person' },
    title: String,
    fans: [{ type: Schema.Types.ObjectId, ref: 'Person' }]
  });

  const Story = connection.model('Story', storySchema);
  const Person = connection.model('Person', personSchema);
  const author = new Person({
    _id: new Types.ObjectId(),
    name: 'James Smith',
    age: 50
  });

  author.save(function (err) {
    if (err) return handleError(err);

    const story1 = new Story({
      title: 'Mongoose Story',
      author: author._id
    });

    story1.save(function (err) {
      if (err) {
        return console.log(err);
      }

      Story.
        findOne({ title: 'Mongoose Story' }).
        populate('author').
        exec(function (err, story) {
          if (err) {
            return console.log(err);
          }
          console.log('author', story.author.name);
        });
    });
  });
}
run();

We created the Story and Person models.

The Story model references the Person model by setting author._id as the value of the author field.

Then we save both the author and story.

Then in the callback for story1.save , we get the Story entry with the exec method with a callback to get the data.

Then we can access the author’s name of the story with the story.author.name property.

The documents created with the models will be saved in their own collections.

Therefore the story.author.name ‘s value is 'James Smith' .

Setting Populated Fields

We can also set the author value directly by writing:

async function run() {
  const { createConnection, Types, Schema } = require('mongoose');
  const connection = createConnection('mongodb://localhost:27017/test');
  const personSchema = Schema({
    _id: Schema.Types.ObjectId,
    name: String,
    age: Number,
    stories: [{ type: Schema.Types.ObjectId, ref: 'Story' }]
  });

  const storySchema = Schema({
    author: { type: Schema.Types.ObjectId, ref: 'Person' },
    title: String,
    fans: [{ type: Schema.Types.ObjectId, ref: 'Person' }]
  });

  const Story = connection.model('Story', storySchema);
  const Person = connection.model('Person', personSchema);
  const author = new Person({
    _id: new Types.ObjectId(),
    name: 'James Smith',
    age: 50
  });

  author.save(function (err) {
    if (err) return handleError(err);

    const story1 = new Story({
      title: 'Mongoose Story',
    });

    story1.save(function (err) {
      if (err) {
        return console.log(err);
      }

      Story.findOne({ title: 'Mongoose Story' }, function (error, story) {
        if (error) {
          return console.log(error);
        }
        story.author = author;
        console.log(story.author.name);
      });
    });
  });
}
run();

We set story.author to author to link the author to the story .

And now story.author.name is 'James Smith' .

Checking Whether a Field is Populated

We can check whether a field is populated with the populated method.

For example, we can write:

async function run() {
  const { createConnection, Types, Schema } = require('mongoose');
  const connection = createConnection('mongodb://localhost:27017/test');
  const personSchema = Schema({
    _id: Schema.Types.ObjectId,
    name: String,
    age: Number,
    stories: [{ type: Schema.Types.ObjectId, ref: 'Story' }]
  });

  const storySchema = Schema({
    author: { type: Schema.Types.ObjectId, ref: 'Person' },
    title: String,
    fans: [{ type: Schema.Types.ObjectId, ref: 'Person' }]
  });

  const Story = connection.model('Story', storySchema);
  const Person = connection.model('Person', personSchema);
  const author = new Person({
    _id: new Types.ObjectId(),
    name: 'James Smith',
    age: 50
  });

  author.save(function (err) {
    if (err) return handleError(err);

    const story1 = new Story({
      title: 'Mongoose Story',
    });

    story1.save(function (err) {
      if (err) {
        return console.log(err);
      }

      Story.findOne({ title: 'Mongoose Story' }, function (error, story) {
        if (error) {
          return console.log(error);
        }
        story.author = author;
        console.log(story.populated('author'));
      });
    });
  });
}
run();

We get the ID of the author that the store is populated with.

We call depopulate to unlink the author and the story .

For example, we can write:

async function run() {
  const { createConnection, Types, Schema } = require('mongoose');
  const connection = createConnection('mongodb://localhost:27017/test');
  const personSchema = Schema({
    _id: Schema.Types.ObjectId,
    name: String,
    age: Number,
    stories: [{ type: Schema.Types.ObjectId, ref: 'Story' }]
  });

  const storySchema = Schema({
    author: { type: Schema.Types.ObjectId, ref: 'Person' },
    title: String,
    fans: [{ type: Schema.Types.ObjectId, ref: 'Person' }]
  });

  const Story = connection.model('Story', storySchema);
  const Person = connection.model('Person', personSchema);
  const author = new Person({
    _id: new Types.ObjectId(),
    name: 'James Smith',
    age: 50
  });

  author.save(function (err) {
    if (err) return handleError(err);

    const story1 = new Story({
      title: 'Mongoose Story',
    });

    story1.save(function (err) {
      if (err) {
        return console.log(err);
      }

      Story.findOne({ title: 'Mongoose Story' }, function (error, story) {
        if (error) {
          return console.log(error);
        }
        story.author = author;
        console.log(story.populated('author'));
        story.depopulate('author');
        console.log(story.populated('author'));
      });
    });
  });
}
run();

We call the depopulate method to unlink the author and story.

Now when we log the value of populated again, we see undefined .

Conclusion

We can join 2 documents together by referencing one document’s ID with the other.

Then we can use Mongoose’s method to check whether they’re linked.

Categories
MongoDB

Using MongoDB with Mongoose — Joins with Various Options

To make MongoDB database manipulation easy, we can use the Mongoose NPM package to make working with MongoDB databases easier.

In this article, we’ll look at how to use Mongoose to manipulate our MongoDB database.

Removing Foreign Documents

If we remove foreign documents, then when we try to reference a linked foreign document, it’ll return null .

For instance, if we have:

async function run() {
  const { createConnection, Types, Schema } = require('mongoose');
  const connection = createConnection('mongodb://localhost:27017/test');
  const personSchema = Schema({
    _id: Schema.Types.ObjectId,
    name: String,
    age: Number,
    stories: [{ type: Schema.Types.ObjectId, ref: 'Story' }]
  });
  const storySchema = Schema({
    author: { type: Schema.Types.ObjectId, ref: 'Person' },
    title: String,
    fans: [{ type: Schema.Types.ObjectId, ref: 'Person' }]
  });
  const Story = connection.model('Story', storySchema);
  const Person = connection.model('Person', personSchema);
  const author = new Person({
    _id: new Types.ObjectId(),
    name: 'James Smith',
    age: 50
  });
  await author.save();
  const story1 = new Story({
    title: 'Mongoose Story',
    author: author._id
  });
  await story1.save();
  await Person.deleteMany({ name: 'James Smith' });
  const story = await Story.findOne({ title: 'Mongoose Story' }).populate('author');
  console.log(story)
}
run();

We created a Story document that is linked to an Author .

Then we deleted the Person with the name 'James Smith' .

Now when we retrieve the latest value of the story with the author with populate , we’ll see that story is null .

Field Selection

We can select a few fields instead of selecting all the fields when we call populate .

For example, we can write:

async function run() {
  const { createConnection, Types, Schema } = require('mongoose');
  const connection = createConnection('mongodb://localhost:27017/test');
  const personSchema = Schema({
    _id: Schema.Types.ObjectId,
    name: String,
    age: Number,
    stories: [{ type: Schema.Types.ObjectId, ref: 'Story' }]
  });
  const storySchema = Schema({
    author: { type: Schema.Types.ObjectId, ref: 'Person' },
    title: String,
    fans: [{ type: Schema.Types.ObjectId, ref: 'Person' }]
  });
  const Story = connection.model('Story', storySchema);
  const Person = connection.model('Person', personSchema);
  const author = new Person({
    _id: new Types.ObjectId(),
    name: 'James Smith',
    age: 50
  });
  await author.save();
  const story1 = new Story({
    title: 'Mongoose Story',
    author: author._id
  });
  await story1.save();
  const story = await Story.findOne({ title: 'Mongoose Story' })
    .populate('author', 'name')
    .exec();
  console.log(story)
}
run();

We save the author and story1 documents into our database.

Then we call findOne to get the story document with the author and the name properties.

Populating Multiple Paths

If we call populate multiple times, then only the last one will take effect.

Query Conditions and Other Options

The populate method can accept query conditions.

For example, we can write:

async function run() {
  const { createConnection, Types, Schema } = require('mongoose');
  const connection = createConnection('mongodb://localhost:27017/test');
  const personSchema = Schema({
    _id: Schema.Types.ObjectId,
    name: String,
    age: Number,
    stories: [{ type: Schema.Types.ObjectId, ref: 'Story' }]
  });
  const storySchema = Schema({
    author: { type: Schema.Types.ObjectId, ref: 'Person' },
    title: String,
    fans: [{ type: Schema.Types.ObjectId, ref: 'Person' }]
  });
  const Story = connection.model('Story', storySchema);
  const Person = connection.model('Person', personSchema);
  const author = new Person({
    _id: new Types.ObjectId(),
    name: 'James Smith',
    age: 50
  });
  await author.save();
  const fan = new Person({
    _id: new Types.ObjectId(),
    name: 'Fan Smith',
    age: 50
  });
  await fan.save();
  const story1 = new Story({
    title: 'Mongoose Story',
    author: author._id,
    fans: [fan._id]
  });
  await story1.save();
  const story = await Story.findOne({ title: 'Mongoose Story' })
    .populate({
      path: 'fans',
      match: { age: { $gte: 21 } },
      select: 'name -_id'
    })
    .exec();
  console.log(story.fans)
}
run();

We add a Person entry to the fans array.

Then when we call populate with an object that finds all the fans with age greater than or equal to 21, we should get our fan entry.

The select property has a string that lets us select the name field but not the _id as indicated by the - sign before the _id .

So the console log should show [{“name”:”Fan Smith”}] .

Conclusion

We can join documents with various options with Mongoose.

Categories
MongoDB

Using MongoDB with Mongoose — Hooks for Operations

To make MongoDB database manipulation easy, we can use the Mongoose NPM package to make working with MongoDB databases easier.

In this article, we’ll look at how to use Mongoose to manipulate our MongoDB database.

Save/Validate Hooks

The save method will trigger validate hooks.

This is because Mongoose calls the pre('save') hook that calls validate .

The pre('validate') and post('validate') hooks are called before any pre('save') hooks.

For example, we can write:

async function run() {  
  const { createConnection, Schema } = require('mongoose');  
  const connection = createConnection('mongodb://localhost:27017/test');  
  const schema = new Schema({ name: String });  
  schema.pre('validate', () => {  
    console.log('1');  
  });  
  schema.post('validate', () => {  
    console.log('2');  
  });  
  schema.pre('save', () => {  
    console.log('3');  
  });  
  schema.post('save', () => {  
    console.log('4');  
  });  
  const User = connection.model('User', schema);  
  new User({ name: 'test' }).save();  
}  
run();

to add the schema hooks.

Then they’ll be called one by one in the same order that they’re listed.

Query Middleware

Pre and post save hooks aren’t run when update methods are run.

For example, if we have:

async function run() {  
  const { createConnection, Schema } = require('mongoose');  
  const connection = createConnection('mongodb://localhost:27017/test');  
  const schema = new Schema({ name: String });  
  schema.pre('updateOne', { document: true, query: false }, function() {  
    console.log('Updating');  
  });  
  const User = connection.model('User', schema);  
  const doc = new User();  
  await doc.updateOne({ $set: { name: 'test' } });  
  await User.updateOne({}, { $set: { name: 'test' } });  
}  
run();

Then when we have query set to false or didn’t add the query property, then the updateOne pre hook won’t run when we run updateOne .

Aggregation Hooks

We can add aggregation hooks.

For example, we can write:

async function run() {  
  const { createConnection, Schema } = require('mongoose');  
  const connection = createConnection('mongodb://localhost:27017/test');  
  const schema = new Schema({  
    name: {  
      type: String,  
      unique: true  
    }  
  });  
  schema.pre('aggregate', function() {  
    this.pipeline().unshift({ $match: { isDeleted: { $ne: true } } });  
  });  
  const User = connection.model('User', schema);  
}  
run();

We listen to the aggregate event.

Error Hooks

We can get errors from hooks.

For example, we can write:

async function run() {  
  const { createConnection, Schema } = require('mongoose');  
  const connection = createConnection('mongodb://localhost:27017/test');  
  const schema = new Schema({  
    name: {  
      type: String,  
      unique: true  
    }  
  });  
  schema.post('update', function (error, res, next) {  
    if (error.name === 'MongoError' && error.code === 11000) {  
      next(new Error('There was a duplicate key error'));  
    } else {  
      next();  
    }  
  });  
  const User = connection.model('User', schema);  
}  
run();

We listen to the update event and get the error from the error parameter.

We can get the name and code to get information about the error.

Synchronous Hooks

Some hooks are always synchronous.

init hooks are always synchronous because the init function is synchronous.

For example, if we have:

async function run() {  
  const { createConnection, Schema } = require('mongoose');  
  const connection = createConnection('mongodb://localhost:27017/test');  
  const schema = new Schema({  
    name: String  
  });  
  schema.pre('init', obj => {  
    console.log(obj);  
  });  
  const User = connection.model('User', schema);  
}  
run();

We added the pre init hook with a callback.

Conclusion

We can add middleware for various events that are emitted when we manipulate data with Mongoose.

Categories
MongoDB

Using MongoDB with Mongoose — Validators on Nested Objects and Updates

To make MongoDB database manipulation easy, we can use the Mongoose NPM package to make working with MongoDB databases easier.

In this article, we’ll look at how to use Mongoose to manipulate our MongoDB database.

Required Validators on Nested Objects

We can define validators on nested objects with Mongoose.

To do that, we can write:

async function run() {
  const { createConnection, Schema } = require('mongoose');
  const connection = createConnection('mongodb://localhost:27017/test');
  const nameSchema = new Schema({
    first: String,
    last: String
  });

  const personSchema = new Schema({
    name: {
      type: nameSchema,
      required: true
    }
  });

  const Person = connection.model('Person', personSchema);
  const doc = new Person({});
  const err = doc.validateSync();
  console.log(err);
}
run();

We created the nameSchema which is embedded in the personSchema so that we can require both the name.first and name.last nested fields.

Now when we create a new Person instance, we’ll see an error because we haven’t added those properties into our document.

Update Validators

Mongoose also supports validation for updating documents with the update , updateOne , updateMany , and findOneAndUpdate methods.

Update validators are off by default. We need the runValidators option to turn it on.

For example, we can write:

async function run() {
  const { createConnection, Schema } = require('mongoose');
  const connection = createConnection('mongodb://localhost:27017/test');
  const toySchema = new Schema({
    color: String,
    name: String
  });
  const Toy = connection.model('Toys', toySchema);
  Toy.schema.path('color').validate(function (value) {
    return /red|green|blue/i.test(value);
  }, 'Invalid color');

  const opts = { runValidators: true };
  Toy.updateOne({}, { color: 'not a color' }, opts, (err) => {
    console.log(err.errors.color.message);
  });
}
run();

Since we have the runValidators property set to true in the opts object, we’ll get validator when we call the updateOne method.

Then we should see the ‘Invalid color’ message logged in the console log in the callback.

Update Validators and this

The value of this for update validators and document validators.

In document validators, this refers to the document itself.

However, when we’re updating a document, the document that’s updated may not be in memory itself.

Therefore, this is not defined by default.

The context optioin lets us set the value of this in update validators.

For example, we can write:

const { captureRejectionSymbol } = require('events');

async function run() {
  const { createConnection, Schema } = require('mongoose');
  const connection = createConnection('mongodb://localhost:27017/test');
  const toySchema = new Schema({
    color: String,
    name: String
  });
  toySchema.path('color').validate(function (value) {
    if (this.getUpdate().$set.name.toLowerCase().indexOf('red') !== -1) {
      return value === 'red';
    }
    return true;
  });

  const Toy = connection.model('Toy', toySchema);
  const update = { color: 'blue', name: 'red car' };
  const opts = { runValidators: true, context: 'query' };
  Toy.updateOne({}, update, opts, (error) => {
    console.log(error.errors['color']);
  });
}
run();

to set the context property in the opts object to 'query' to make this defined in the validator when we do updates.

Conclusion

We can add validators to updates operations in Mongoose. It’s not enabled by default.

Categories
MongoDB

Using MongoDB with Mongoose — Pre Middleware Errors and Post Middlewares

To make MongoDB database manipulation easy, we can use the Mongoose NPM package to make working with MongoDB databases easier.

In this article, we’ll look at how to use Mongoose to manipulate our MongoDB database.

Errors in Pre Hooks

We can raise errors in pre hooks in various ways.

For example, we can write:

async function run() {
  const { createConnection, Schema } = require('mongoose');
  const connection = createConnection('mongodb://localhost:27017/test');
  const schema  = new Schema({
    name: { type: String, required: true },
    age: Number
  });
  schema.pre('save', (next) => {
    const err = new Error('error');
    next(err);
  });
  const Kitten = connection.model('Kitten', kittenSchema);
}
run();

to pass an Error instance in the next method.

Also, we can return a promise:

async function run() {
  const { createConnection, Schema } = require('mongoose');
  const connection = createConnection('mongodb://localhost:27017/test');
  const kittenSchema = new Schema({
    name: { type: String, required: true },
    age: Number
  });
  schema.pre('save', (next) => {
    const err = new Error('error');
    return Promise.reject(err);
  });
  const Kitten = connection.model('Kitten', kittenSchema);
}
run();

Or we can throw an error:

async function run() {
  const { createConnection, Schema } = require('mongoose');
  const connection = createConnection('mongodb://localhost:27017/test');
  const schema = new Schema({
    name: { type: String, required: true },
    age: Number
  });
  schema.pre('save', (next) => {
    throw new Error('error');
  });
  const Kitten = connection.model('Kitten', kittenSchema);
}
run();

Post Middleware

Post middlewares are run after the hooked method and all its pre middleware are run.

For example, we can write:

async function run() {
  const { createConnection, Schema } = require('mongoose');
  const connection = createConnection('mongodb://localhost:27017/test');
  const schema = new Schema({
    name: { type: String, required: true },
    age: Number
  });
  schema.post('init', (doc) => {
    console.log('%s has been initialized from the db', doc._id);
  });
  schema.post('validate', (doc) => {
    console.log('%s has been validated (but not saved yet)', doc._id);
  });
  schema.post('save', (doc) => {
    console.log('%s has been saved', doc._id);
  });
  schema.post('remove', (doc) => {
    console.log('%s has been removed', doc._id);
  });
  const Kitten = connection.model('Kitten', kittenSchema);
}
run();

We added post middlewares for the init , validate , save , and remove operations that are run after the given document operations.

Async Post Hooks

We can add async post hooks. We just need to call next to proceed to the next post hook:

async function run() {
  const { createConnection, Schema } = require('mongoose');
  const connection = createConnection('mongodb://localhost:27017/test');
  const schema = new Schema({
    name: { type: String, required: true },
    age: Number
  });
  schema.post('save', (doc, next) => {
    setTimeout(() => {
      console.log('post1');
      next();
    }, 10);
  });

  schema.post('save', (doc, next) => {
    console.log('post2');
    next();
  });
  const Kitten = connection.model('Kitten', schema);
}
run();

We call the next function in the setTimeout callback to proceed to the next post middleware.

Define Middleware Before Compiling Models

We have to define middleware before we create the model with the schema for it to fire.

For example, we can write:

async function run() {
  const { createConnection, Schema } = require('mongoose');
  const connection = createConnection('mongodb://localhost:27017/test');
  const schema = new Schema({ name: String });
  schema.pre('save', () => console.log('pre save called'));
  const User = connection.model('User', schema);
  new User({ name: 'test' }).save();
}
run();

We created the schema and then added a save pre middleware right after we defined the schema and before the model is created.

This way, the callback in the pre method will be run when we create a document with the model.

Conclusion

We can add post middlewares and be careful where we define the model.