Modelling reference to embedding document using Mongoose - node.js

I am modelling two types of events (events and subevents) in a MongoDB like this:
var EventSchema = mongoose.Schema({
'name' : String,
'subEvent' : [ SubeventSchema ]
});
var SubeventSchema = mongoose.Schema({
'name' : String
});
Now when I query a subevent I want to be able to also retrieve data about its corresponding superevent, so that some example data retrieved using Mongoose population feature could look like this:
EventModel.findOne({
name : 'Festival'
})
.populate('subEvent')
.execute(function (err, evt) { return evt; });
{
name : 'Festival',
subEvent: [
{ name : 'First Concert' },
{ name : 'Second Concert' }
]
}
EventModel.findOne({
'subEvent.name' : 'FirstConcert'
}, {
'subEvent.$' : 1
})
.populate('superEvent') // This will not work, this is the actual problem of my question
.execute(function (err, subevt) { return subevt; });
{
name: 'First Concert',
superEvent: {
name: 'Festival'
}
}
A solution I can think of is not to embed but to reference like this:
var EventSchema = mongoose.Schema({
'name' : String,
'subEvent' : [ {
'type' : mongoose.Schema.Types.ObjectId,
'ref' : 'SubeventSchema'
} ]
});
var SubeventSchema = mongoose.Schema({
'name' : String,
'superEvent' : {
'type' : mongoose.Schema.Types.ObjectId,
'ref' : 'EventSchema'
}
});
I am looking for a solution based on the first example using embedded subevents, though. Can this be achieved and in case yes, how?

I think your mental model of document embedding isn't correct. The major misunderstanding (and this is very common) is that you "query a subevent" (query an embedded document). According to your current Event schema, a Subevent is just a document embedded in an Event document. The embedded SubEvent is not a top-level document; it's not a member of any collection in MongoDB. Therefore, you don't query for it. You query for Events (which are the actual collection-level documents in your schema) whose subEvents have certain properties. E.g. one way people translate the query
db.events.find({ "subEvent" : { "name" : "First Concert" } })
into plain English is as "find all the subevents with the name "First Concert". This is wrong. The right translation is "find all events that have at least one subevent whose name is "First Concert" (the "at least one" part depends on knowledge that subEvent is an array).
Coming back to the specific question, you can hopefully see now that trying to do a populate of a "superevent" on a subevent makes no sense. Your queries return events. The optimal schema, be it subevents embedded in events, one- or two-way references between events and subevents documents in separate collections, or events denormalized into the constituent subevent documents, cannot be determined from the information in the question because the use case is not specified.

Perhaps this is a situation where you need to modify your thinking rather than the schema itself. Mongoose .populate() supports the basic ideas of MongoDB "projection", or more commonly referred to as "field selection". So rather than try to model around this, just select the fields you want to populate.
So your second schema form is perfectly valid, just change how you populate:
EventModel.find({}).populate("subEvent", "name").execute(function(err,docs) {
// "subevent" array items only contain "name" now
});
This is actually covered in the Mongoose documentation under the "populate" section.

Related

mongoose own populate with custom query

I'm trying to create a custom query method in mongoose - similar to the populate()-function of mongoose. I've the following two simple schemas:
const mongoose = require('mongoose')
const bookSchema = new mongoose.Schema({
title: String,
author: {type: mongoose.Schema.Types.ObjectId, required: true, ref: 'Author'}
}, {versionKey: false})
const authorSchema = new mongoose.Schema({
name: String
}, {versionKey: false})
Now, I want retrieve authors information and furthermore the books written by the author. As far as I know, mongoose provides custom queries, hence my idea was to write a custom query function like:
authorSchema.query.populateBooks = function () {
// semi-code:
return forAll(foundAuthors).findAll(books)
}
Now, to get all authors and all books, I can simply run:
authorModel.find({}).populateBooks(console.log)
This should result in something like this:
[ {name: "Author 1", books: ["Book 1", "Book 2"]}, {name: "Author 2", books: ["Book 3"] } ]
Unfortunately, it doesn't work because I don't know how I can access the list of authors selected previously in my populateBooks function. What I need in my custom query function is the collection of the previous-selected documents.
For example, authorModel.find({}) already returns a list of authors. In populateBooks() I need to iterate through this list to find all books for all authors. Anyone know how I can access this collection or if it's even possible?
populate: "Population is the process of automatically replacing the specified paths in the document with document(s) from other collection(s)" (from the docs i linked).
Based on your question, you're not looking for population. yours is a simple query (the following code is to achieve the example result you gave at the end. note that your books field had a value of an array of strings, I'm assuming those were the titles). Also, do note that the following code will work with the models you've already provided, but this is a bad implementation that i recommend against - for multiple reasons: efficiency, elegance, potential errors (for e.g, authors with identical names), see note after code:
Author.find({}, function(err, foundAuthors){
if (err){
console.log(err); //handle error
}
//now all authors are objects in foundAuthors
//but if you had certain search parameters, foundAuthors only includes those authors
var completeList = [];
for (i=0;i<foundAuthors.length;i++){
completeList.push({name: foundAuthors[i].name, books: []});
}
Book.find({}).populate("author").exec(function(err, foundBooks){
if (err){
console.log(err); //handle err
}
for (j=0;j<foundBooks.length;j++){
for (k=0;k<completeList.length;k++){
if (completeList[k].name === foundBooks[j].author.name){
completeList[k].books.push(foundBooks[j].title);
}
}
}
//at this point, completeList has exactly the result you asked for
});
});
However, as i stated, i recommend against this implementation, this was based on the code you already provided without changing it.
I recommend changing your author schema to include a books property:
var AuthorSchema = new mongoose.Schema({
books: [{
type: mongoose.Schema.Types.ObjectId,
ref: "Book"
}]
//all your other code for the schema
});
And add all books to their respective authors. This way, all you would need to do to get an array of objects, each of which contains an author and all of his books is one query:
Author.find({}).populate("books").exec(function(err, foundAuthors){
//if there's no err, then foundAuthors is an array of authors with their books
});
That is far simpler, more efficient, more elegant and more effective than the earlier possible solution i gave, based on your already existing code without changing it.

How to use mongoose schema when you have dynamic values?

I am trying to create schema where body can have different keys in it based on the incoming event. So when i try to rendered data it just send _id to client event is not part of results. Did i implemented wrong schema with for this approach ?
event.model.js
var mongoose = require('bluebird').promisifyAll(require('mongoose'));
var bmpEventSchema = new mongoose.Schema({
event: {
type: String,
body : {}
}
});
export default mongoose.model('BmpEvent', bmpEventSchema);
JsonDocument
{
"_id" : ObjectId("596f672f4c387baa25db5ec6"),
"event" : {
"type" : "json",
"body" : {
"evntType" : "Smtduki",
"tkt" : "75522655",
"cat" : "RNT",
"esc_lvl" : "4",
"asset" : "DNEC843027 ATI",
"esc_tm" : "2017-05-26 09:18:00",
"tos" : "T3APLS",
"mcn" : "SL6516",
"cusTkt" : "",
"tktSrc" : "BMP",
"tier1" : "STLMOETS"
}
}
}
This is a use case for discrimnators. You can make body a Mixed type but that will defeat the purpose of mongoose to provide validation. Suppose you have are modeling a books' database. You make a key named Professor for an academic book. But then you need to make a key novelist for a novel. You need to store genre for novel but not for educational books.
Now you can make a type key like you did in your use case and play with the results. But then you may have to apply default values for novelist in novels. Or you may need to set a field required in one of the types and not the other. Another problem with that approach would be to use middlewares (hooks) to the different types. You may want to perform a different function on creation of novel and a different function on creation of an educational book. It is just a scenario and you can have like 10 or 15 types which will be even more cumbersome to handle.
Now in order to avoid these issues you can make a different model for each type. But if you do that, when you want to query all books, you will have to perform a query on each of the models which will be ineffecient. You need something on the ODM layer. This is where discriminators come into play.
You make a base model with all the keys you want in all types of books and add a discrimnator key to it(refer to docs). Then you create novel from this model's discriminator function and add additional keys which will only be in novel. You can create as many child models as you like this way and then you can use them in simply a polymorphic manner. Internally, this will create a single collection named books but for novels it will store only novels' keys. The validation, middlewares etc of the different types of models will be handled by the ODM layer.
http://mongoosejs.com/docs/discriminators.html
Schema is wrong. Should be:
var bmpEventSchema = new mongoose.Schema({
event: {
type: String,
body : Mixed
}
});
There are two approaches I can suggest:
you simply don't list your keys as in your example
you list all possible keys, and mark some of them as required (according to your logic)
Example:
"key": {
type: "string",
required: true
}

Mongoose: How to find documents by sub-collection's document property value

I’m using Mongoose version 4.6.8 and MongoLab (MLab). I have a Mongoose schema called “Group” that has a collection of User subdocuments called “teachers”:
var GroupSchema = new Schema({
//…more properties here…//
teachers: [{
type: Schema.ObjectId,
ref: 'User'
}]
});
This is a document from the “groups” collection on MongoLab:
{
//…more properties here…//
"teachers": [
{
"$oid": "5799a9c759feea9c208c004c"
}
]
}
And this is a document from the “users” collection on MongoLab:
{
//…more properties here…//
"username": "bob"
}
But if I want to get a list of Groups that have a particular teacher (User) with the username of “bob”, this doesn’t work (the list of groups is empty):
Group.find({"teachers.username": "bob"}).exec(callback);
This also returns no items:
Group.find().where('teachers.username').equals('bob').exec(callback);
How can I achieve this?
Without some more knowledge of your set up (specifically whether you want anybody named Bob or a specific Bob whose id you could pick up first) - this might be some help although I think it would require you to flatten your teachers array to just their ID's, not single-key objects.
User.findById(<Id of Bob>, function(err, user){
Group.find({}, function(err, groups){
var t = groups.map(function(g){
if(g['teachers'].indexOf(user.id))
return g
})
// Do something with t
})
})
You can use populate to do that.
Try this:
Group.find({})
.populate({
path : 'teachers' ,
match : { username : "bob" }
})
.exec(callback);
populate will populate based on the teachers field (given path) and match will return only those who have username bob.
For more information on mongoose populate options, Please read Mongoose populate documentation.
I think the solution in this case is to get a teacher’s groups through the User module instead of my first inclination which was to go through the Groups module. This makes sense because it is in line with how modern APIs represent a one-to-many relationship.
As an example, in Behance’s API, an endpoint for a user’s projects is:
GET /v2/users/user/projects
And a request to this endpoint (where the User’s username is “matiascorea”) would look like this:
https://api.behance.net/v2/users/matiascorea/projects?client_id=1234567890
So in my case, instead of finding the groups by teacher, I would need to simply find the User (teacher) by username, populate the teacher’s groups, and use them:
User.findOne({username: 'bob'})
.populate('groups')
.exec(callback);
And the API call for this would be:
GET /api/users/user/groups
And a request to this endpoint would look like this:
https://example.com/api/users/bob/groups

Defining a map with ObjectId key and array of strings as value in mongoose schema

I'm facing a problem while creating Mongoose schema for my DB. I want to create a map with a objectId as key and an array of string values as its value. The closest that I can get is:
var schema = new Schema({
map: [{myId: {type:mongoose.Schema.Types.ObjectId, ref: 'MyOtherCollection'}, values: [String]}]
});
But somehow this is not working for me. When I perform an update with {upsert: true}, it is not correctly populating the key: value in the map. In fact, I'm not even sure if I have declared the schema correctly.
Can anyone tell me if the schema is correct ? Also, How can I perform an update with {upsert: true} for this schema?
Also, if above is not correct and can;t be achieved then how can I model my requirement by some other way. My use case is I want to keep a list of values for a given objectId. I don't want any duplicates entries with same key, that's why picked map.
Please suggest if the approach is correct or should this be modelled some other way?
Update:
Based on the answer by #phillee and this, I'm just wondering can we modify the schema mentioned in the accepted answer of the mentioned thread like this:
{
"_id" : ObjectId("4f9519d6684c8b1c9e72e367"),
... // other fields
"myId" : {
"4f9519d6684c8b1c9e73e367" : ["a","b","c"],
"4f9519d6684c8b1c9e73e369" : ["a","b"]
}
}
Schema will be something like:
var schema = new Schema({
myId: {String: [String]}
});
If yes, how can I change my { upsert:true } condition accordingly ? Also, complexity wise will it be more simpler/complex compared to the original schema mentioned in the thread?
I'd suggest changing the schema so you have one entry per myId,
var schema = new Schema({
myId : {type:mongoose.Schema.Types.ObjectId, ref: 'MyOtherCollection'},
values : [String]
})
If you want to update/upsert values,
Model.update({ myId : myId }, { $set : { values : newValues } }, { upsert : true })

How Do I Query A Child Model Restricting Results Based on Parent Model in Mongoose

I'm not sure how to even phrase this question... but here is a try. I'm calling the Book the "Parent" model and the Author the "Child" model.
I have two mongoose models--- Author and Books:
var Author = mongoose.model("Author", {
name: String
});
var Book = mongoose.model("Book", {
title: String,
inPrint: Boolean,
authors: [ { type: mongoose.Schema.ObjectId, ref: "Author"} ]
});
I am trying to run a query which would return all of the authors (child model) who have books (parent model) which are inPrint.
I could think of ways to do it with multiple queries, but I'm wondering if there is a way to do it with one query.
You could use populate as stated in the docs
There are no joins in MongoDB but sometimes we still want references to documents in other collections. This is where population comes in. Read more about how to include documents from other collections in your query results here.
In your case, it would look something like this:
Book.find().populate('authors')
.where('inPrint').equals(true)
.select('authors')
.exec(function(books) {
// Now you should have an array of books containing authors, which can be
// mapped to a single array.
});
I just stumbled upon this problem today and solved it:
Author.find()
.populate({ path: 'books', match: { inPrint: true } })
.exec(function (err, results) {
console.log(results); // Should do the trick
});
The magic occurs in the match option of populate, which refers to a property of the nested document to populate.
Also check my original post
EDIT: I was confusing books for authors, now it's corrected

Resources