Categories
MongoDB

Using MongoDB with Mongoose — Populate Virtuals

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 Virtuals

We can control how 2 models are joined together.

For example, we can write:

async function run() {
  const { createConnection, Types, Schema } = require('mongoose');
  const db = createConnection('mongodb://localhost:27017/test');
  const PersonSchema = new Schema({
    name: String,
    band: String
  });

  const BandSchema = new Schema({
    name: String
  });

  BandSchema.virtual('members', {
    ref: 'Person',
    localField: 'name',
    foreignField: 'band',
    justOne: false,
    options: { sort: { name: -1 }, limit: 5 }
  });

  const Person = db.model('Person', PersonSchema);
  const Band = db.model('Band', BandSchema);
  const person = new Person({ name: 'james', band: 'superband' });
  await person.save();
  const band = new Band({ name: 'superband' });
  await band.save();
  const bands = await Band.find({}).populate('members').exec();
  console.log(bands[0].members);
}
run();

We create the PersonSchema as usual, but the BandSchema is different.

We call the virtual method with the join field call members to get the persons with the band name set to a given name.

The ref property is the name of the model we want to join.

localField is the field of the BandSchema that we want to join with PersonSchema .

The foreignField is the field of PersonSchema that we want to join with the BandSchema .

justOne means we only return the first entry of the join.

options has the options for querying.

Virtuals aren’t included in the toJSON() output by default.

If we want populate virtual to show when using functions that rely on JSON.stringify() , then add the virtuals option and set it to true .

For example, we can write:

async function run() {
  const { createConnection, Types, Schema } = require('mongoose');
  const db = createConnection('mongodb://localhost:27017/test');
  const PersonSchema = new Schema({
    name: String,
    band: String
  });

  const BandSchema = new Schema({
    name: String
  }, { toJSON: { virtuals: true } });

  BandSchema.virtual('members', {
    ref: 'Person',
    localField: 'name',
    foreignField: 'band',
    justOne: false,
    options: { sort: { name: -1 }, limit: 5 }
  });

  const Person = db.model('Person', PersonSchema);
  const Band = db.model('Band', BandSchema);
  const person = new Person({ name: 'james', band: 'superband' });
  await person.save();
  const band = new Band({ name: 'superband' });
  await band.save();
  const bands = await Band.find({}).populate('members').exec();
  console.log(bands[0].members);
}
run();

to add the virtuals option to the BandSchema .

If we use populate projections, then foreignField should be included in the projection:

async function run() {
  const { createConnection, Types, Schema } = require('mongoose');
  const db = createConnection('mongodb://localhost:27017/test');
  const PersonSchema = new Schema({
    name: String,
    band: String
  });

  const BandSchema = new Schema({
    name: String
  }, { toJSON: { virtuals: true } });

  BandSchema.virtual('members', {
    ref: 'Person',
    localField: 'name',
    foreignField: 'band',
    justOne: false,
    options: { sort: { name: -1 }, limit: 5 }
  });

  const Person = db.model('Person', PersonSchema);
  const Band = db.model('Band', BandSchema);
  const person = new Person({ name: 'james', band: 'superband' });
  await person.save();
  const band = new Band({ name: 'superband' });
  await band.save();
  const bands = await Band.find({}).populate({ path: 'members', select: 'name band' }).exec();
  console.log(bands[0].members);
}
run();

We call populate with an object with the path to get the virtual field and the select property has a string with the field names that we want to get separated by a space.

Conclusion

We can use Mongoose populate virtual feature to join 2 models by a column other than an ID column.

Categories
MongoDB

Using MongoDB with Mongoose — Discriminators

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

Discriminators are a schema inheritance mechanism.

They let us enable multiple models to have overlapping schemas on top of the same MongoDB collection.

For example, we can use them as follows:

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

  const eventSchema = new Schema({ time: Date }, options);
  const Event = db.model('Event', eventSchema);
  const ClickedLinkEvent = Event.discriminator('ClickedLink',
    new Schema({ url: String }, options));
  const genericEvent = new Event({ time: Date.now(), url: 'mongodb.com' });
  console.log(genericEvent)

  const clickedEvent =
    new ClickedLinkEvent({ time: Date.now(), url: 'mongodb.com' });
  console.log(clickedEvent)
}
run();

We created an Event model from the eventSchema .

It has the discriminatorKey so that we get can discriminate between the 2 documents we create later.

To create the ClickedLinkEvent model, we call Event.discriminator to create a model by inheriting from the Event schema.

We add the url field to the ClickedLinkEvent model.

Then when we add the url to the Event document and the ClickedLinkEvent document, only the clickedEvent object has the url property.

We get:

{ _id: 5f6f78f17f83ca22408eb627, time: 2020-09-26T17:22:57.690Z }

as the value of genericEvent and:

{
  _id: 5f6f78f17f83ca22408eb628,
  kind: 'ClickedLink',
  time: 2020-09-26T17:22:57.697Z,
  url: 'mongodb.com'
}

as the value of clickedEvent .

Discriminators Save to the Model’s Collection

We can save different kinds of events all at once.

For example, we can write:

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

  const eventSchema = new Schema({ time: Date }, options);
  const Event = db.model('Event', eventSchema);

  const ClickedLinkEvent = Event.discriminator('ClickedLink',
    new Schema({ url: String }, options));

  const SignedUpEvent = Event.discriminator('SignedUp',
    new Schema({ user: String }, options));

  const event1 = new Event({ time: Date.now() });
  const event2 = new ClickedLinkEvent({ time: Date.now(), url: 'mongodb.com' });
  const event3 = new SignedUpEvent({ time: Date.now(), user: 'mongodbuser' });
  await Promise.all([event1.save(), event2.save(), event3.save()]);
  const count = await Event.countDocuments();
  console.log(count);
}
run();

We created the eventSchema as an ordinary schema.

And the rest of the models are created from the Event.discriminator method.

Then we created the models and saved them all.

And finally, we called Event.countDocuments to get the number of items saved under the Event model.

Then count should be 3 since ClickedLinkEvent and SignedUpEvent both inherit from Event itself.

Discriminator Keys

We can tell the difference between each type of models with the __t property by default.

For instance, we can write:

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

  const eventSchema = new Schema({ time: Date });
  const Event = db.model('Event', eventSchema);

  const ClickedLinkEvent = Event.discriminator('ClickedLink',
    new Schema({ url: String }));

  const SignedUpEvent = Event.discriminator('SignedUp',
    new Schema({ user: String }));

  const event1 = new Event({ time: Date.now() });
  const event2 = new ClickedLinkEvent({ time: Date.now(), url: 'mongodb.com' });
  const event3 = new SignedUpEvent({ time: Date.now(), user: 'mongodbuser' });
  await Promise.all([event1.save(), event2.save(), event3.save()]);
  console.log(event1.__t);
  console.log(event2.__t);
  console.log(event3.__t);
}
run();

to get the type of data that’s saved from the console logs. We should get:

undefined
ClickedLink
SignedUp

logged.

We can add the discriminatorKey in the options to change the discriminator key.

So we can write:

async function run() {
  const { createConnection, Types, Schema } = require('mongoose');
  const db = createConnection('mongodb://localhost:27017/test');
  const options = { discriminatorKey: 'kind' };
  const eventSchema = new Schema({ time: Date }, options);
  const Event = db.model('Event', eventSchema);

const ClickedLinkEvent = Event.discriminator('ClickedLink',
    new Schema({ url: String }, options));

const SignedUpEvent = Event.discriminator('SignedUp',
    new Schema({ user: String }, options));

const event1 = new Event({ time: Date.now() });
  const event2 = new ClickedLinkEvent({ time: Date.now(), url: 'mongodb.com' });
  const event3 = new SignedUpEvent({ time: Date.now(), user: 'mongodbuser' });
  await Promise.all([event1.save(), event2.save(), event3.save()]);
  console.log(event1.kind);
  console.log(event2.kind);
  console.log(event3.kind);
}
run();

to set the options for each model and then access the kind property instead of __t and get the same result as before.

Conclusion

We can use discriminators to create new models by inheriting one model from another.

Categories
MongoDB

Using MongoDB with Mongoose — Discriminators and Hooks

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 Queries

Queries are smart enough to take into account discriminators.

For example, if we have:

async function run() {
  const { createConnection, Types, Schema } = require('mongoose');
  const db = createConnection('mongodb://localhost:27017/test');
  const options = { discriminatorKey: 'kind' };
  const eventSchema = new Schema({ time: Date }, options);
  const Event = db.model('Event', eventSchema);

  const ClickedLinkEvent = Event.discriminator('ClickedLink',
    new Schema({ url: String }, options));

  const SignedUpEvent = Event.discriminator('SignedUp',
    new Schema({ user: String }, options));

  const event1 = new Event({ time: Date.now() });
  const event2 = new ClickedLinkEvent({ time: Date.now(), url: 'mongodb.com' });
  const event3 = new SignedUpEvent({ time: Date.now(), user: 'mongodbuser' });
  await Promise.all([event1.save(), event2.save(), event3.save()]);
  const clickedLinkEvent =  await ClickedLinkEvent.find({});
  console.log(clickedLinkEvent);
}
run();

We called the ClickedLinkEvent.find method.

Therefore, we’ll get all the ClickedLinkEvent instances.

Discriminators Pre and Post Hooks

We can add pre and post hooks to schemas created with discriminators.

For example, we can write:

async function run() {
  const { createConnection, Types, Schema } = require('mongoose');
  const db = createConnection('mongodb://localhost:27017/test');
  const options = { discriminatorKey: 'kind' };
  const eventSchema = new Schema({ time: Date }, options);
  const Event = db.model('Event', eventSchema);

  const clickedLinkSchema = new Schema({ url: String }, options)
  clickedLinkSchema.pre('validate', (next) => {
    console.log('validate click link');
    next();
  });
  const ClickedLinkEvent = Event.discriminator('ClickedLink',
    clickedLinkSchema);

  const event1 = new Event({ time: Date.now() });
  const event2 = new ClickedLinkEvent({ time: Date.now(), url: 'mongodb.com' });
  await event2.validate();
}
run();

to add a pre hook for the validate operation to the clickedLinkSchema .

Handling Custom _id Fields

If an _id field is set on the base schema, then it’ll always override the discriminator’s _id field.

For example, we can write:

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

  const eventSchema = new Schema({ _id: String, time: Date },
    options);
  const Event = db.model('BaseEvent', eventSchema);

  const clickedLinkSchema = new Schema({
    url: String,
    time: String
  }, options);

  const ClickedLinkEvent = Event.discriminator('ChildEventBad',
    clickedLinkSchema);

  const event1 = new ClickedLinkEvent({ _id: 'custom id', time: '12am' });
  console.log(typeof event1._id);
  console.log(typeof event1.time);
}
run();

And from the console log, we can see that both the _id and time fields of event1 are strings.

So the _id field is the same one as the eventSchema , but the ClickedLinkEvent field has the same type as the one in clickedLinkSchema .

Using Discriminators with Model.create()

We can use discriminators with the Model.create method.

For example, we can write:

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

  const Shape = db.model('Shape', shapeSchema);

  const Circle = Shape.discriminator('Circle',
    new Schema({ radius: Number }));
  const Square = Shape.discriminator('Square',
    new Schema({ side: Number }));

  const shapes = [
    { name: 'Test' },
    { kind: 'Circle', radius: 5 },
    { kind: 'Square', side: 10 }
  ];
  const [shape1, shape2, shape3] = await Shape.create(shapes);
  console.log(shape1 instanceof Shape);
  console.log(shape2 instanceof Circle);
  console.log(shape3 instanceof Square);
}
run();

We created 3 schemas for shapes with the discriminator method.

Then we called Shape.create with an array of different shape objects.

We specified the type with the kind property since we set that as the discriminator key.

Then in the console log, they should all log true since we specified the type of each entry.

If it’s not specified, then it has the base type.

Conclusion

We can add hooks to schemas created from discriminators.

_id fields are handled differently from other discriminator fields.

And we can use the create method with discriminators.

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.