Mongoose: popluate level 2 child object - mongodb

I have the following code in a file "test.js", in which I am trying to populate story.fans[0].stories[0]
, but it doesn't work. The rest of the code runs fine, but when it is trying to populate fan[0]'s child object stories, it doesn't seem to work.
Can mongoose popluate 2 level child objects?
const mongoose = require('mongoose');
main().catch(err => console.log(err));
async function main() {
await mongoose.connect('mongodb://localhost:27017/testMongoose');
const Schema = mongoose.Schema;
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 = mongoose.model('Story', storySchema);
const Person = mongoose.model('Person', personSchema);
Story.
findOne({ title: 'Casino Royale' }).populate('fans').
exec(function (err, story) {
if (err) return handleError(err);
console.log('the story is',story.title);
console.log('The fans[0] is %s', story.fans[0].name);
story.fans[0].populate('stories');
console.log('the story written by fan is',story.fans[0].stories[0].title);
//option2
story.fans[0].populate('stories').exec(function(err,fan){
console.log('the story written by fan is',fan.stories[0].title);
});
});
}
Here is the error message:
----------------
This is my stories collection:
/* 1 */
{
"_id" : ObjectId("61cfd221256ef6d903523700"),
"author" : ObjectId("61cfd221256ef6d9035236fe"),
"title" : "Casino Royale",
"fans" : [
ObjectId("61cfee8b5059fb3fe37b3c5f")
],
"__v" : 1
}
/* 2 */
{
"_id" : ObjectId("61d09887abeb41f82a7e1678"),
"author" : ObjectId("61cfee8b5059fb3fe37b3c5f"),
"title" : "Story 001",
"fans" : [],
"__v" : 0
}
This is my people collection
/* 1 */
{
"_id" : ObjectId("61cfd221256ef6d9035236fe"),
"name" : "Ian Fleming",
"age" : 50,
"stories" : [],
"__v" : 0
}
/* 2 */
{
"_id" : ObjectId("61cfee8b5059fb3fe37b3c5f"),
"name" : "Fan 001",
"age" : 38,
"stories" : [
ObjectId("61d09fbfbd8f3fa20beaa616")
],
"__v" : 14
}

Considering the data provided by you, there is nothing in layer 2 to be populated, since the referenced id in Fan 001 is not matching with any of the stories.
Fans stories:
61d09fbfbd8f3fa20beaa616
==> is not found in
Available stories:
61d09887abeb41f82a7e1678
61cfd221256ef6d903523700
But if you fix your references what you want to be using is called deep Population
You basically populate a field in the object you just populated.
Your code then looks like this:
Story.findOne({ title: 'Casino Royale' })
.populate({ path: 'fans', model: 'Person', populate: { path: 'stories', model: 'Story' } }).
exec(function(err, story) {
if (err) return handleError(err);
console.log(story)
console.log('the story is', story.title);
console.log('The fans[0] is %s', story.fans[0].name);
console.log('the story written by fan is', story.fans[0].stories[0].title);
});
I rebuild the example but fixed the references and the output now looks like expected:

Related

how to control the find() to get all children with parent comment

I am a bigenner in mongoose, I implement a concept of comment. In fact, I create a schema comment in which I identify the different fields, as it is shown:
import mongoose from 'mongoose';
/* eslint-disable-next-line */
import db from './db';
// Define Schema method= mongoose;
const { Schema } = mongoose;
const CommentSchema = new Schema({
id: {
type: String,
required: true,
unique: true,
},
author_id: {
type: String,
required: true,
},
content: {
type: String,
},
creation_date: {
type: Date,
default: Date.now,
},
interesting_comment: [String],
updated_content_list: [{ previous_date: Date, previous_content: String }],
children: [String],
});
export default mongoose.model('Comment', CommentSchema);
Note: children is about the response to a parent comment, here is the example:
{
"_id" : ObjectId("60217951f57f24079f885d66"),
"interesting_comment" : [
"6ff10e9"
],
"children" : [
"c13e5487-07ba-4bc9-996e-ef1c8f826b32"
],
"updated_content_list" : [ ],
"id" : "6f169ee4-d22d-49a2-9f71-5dacadecf6bb",
"author_id" : "b08a3310-b3f4-4521-9b1a-2d5667a3ea1d",
"content" : "Hello wijdene",
"creation_date" : ISODate("2021-02-08T17:48:01.026Z"),
"__v" : 4
}
{
"_id" : ObjectId("602179d1f57f24079f885d68"),
"interesting_comment" : [ ],
"children" : [ ],
"updated_content_list" : [ ],
"id" : "c13e5487-07ba-4bc9-996e-ef1c8f826b32",
"author_id" : "111",
"content" : "hi nesrine",
"creation_date" : ISODate("2021-02-08T17:50:09.075Z"),
"__v" : 0
}
I implement this function:
async function ListComment() {
const comments = await Comment.find();
return comments;
}
I want to get all the children by their parent comment, I spend hours and I don't get the idea any help please
I try to fix this problem with lookup:
async function ListComment() {
const comments = await Comment.aggregate([{$unwind:{path:"$children",preserveNullAndEmptyArrays: true}},{$lookup:{from:"comments",localField:"children",foreignField:"id",as:"children"}}]);
return comments;
}
But even the child, it is shown in this way. In fact, I add preserveNullAndEmptyArrays: true to preserve parent which doesn't contain a child. But what I get is the new combination + parent without children+children. I want to remove children, any idea

MongoDB / Mongoose one-to-many relationship

Working on a database structure for a new project, and trying to figure out how exactly one-to-many relationships in MongoDB / mongoose work.
So considering a following simple structure :
I have a project table/schema with multiple images linked to it:
const ProjectSchema = mongoose.Schema({
_id: mongoose.Types.ObjectId, // Just the id,
name: { type: String, required: true, trim: true, maxLength: 60 },
description: { type: String, required: false },
images: [{
type: Schema.Types.ObjectId, ref: 'Image'
}]
});
And then an image table/schema:
const ImageSchema = new Schema({
_id: mongoose.Types.ObjectId, // Just the id,
url: { type: String, unique: true, required: true }
project: {
type: Schema.Types.ObjectId, ref: 'Project'
}
});
I want one-to-many between images and projects, so I save an image with a project id:
const image = new Image(
{
project: project._id,
_id: new mongoose.Types.ObjectId(),
url: 'https://someurl',
});
await image.save();
If I then find this image, and populate the project field - it contains all the project's info nicely, BUT if I find this project, it doesn't have anything in the images array (referencing Image):
images: [{
type: Schema.Types.ObjectId, ref: 'Image'
}]
I thought that with ref you're creating the foreign key reference between Project and Image and then both Image should have the linked Project and Project should see the linked image in the array.
Is that not the case or am I missing something here ?
You should be able to populate images from project with no problem.
Let's say you have this project document:
{
"_id" : ObjectId("5e592a438b93764a40a81a96"),
"images" : [
ObjectId("5e592aba8b93764a40a81a98"),
ObjectId("5e592ac78b93764a40a81a99")
],
"name" : "Project 1",
"__v" : 0
}
And these image documents:
{
"_id" : ObjectId("5e592ac78b93764a40a81a99"),
"url" : "Url 2",
"project" : ObjectId("5e592a438b93764a40a81a96"),
"__v" : 0
},
{
"_id" : ObjectId("5e592aba8b93764a40a81a98"),
"url" : "Url 1",
"project" : ObjectId("5e592a438b93764a40a81a96"),
"__v" : 0
}
We can use the following code to populate images:
router.get("/projects/:id", async (req, res) => {
const result = await Project.findById(req.params.id).populate("images");
res.send(result);
});
This will give a result like this:
{
"images": [
{
"_id": "5e592aba8b93764a40a81a98",
"url": "Url 1",
"project": "5e592a438b93764a40a81a96",
"__v": 0
},
{
"_id": "5e592ac78b93764a40a81a99",
"url": "Url 2",
"project": "5e592a438b93764a40a81a96",
"__v": 0
}
],
"_id": "5e592a438b93764a40a81a96",
"name": "Project 1",
"__v": 0
}
So for your case, check if your project document really contains images array with the Object ids for image documents, and you populate correctly.
Also you don't need to add _id fields to the schema, mongodb will itself generate _id automatically.
project
const mongoose = require("mongoose");
const Schema = mongoose.Schema;
const ProjectSchema = Schema({
name: { type: String, required: true, trim: true, maxLength: 60 },
description: { type: String, required: false },
images: [
{
type: Schema.Types.ObjectId,
ref: "Image"
}
]
});
module.exports = mongoose.model("Project", ProjectSchema);
image
const mongoose = require("mongoose");
const Schema = mongoose.Schema;
const ImageSchema = new Schema({
url: { type: String, unique: true, required: true },
project: {
type: Schema.Types.ObjectId,
ref: "Project"
}
});
module.exports = mongoose.model("Image", ImageSchema);

Mongoose query not showing subdocument

I can't get mongoose to show subdocument when running find() while it displays perfectly well in mongodb shell.
Subdocument should be embedded based on my schema, not objectId referenced, so I shouldn't be running any black magic voodoo to get my data to show up.
const UserSchema = new mongoose.Schema({
username: String;
xp: Number;
//etc.
});
const RoomSchema = new mongoose.Schema({
timestamp: { type: Date, default: Date.now },
status: { type: String, enum: ["pending", "ongoing", "completed"]},
players: {
type: [{
points: { type: Number, default: 0 },
position: String,
user: UserSchema
}],
maxlength:2
}
});
After adding a new room with:
let room = new Room(coreObj);
room.players.push({
points: 0,
position: 'blue',
user: userObj //where userObj is a result of running findById on User model
});
It displays nicely in mongo shell, when running db.rooms.find({}).pretty() I can see that full document has been added. However, when running on mongoose model:
Room.find({}).exec((err,rooms)=>{
console.log(rooms[0].toJSON());
});
I don't see user subdocument, moreover I cannot see user field entirely! What seems to be the problem?
logged json from mongoose model:
{
"status": "pending",
"_id": "5cf5a25c050db208641a2076",
"timestamp": "2019-06-03T22:42:36.946Z",
"players": [
{
"points": 0,
"_id": "5cf5a25c050db208641a2077",
"position": "blue"
}
],
"__v": 0
}
json from mongo shell:
{
"_id" : ObjectId("5cf5a25c050db208641a2076"),
"status" : "pending",
"timestamp" : ISODate("2019-06-03T22:42:36.946Z"),
"players" : [
{
"points" : 0,
"_id" : ObjectId("5cf5a25c050db208641a2077"),
"position" : "blue",
"user" : {
"xp" : 0,
"_id" : ObjectId("5cf2da91a45db837b8061270"),
"username" : "bogdan_zvonko",
"__v" : 0
}
}
],
"__v" : 0
}
Keeping best practice in mind, I think it would be more appropriate to reference the UserSchema in the RoomSchema. Something like:
...
user: {
type: Schema.Types.ObjectId,
ref: 'UserSchema'
}
Then you would store the user._id in that field.
This way, if the user is modified, your RoomSchema is always referencing the correct information. You could then get the user using Mongoose's populate
I'm not entirely sure why you can't see the sub-sub-document, but this code example printed it correctly for me. Example was originally posted in https://mongoosejs.com/docs/subdocs.html but modified slightly to contain sub-sub-document so it looks similar to your code:
var grandChildSchema = new mongoose.Schema({ name: 'string' });
var childSchema = new mongoose.Schema({ name: 'string', grandChild: grandChildSchema });
var parentSchema = new mongoose.Schema({ children: [childSchema] });
var Parent = mongoose.model('Parent', parentSchema);
var parent = new Parent({
children: [
{ name: 'Matt', grandChild: {name: 'Matt Jr'} },
{ name: 'Sarah', grandChild: {name: 'Sarah Jr'} }
]
})
parent.save(function() {
Parent.find().exec(function(err, res) {
console.log(JSON.stringify(res[0]))
mongoose.connection.close()
})
});
Executing this code resulted in:
{
"_id": "5cf7096408b1f54151ef907c",
"children": [
{
"_id": "5cf7096408b1f54151ef907f",
"name": "Matt",
"grandChild": {
"_id": "5cf7096408b1f54151ef9080",
"name": "Matt Jr"
}
},
{
"_id": "5cf7096408b1f54151ef907d",
"name": "Sarah",
"grandChild": {
"_id": "5cf7096408b1f54151ef907e",
"name": "Sarah Jr"
}
}
],
"__v": 0
}
This was tested using Mongoose 5.5.12.
Note that I was using JSON.stringify() to print the document instead of using Mongoose's toJSON().
I just met a very similar problem, i think i got it.
the whole point is in model which you use:
const RoomSchema = new mongoose.Schema({
...
players: {
type: [{
...
user: UserSchema
...
but then you make
room.players.push({
points: 0,
position: 'blue',
user: userObj //where userObj is a result of running findById on User model
});
so you are missing the "type" subfield, so your doc is not compliant with your RoomSchema and mongoose do not show the parts which does not fit schema.

populate following users mongoose

Lemme take time to explain what is happening from start to finish.
Preamble:
A user a follows 10 other people. When user A logs in, an X number of posts from each of the 10 people are pulled into view.
I do not know if it is the right thing to do, and will appreciate a better way of doing it. However, I wanna give it a try, and it ain't working.
Follow Model:
let mongoose = require('mongoose');
let Schema = mongoose.Schema;
let FollowSchema = new Schema({
user: {
type: Schema.Types.ObjectId,
ref: 'User'
},
followers: [{
type: Schema.Types.ObjectId,
ref: 'Card'
}],
following: [{
type: Schema.Types.ObjectId,
ref: 'Card'
}]
});
module.exports = mongoose.model('Follow', FollowSchema);
Card Model
let mongoose = require('mongoose');
let Schema = mongoose.Schema;
let CardSchema = new Schema({
title: String,
content: String,
createdById: {
type: Schema.Types.ObjectId,
ref: 'User'
},
createdBy: {
type: String
}
});
module.exports = mongoose.model('Card', CardSchema);
Follow logic
When user A follows user B, do two things:
Push the user_id of B to user A document on field 'following' (A is following B)
Push user_id of A to user B document on field 'followers' (B is followed by A)
router.post('/follow', utils.loginRequired, function(req, res) {
const user_id = req.user._id;
const follow = req.body.follow_id;
let bulk = Follow.collection.initializeUnorderedBulkOp();
bulk.find({ 'user': Types.ObjectId(user_id) }).upsert().updateOne({
$addToSet: {
following: Types.ObjectId(follow)
}
});
bulk.find({ 'user': Types.ObjectId(follow) }).upsert().updateOne({
$addToSet: {
followers: Types.ObjectId(user_id)
}
})
bulk.execute(function(err, doc) {
if (err) {
return res.json({
'state': false,
'msg': err
})
}
res.json({
'state': true,
'msg': 'Followed'
})
})
})
Actual DB values
> db.follows.find().pretty()
{
"_id" : ObjectId("59e3e27dace1f14e0a70862d"),
"user" : ObjectId("59e2194177cae833894c9956"),
"following" : [
ObjectId("59e3e618ace1f14e0a708713")
]
}
{
"_id" : ObjectId("59e3e27dace1f14e0a70862e"),
"user" : ObjectId("59e13b2dca5652efc4ca2cf5"),
"followers" : [
ObjectId("59e2194177cae833894c9956"),
ObjectId("59e13b2d27cfed535928c0e7"),
ObjectId("59e3e617149f0a3f1281e849")
]
}
{
"_id" : ObjectId("59e3e71face1f14e0a708770"),
"user" : ObjectId("59e13b2d27cfed535928c0e7"),
"following" : [
ObjectId("59e3e618ace1f14e0a708713"),
ObjectId("59e13b2dca5652efc4ca2cf5"),
ObjectId("59e21942ca5652efc4ca30ab")
]
}
{
"_id" : ObjectId("59e3e71face1f14e0a708771"),
"user" : ObjectId("59e3e618ace1f14e0a708713"),
"followers" : [
ObjectId("59e13b2d27cfed535928c0e7"),
ObjectId("59e2194177cae833894c9956")
]
}
{
"_id" : ObjectId("59e3e72bace1f14e0a708779"),
"user" : ObjectId("59e21942ca5652efc4ca30ab"),
"followers" : [
ObjectId("59e13b2d27cfed535928c0e7"),
ObjectId("59e2194177cae833894c9956"),
ObjectId("59e3e617149f0a3f1281e849")
]
}
{
"_id" : ObjectId("59f0eef155ee5a5897e1a66d"),
"user" : ObjectId("59e3e617149f0a3f1281e849"),
"following" : [
ObjectId("59e21942ca5652efc4ca30ab"),
ObjectId("59e13b2dca5652efc4ca2cf5")
]
}
>
With the above database results, this is my query:
Query
router.get('/follow/list', utils.loginRequired, function(req, res) {
const user_id = req.user._id;
Follow.findOne({ 'user': Types.ObjectId(user_id) })
.populate('following')
.exec(function(err, doc) {
if (err) {
return res.json({
'state': false,
'msg': err
})
};
console.log(doc.username);
res.json({
'state': true,
'msg': 'Follow list',
'doc': doc
})
})
});
With the above query, from my little understanding of Mongoose populate, I expect to get cards from each of the Users in the following array.
My understanding and expectations might be wrong, however with such an endgoal, is this populate approach okay? Or am I trying to solve an aggregation task with population?
UPDATE:
Thanks for the answer. Getting quite close, but still, the followingCards array contains no result. Here's the contents of my current Follow model:
> db.follows.find().pretty()
{
"_id" : ObjectId("59f24c0555ee5a5897e1b23d"),
"user" : ObjectId("59f24bda1d048d1edad4bda8"),
"following" : [
ObjectId("59f24b3a55ee5a5897e1b1ec"),
ObjectId("59f24bda55ee5a5897e1b22c")
]
}
{
"_id" : ObjectId("59f24c0555ee5a5897e1b23e"),
"user" : ObjectId("59f24b3a55ee5a5897e1b1ec"),
"followers" : [
ObjectId("59f24bda1d048d1edad4bda8")
]
}
{
"_id" : ObjectId("59f24c8855ee5a5897e1b292"),
"user" : ObjectId("59f24bda55ee5a5897e1b22c"),
"followers" : [
ObjectId("59f24bda1d048d1edad4bda8")
]
}
>
Here are all the current content I have from Card Model:
> db.cards.find().pretty()
{
"_id" : ObjectId("59f24bc01d048d1edad4bda6"),
"title" : "A day or two with Hubtel's HTTP API",
"content" : "a day or two",
"external" : "",
"slug" : "a-day-or-two-with-hubtels-http-api-df77056d",
"createdBy" : "seanmavley",
"createdById" : ObjectId("59f24b391d048d1edad4bda5"),
"createdAt" : ISODate("2017-10-26T20:55:28.293Z"),
"__v" : 0
}
{
"_id" : ObjectId("59f24c5f1d048d1edad4bda9"),
"title" : "US couple stole goods worth $1.2m from Amazon",
"content" : "for what",
"external" : "https://bbc.com",
"slug" : "us-couple-stole-goods-worth-dollar12m-from-amazon-49b0a524",
"createdBy" : "nkansahrexford",
"createdById" : ObjectId("59f24bda1d048d1edad4bda8"),
"createdAt" : ISODate("2017-10-26T20:58:07.793Z"),
"__v" : 0
}
With the Populate Virtual example from yours (#Veeram), here's the response I get:
{"state":true,"msg":"Follow list","doc":{"_id":"59f24c0555ee5a5897e1b23d","user":"59f24bda1d048d1edad4bda8","following":["59f24b3a55ee5a5897e1b1ec","59f24bda55ee5a5897e1b22c"],"followers":[],"id":"59f24c0555ee5a5897e1b23d","followingCards":[]}}
The followingCards array is empty.
Using the $lookup query on the other hand simply returns []
I'm likely missing something?
You can use either virtual populate or $lookup operator in aggregation pipeline.
Using Virtual Populate
FollowSchema.virtual('followingCards', {
ref: 'Card',
localField: 'following',
foreignField: 'createdById'
});
Follow.findOne({
'user': Types.ObjectId(user_id) })
.populate('followingCards')
.exec(function(err, doc) {
console.log(JSON.stringify(doc));
});
Using $lookup aggregation
Follow.aggregate([
{
"$match": {
"user": Types.ObjectId(user_id)
}
},
{
"$lookup": {
"from": "cards",
"localField": "following",
"foreignField": "createdById",
"as": "followingCards"
}
}
]).exec(function (err, doc) {
console.log(JSON.stringify(doc));
})
var mongoose = require('mongoose'), Schema = mongoose.Schema
var eventSchema = Schema({
title : String,
location : String,
startDate : Date,
endDate : Date
});
var personSchema = Schema({
firstname: String,
lastname: String,
email: String,
dob: Date,
city: String,
eventsAttended: [{ type: Schema.Types.ObjectId, ref: 'Event' }]
});
var Event = mongoose.model('Event', eventSchema);
var Person = mongoose.model('Person', personSchema);
To show how populate is used, first create a person object,
aaron = new Person({firstname: 'Aaron'}) and an event object,
event1 = new Event({title: 'Hackathon', location: 'foo'}):
aaron.eventsAttended.push(event1);
aaron.save(callback);
Then, when you make your query, you can populate references like this:
Person
.findOne({ firstname: 'Aaron' })
.populate('eventsAttended') .exec(function(err, person) {
if (err) return handleError(err);
console.log(person);
});
// only works if we pushed refs to person.eventsAttended
note: change Activity.find to Card.find
const { ObjectID } = require("mongodb");
// import Follow and Activity(Card) schema
const userId = req.tokenData.userId; // edit this too...
Follow.aggregate([
{
$match: {
user: ObjectID(userId)
}
}
])
.then(data => {
// console.log(data)
var dataUsers = data[0].following.map(function(item) {
return item._id;
});
// console.log(dataUsers)
Activity.find(
{ createdById: { $in: dataUsers } },
{
_id: 1,
title: 1,
content: 1,
createdBy: 1,
creatorAvatar: 1,
activityType: 1,
createdAt: 1
}
)
// .sort({createdAt:-1)
.then(posts => res.send({ posts }));
});

Correct way to return from mongo to datatable

I'm using mongoose and returning documents from a collection to be displayed using datatables. I'm having some issues though. The client-side code is
var table = $('#dataTables-example').DataTable( {
"bProcessing" : true,
"bServerSide" : true,
"ajax" : {
"url" : "/mongo/get/datatable",
"dataSrc": ""
},
"columnDefs": [
{
"data": null,
"defaultContent": "<button id='removeProduct'>Remove</button>",
"targets": -1
}
],
"aoColumns" : [
{ "mData" : "name" },
{ "mData" : "price" },
{ "mData" : "category" },
{ "mData" : "description" },
{ "mData" : "image" },
{ "mData" : "promoted" },
{ "mData" : null}
]
});
Then this handled on the server-side using the following
db.once('open', function callback ()
{
debug('Connection has successfully opened');
productSchema = mongoose.Schema({
name: String,
price: String,
category: String,
description: String,
image: String,
promoted: Boolean
});
Product = mongoose.model('Product', productSchema, 'products');
});
exports.getDataForDataTable = function (request, response) {
Product.dataTable(request.query, function (err, data) {
debug(data);
response.send(data);
});
};
If I use the above code the datatable fails to display the documents, claiming no matching records found BUT it does correctly display the number of docs Showing 1 to 2 of 2 entries. If I change the server side code to response with data.data instead of data, the documents are correctly populated in the table BUT the number of records is no longer found, instead saying Showing 0 to 0 of 0 entries (filtered from NaN total entries)
exports.getDataForDataTable = function (request, response) {
Product.dataTable(request.query, function (err, data) {
debug(data);
response.send(data.data);
});
The actual data being returned when querying mongo is
{ draw: '1', recordsTotal: 2, recordsFiltered: 2, data: [ { _id: 5515274643e0bf403be58fd1, name: 'camera', price: '2500', category: 'electronics', description: 'lovely', image: 'some image', promoted: true }, { _id: 551541c2e710d65547c6db15, name: 'computer', price: '10000', category: 'electronics', description: 'nice', image: 'iamge', promoted: true } ] }
The third parameter in mongoose.model sets the collection name which is pluralized and lowercased automatically so it has no effect in this case.
Assuming your Product variable has been declared early on and global, try this:
products = mongoose.model('products', productSchema);
Product = require('mongoose').model('products');
Did you try to remove the dataSrc field in the DataTable configuration:
"ajax" : {
"url" : "/mongo/get/datatable",
},