Categories
MongoDB

Using MongoDB with Mongoose — Discriminators and Arrays

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.

Embedded Discriminators in Arrays

We can add embedded discriminators into arrays.

For example, we can write:

async function run() {
  const { createConnection, Types, Schema } = require('mongoose');
  const db = createConnection('mongodb://localhost:27017/test');
  const eventSchema = new Schema({ message: String },
    { discriminatorKey: 'kind', _id: false });

  const batchSchema = new Schema({ events: [eventSchema] });
  const docArray = batchSchema.path('events');
  const clickedSchema = new Schema({
    element: {
      type: String,
      required: true
    }
  }, { _id: false });
  const Clicked = docArray.discriminator('Clicked', clickedSchema);
  const Purchased = docArray.discriminator('Purchased', new Schema({
    product: {
      type: String,
      required: true
    }
  }, { _id: false }));

  const Batch = db.model('EventBatch', batchSchema);
  const batch = {
    events: [
      { kind: 'Clicked', element: '#foo', message: 'foo' },
      { kind: 'Purchased', product: 'toy', message: 'world' }
    ]
  };
  const doc = await Batch.create(batch);
  console.log(doc.events);
}
run();

We add the events array into the eventSchema .

Then we create the docArray by calling the batchSchema.path method so that we can create the discriminator with the docArray method.

Then we create the discriminators by calling docArray.discriminator method with the name of the model and the schema.

Next, we create the Batch model from the batchSchema so that we can populate the model.

We call Batch.create with the batch object that has an events array property to add the items.

The kind property has the type of object we want to add.

Then doc.events has the event entries, which are:

[
  { kind: 'Clicked', element: '#foo', message: 'foo' },
  { kind: 'Purchased', product: 'toy', message: 'world' }
]

Recursive Embedded Discriminators in Arrays

We can also add embedded discriminators recursively in arrays.

For example, we can write:

async function run() {
  const { createConnection, Types, Schema } = require('mongoose');
  const db = createConnection('mongodb://localhost:27017/test');
  const singleEventSchema = new Schema({ message: String },
    { discriminatorKey: 'kind', _id: false });

  const eventListSchema = new Schema({ events: [singleEventSchema] });

  const subEventSchema = new Schema({
    subEvents: [singleEventSchema]
  }, { _id: false });

  const SubEvent = subEventSchema.path('subEvents').
    discriminator('SubEvent', subEventSchema);
  eventListSchema.path('events').discriminator('SubEvent', subEventSchema);

  const Eventlist = db.model('EventList', eventListSchema);
  const list = {
    events: [
      { kind: 'SubEvent', subEvents: [{ kind: 'SubEvent', subEvents: [], message: 'test1' }], message: 'hello' },
      { kind: 'SubEvent', subEvents: [{ kind: 'SubEvent', subEvents: [{ kind: 'SubEvent', subEvents: [], message: 'test3' }], message: 'test2' }], message: 'world' }
    ]
  };

  const doc = await Eventlist.create(list)
  console.log(doc.events);
  console.log(doc.events[1].subEvents);
  console.log(doc.events[1].subEvents[0].subEvents);
}
run();

We create the singleEventSchema .

Then we use as the schema for the objects in the events array property.

Next, we create the subEventSchema the same way.

Then we create the SubEvent model calling the path and the discriminator methods.

This will create the schema for the subEvents property.

Then we link that to the events property with the:

eventListSchema.path('events').discriminator('SubEvent', subEventSchema);

call.

Now we have can subEvents properties in events array entrys that are recursive.

Next, we create the list object with the events array that has the subEvents property added recursively.

Then we call Eventlist.create to create the items.

The last 2 console logs should get the subevents from the 2nd event entry.

Conclusion

We can add discriminators to arrays and also add them recursively.

Categories
MongoDB

Using MongoDB with Mongoose - Nested Document Discriminators and Plugins

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.

Discriminators and Single Nested Documents

We can define discriminators on single nested documents.

For instance, we can write:

async function run() {
  const { createConnection, Types, Schema } = require('mongoose');
  const db = createConnection('mongodb://localhost:27017/test');
  const shapeSchema = Schema({ name: String }, { discriminatorKey: 'kind' });
  const schema = Schema({ shape: shapeSchema });

schema.path('shape').discriminator('Circle', Schema({ radius: String }));
  schema.path('shape').discriminator('Square', Schema({ side: Number }));

  const Model = db.model('Model', schema);
  const doc = new Model({ shape: { kind: 'Circle', radius: 5 } });
  console.log(doc)
}
run();

We call discriminator on the shape property with:

schema.path('shape').discriminator('Circle', Schema({ radius: String }));
schema.path('shape').discriminator('Square', Schema({ side: Number }));

We call the discriminator method to add the Circle and Square discriminators.

Then we use them by setting the kind property when we create the entry.

Plugins

We can add plugins to schemas.

Plugins are useful for adding reusable logic into multiple schemas.

For example, we can write:

const loadedAtPlugin = (schema, options) => {
  schema.virtual('loadedAt').
    get(function () { return this._loadedAt; }).
    set(function (v) { this._loadedAt = v; });

    schema.post(['find', 'findOne'], function (docs) {
    if (!Array.isArray(docs)) {
      docs = [docs];
    }
    const now = new Date();
    for (const doc of docs) {
      doc.loadedAt = now;
    }
  });
};

async function run() {
  const { createConnection, Types, Schema } = require('mongoose');
  const db = createConnection('mongodb://localhost:27017/test');
  const gameSchema = new Schema({ name: String });
  gameSchema.plugin(loadedAtPlugin);
  const Game = db.model('Game', gameSchema);

  const playerSchema = new Schema({ name: String });
  playerSchema.plugin(loadedAtPlugin);
  const Player = db.model('Player', playerSchema);

  const player = new Player({ name: 'foo' });
  const game = new Game({ name: 'bar' });
  await player.save()
  await game.save()
  const p = await Player.findOne({});
  const g = await Game.findOne({});
  console.log(p.loadedAt);
  console.log(g.loadedAt);
}
run();

We created th loadedAtPlugin to add the virtual loadedAt property to the retrieved objects after we call find or findOne .

We call schema.post in the plugin to listen to the find and findOne events.

Then we loop through all the documents and set the loadedAt property and set that to the current date and time.

In the run function, we add the plugin by calling the plugin method on each schema.

This has to be called before we define the model .

Otherwise, the plugin won’t be added.

Now we should see the timestamp when we access the loadedAt property after calling findOne .

Conclusion

We can add the discriminators to singly nested documents.

Also, we can add plugins to add reusable logic into our models.

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.