Categories
MongoDB

Using MongoDB with Mongoose — Dynamic References

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.

Dynamic References via refPath

We can join more than one model with dynamic references and the refPath property.

For example, we can write:

async function run() {
  const { createConnection, Types, Schema } = require('mongoose');
  const db = createConnection('mongodb://localhost:27017/test');
  const commentSchema = new Schema({
    body: { type: String, required: true },
    subject: {
      type: Schema.Types.ObjectId,
      required: true,
      refPath: 'subjectModel'
    },
    subjectModel: {
      type: String,
      required: true,
      enum: ['BlogPost', 'Product']
    }
  });
  const Product = db.model('Product', new Schema({ name: String }));
  const BlogPost = db.model('BlogPost', new Schema({ title: String }));
  const Comment = db.model('Comment', commentSchema);
  const book = await Product.create({ name: 'Mongoose for Dummies' });
  const post = await BlogPost.create({ title: 'MongoDB for Dummies' });
  const commentOnBook = await Comment.create({
    body: 'Great read',
    subject: book._id,
    subjectModel: 'Product'
  });
  await commentOnBook.save();
  const commentOnPost = await Comment.create({
    body: 'Very informative',
    subject: post._id,
    subjectModel: 'BlogPost'
  });
  await commentOnPost.save();
  const comments = await Comment.find().populate('subject').sort({ body: 1 });
  console.log(comments)
}
run();

We have the commentSchema that has the subject and subjectModel properties.

The subject is set to an object ID. We have the refPath property that references the model that it can reference.

The refPath is set to the subjectModel , and the subjectModel references the BlogPost and Product models.

So we can link comments to a Product entry or a Post entry.

To do the linking to the model we want, we set the subject and subjectModel when we create the entry with the create method.

Then we call populate with subject to get the subject field’s data.

Equivalently, we can put the related items into the root schema.

For example, we can write:

async function run() {
  const { createConnection, Types, Schema } = require('mongoose');
  const db = createConnection('mongodb://localhost:27017/test');
  const commentSchema = new Schema({
    body: { type: String, required: true },
    product: {
      type: Schema.Types.ObjectId,
      required: true,
      ref: 'Product'
    },
    blogPost: {
      type: Schema.Types.ObjectId,
      required: true,
      ref: 'BlogPost'
    }
  });
  const Product = db.model('Product', new Schema({ name: String }));
  const BlogPost = db.model('BlogPost', new Schema({ title: String }));
  const Comment = db.model('Comment', commentSchema);
  const book = await Product.create({ name: 'Mongoose for Dummies' });
  const post = await BlogPost.create({ title: 'MongoDB for Dummies' });
  const commentOnBook = await Comment.create({
    body: 'Great read',
    product: book._id,
    blogPost: post._id,
  });
  await commentOnBook.save();
  const comments = await Comment.find()
    .populate('product')
    .populate('blogPost')
    .sort({ body: 1 });
  console.log(comments)
}
run();

Then we set the product and blogPost in the same object.

We rearranged the commentSchema to have the products and blogPost references.

Then we call populate on both fields so that we can get the comments.

Conclusion

We can reference more than one schema within a schema.

Then we can call populate to get all the fields.

Categories
MongoDB

Using MongoDB with Mongoose — Query Limits and Child Refs

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.

limit vs. perDocumentLimit

Populate has a limit option, but it doesn’t support limit on a per-document basis.

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',
      options: { limit: 2 }
    })
    .exec();
  console.log(story.fans)
}
run();

to query up to numDocuments * limit .

Mongoose 5.9.0 or later supports the perDocumentLimit property to add a per-document limit.

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',
      perDocumentLimit: 2
    })
    .exec();
  console.log(story.fans)
}
run();

Refs to Children

If we call push to items to children, then we can get the refs to the child items.

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,
  });
  story1.fans.push(fan);
  await story1.save();
  const story = await Story.findOne({ title: 'Mongoose Story' })
    .populate('fans')
    .exec();
  console.log(story.fans)
}
run();

We call push on story.fans to add an entry to the fans array field.

Now when we query the story, we get the fans array with the fan in it.

Conclusion

We can limit how many documents are returned and add entries to array fields so that we can access the refs to child entries.

Categories
MongoDB

Using MongoDB with Mongoose — Populating Existing Documents and Across Databases

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.

Populating an Existing Document

We can call populate on an existing document.

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,
  });
  story1.fans.push(fan);
  await story1.save();
  const story = await Story.findOne({ title: 'Mongoose Story' })
  await story.populate('fans').execPopulate();
  console.log(story.populated('fans'));
  console.log(story.fans[0].name);
}
run();

We created the story with the fans and author field populated.

Then we get the entry with the Story.findOne method.

Then we call populate in the resolved story object and then call execPopulate to do the join.

Now the console log should see the fans entries displayed.

Populating Multiple Existing Documents

We can populate across multiple levels.

For example, we can write:

async function run() {
  const { createConnection, Types, Schema } = require('mongoose');
  const connection = createConnection('mongodb://localhost:27017/test');
  const userSchema = new Schema({
    name: String,
    friends: [{ type: Schema.Types.ObjectId, ref: 'User' }]
  });
  const User = connection.model('User', userSchema);
  const user1 = new User({
    _id: new Types.ObjectId(),
    name: 'Friend',
  });
  await user1.save();
  const user = new User({
    _id: new Types.ObjectId(),
    name: 'Val',
  });
  user.friends.push(user1);
  await user.save();
  const userResult = await User.
    findOne({ name: 'Val' }).
    populate({
      path: 'friends',
      populate: { path: 'friends' }
    });
  console.log(userResult);
}
run();

We have a User schema that is self-referencing.

The friends property references the User schema itself.

Then when we query the User , we call populate with the path to query.

We can query across multiple levels with the populate property.

Cross-Database Populate

We can populate across databases.

For example, we can write:

async function run() {
  const { createConnection, Types, Schema } = require('mongoose');
  const db1 = createConnection('mongodb://localhost:27017/db1');
  const db2 = createConnection('mongodb://localhost:27017/db2');

  const conversationSchema = new Schema({ numMessages: Number });
  const Conversation = db2.model('Conversation', conversationSchema);

  const eventSchema = new Schema({
    name: String,
    conversation: {
      type: Schema.Types.ObjectId,
      ref: Conversation
    }
  });
  const Event = db1.model('Event', eventSchema);
  const conversation = new Conversation({ numMessages: 2 });
  conversation.save();
  const event = new Event({
    name: 'event',
    conversation
  })
  event.save();
  const events = await Event
    .findOne({ name: 'event' })
    .populate('conversation');
  console.log(events);
}
run();

We create 2 models with the Coversation and Event models that are linked to different database connections.

We can create the Conversation and Event entries and link them together.

And then we can call findOne on Event to get the linked data.

Conclusion

We can populate existing documents and populate across databases with Mongoose.

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.