Related
This question already has answers here:
Retrieve only the queried element in an object array in MongoDB collection
(18 answers)
Closed 5 years ago.
Am having the following Mongoose Schema in my Node Application
var expenseSchema = new Schema({
particular : String,
date : {type : Date, default: Date.now},
paid_by : String,
amount : Number,
month : String
});
var roomSchema = new Schema({
name: String,
admin: String,
roomies : [String],
expenses : [expenseSchema]
},{
collection : 'rooms'
});
I need to get expenses for a room for a particular month using find().
Here is what I was trying, however it returns me the whole room object
Room.findOne({_id : req.params._id, 'expenses.month' : 'oct'}).exec(function(err, result){
if(result == null) {
res.json({result : 'Oops! We couldn\'t find any rooms...'});
} else if(err) {
res.json({result : 'Error in getting Rooms'});
} else {
res.json({result : result});
}
});
Can someone help me with this?
You can use the positional $ operator in your projection to return the matching expenses array element in your result:
Room.findOne(
{ "_id": req.params._id, "expenses.month": "oct" },
{ "expenses.$": 1 }
).exec(callback);
or for multiple matches you could use the aggregation framework's $filter operator in the $project pipeline as:
var ObjectId = mongoose.Types.ObjectId;
Room.aggregate()
.match({ "_id": new ObjectId(req.params._id), "expenses.month": "oct" })
.project({
"expenses": {
"$filter": {
"input": "$expenses",
"as": "item",
"cond": { "$eq": [ "$$item.month", "oct" ] }
}
}
})
.exec(callback);
or if you are using an older version of Mongoose driver that does not have support for the MongoDB 3.2 $filter, you could use $map alongside $setDifference, where you can "filter" array contents without using $unwind:
var ObjectId = mongoose.Types.ObjectId;
Room.aggregate([
{ "$match": { "_id": new ObjectId(req.params._id), "expenses.month": "oct" } },
{ "$project": {
"expenses": {
"$setDifference": [
{
"$map": {
"input": "$expenses",
"as": "items",
"in": {
"$cond": [
{ "$eq": [ "$$items.month", "oct" ] },
"$$items",
false
]
}
}
},
[false]
]
}
} }
], function(err, results){
console.log(JSON.stringify(results[0], null, 4));
});
Assuming this can work
var expenseSchema = new Schema({
particular : String,
date : {type : Date, default: Date.now},
paid_by : String,
amount : Number,
month : String
});
var roomSchema = new Schema({
name: String,
admin: String,
roomies : [String],
expense_id : type: Schema.Types.ObjectId, ref: 'expenseSchema'
},{
collection : 'rooms'
});
Room.findOne({expense_id: req.params._id , "expenses.month": "oct" })
.populate('expense_id')
.exec(function (err, doc) {
if (err) {
throw err
}
console.log(doc)
})
Room.findOne({
_id : req.params._id,
'expenses.month':'oct'
},{
'expenses': {
$elemMatch:{
'month':'oct'
}
}
).exec(
function(err, result){
if(result == null) {
res.json({result : 'Oops! We couldn\'t find any rooms...'});
} else if(err) {
res.json({result : 'Error in getting Rooms'});
} else {
res.json({result : result});
}
});
I have a schema that Looks like this
var Post = new mongoose.Schema({
author: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User'
},
created: {
type: Date,
Default: Date.now
})
I have a User Table as well. I Have a array of user ids and i am trying to search the post table based on an array of user ids
For Example
var userIds = ["575e96652473d2ab0ac51c1e","575e96652473d2ab0ac51c1d"] .... and so on
I want to return all posts created by these users. And posts should be sorted by their creation date. Is there a way to group this post based on the user ids provided, basically match the posts for an individual user?
The result I am trying to attain is something like this:
[{
userAId : "56656.....",
post : [postA, postB],
},{
userBId :"12345...",
post : [postA, postB]
}]
How do I write this query?
This is what I have so far
Post.aggregate([{
// {"$unwind" : ""},
// "$group": {
// _id: "$author",
// "created" : {"$sum" : 1 }
// }
"$match" : { author : id}
}]).exec(function(error, data) {
if(error){
return console.log(error);
}else{
return console.log(data)
}
})
{
"_id" : ObjectId("575e95bc2473d2ab0ac51c1b"),
"lastMod" : ISODate("2016-06-13T11:15:08.950Z"),
"author" : ObjectId("575dac62ec13010678fe41cd"),
"created" : ISODate("2016-06-13T11:15:08.947Z"),
"type" : "photo",
"end" : null,
"commentCount" : 0,
"viewCount" : 0,
"likes" : 0,
"tags" : [],
"title" : "Today is a good day",
"__v" : 0
}
To return all posts created by users depicted in a list of ids, use the $in operator in your query and then chain the sort() method to the query to order the results by the created date field:
Post.find({ "author": { "$in": userIds } })
.sort("-created") // or .sort({ field: 'asc', created: -1 });
.exec(function (err, data){
if(err){
return console.log(err);
} else {
return console.log(data);
}
});
To get a result where you have the post id's grouped per user, you need to run the following aggregation operation:
Post.aggregate([
{ "$match" : { "author": { "$in": userIds } } },
{ "$sort": { "created": -1 } },
{
"$group" : {
"_id" : "$author",
"posts" : { "$push": "$_id" }
}
},
{
"$project": {
"_id": 0,
"userId": "$_id",
"posts": 1
}
}
]).exec(function (err, result){
if(err){
return console.log(err);
} else {
return console.log(result);
}
});
Or with the fluent API:
Post.aggregate()
.match({ "author": { "$in": userIds } })
.sort("-created")
.group({
"_id" : "$author",
"posts" : { "$push": "$_id" }
})
.project({
"_id" : 0,
"userId" : "$_id",
"posts": 1
})
.exec(function (err, result){
if(err){
return console.log(err);
} else {
return console.log(result);
}
});
This should be possible without aggregation.
Post
.find({ author: { $in: userIds } })
.sort({ created: -1 })
If you get CastError: Cast to ObjectId failed, make sure to map your userIds array from an array of strings to an array of mongoose id's.
userIds = userIds.map(userId => new mongoose.Types.ObjectId(userId))
I have a mongoose model like this:
var activityItem = mongoose.Schema({
timestampValue: Number,
xabc: String,
full: Boolean,
comp: Boolean
});
var ABC = mongoose.Schema({
activity: [activityItem],
user: {
type: mongoose.Schema.ObjectId,
ref: 'User'
},
username: String
});
I want to get the activityItem array elements that have a timestampValue less than a specific value. Also, I want to sort the activity array first according to the timestampValue
This is the code that I currently have. And it doesn't work.
UserActivity.findOne({
'user': current_user,
'activity' : {
$all: [
{
"$elemMatch": {
timestampValue: {
$lte: time
}
}
}
]
}
},
function(err, user){
})
Sample Document structure:
{
"_id" : ObjectId("56d5e88adfd14baf1848a7c6"),
"user" : ObjectId("56bf225342e662f4277ded73"),
"notifications" : [],
"completed" : [],
"activity" : [
{
"timestampValue": 1456902600000,
"xabc": "Some value",
"full": true,
"comp": false,
"_id" : ObjectId("56d5e88adfd14baf1848a7d2")
},
{
"timestampValue": 1456702600000,
"xabc": "Some other value",
"full": true,
"comp": false,
"_id" : ObjectId("56d5e88adfd14baf1848a7d3")
}
],
"__v" : 1
}
The POST call has the following params
hash: "2e74aaaf42aa5ea733be963cb61fc5ff"
time: 1457202600000
hash comes into the picture once i have the docs from mongo
time is a unix timestamp value.
Instead of returning only the elements that are less than the time value, it is returning all the array elements. I tried the aggregation framework to sort the array before querying, but couldn't get the hang of it.
Any help would be greatly appreciated.
Please try to do it through aggregation as below
ABS.aggregate([
// filter the document by current_user
{$match: {user: ObjectId(current_user)}},
// unwind the activity array
{$unwind: '$activity'},
// filter the timestampValue less than time
{$match: {'activity.timestampValue': {$lte: time}}},
// sort activity by timestampValue in ascending order
{$sort: {'activity.timestampValue': 1}},
// group by _id, and assemble the activity array.
{$group: {_id: '$_id', user: {$first: '$user'},activity: {$push: '$activity'}}}
], function(err, results){
if (err)
throw err;
// populate user to get details of user information if needed
//ABS.populate( results, { "path": "user" }, function(err, rets) {
//
//});
});
Well, it seems little bit tricky with MongoDb aggregation pipeline unless you have MongoDB 3.2, but you can definitely
achieve your result with help of map-reduce.
e.g.
MongoDB version < 3.2
var findActivities = function (time) {
db.col.mapReduce(function () {
var item = Object.assign({}, this);
delete item.activity;
item.activity = [];
for (var i = 0; i < this.activity.length; i++) {
if (this.activity[i].timestampValue <= time) {
item.activity.push(this.activity[i]);
}
}
emit(item._id, item);
}, function (k, v) {
return {items: v};
}, {
out: {"inline": true},
scope: {time: time}
}).results.forEach(function (o) {
printjson(o); // Or perform action as appropriate
});
};
Based your sample data when called findActivities(1456802600000), it will find and return only those documents matching criteria.
{
"_id" : ObjectId("56d5e88adfd14baf1848a7c6"),
"value" : {
"_id" : ObjectId("56d5e88adfd14baf1848a7c6"),
"user" : ObjectId("56bf225342e662f4277ded73"),
"notifications" : [
],
"completed" : [
],
"__v" : NumberInt(1),
"activity" : [
{
"timestampValue" : NumberLong(1456702600000),
"xabc" : "Some other value",
"full" : true,
"comp" : false,
"_id" : ObjectId("56d5e88adfd14baf1848a7d3")
}
]
}
}
MongoDB version 3.2+
db.col.aggregate([
{$project:{user:1, notifications:1, completed:1, activity:{
$filter:{input: "$activity", as: "activity", cond:{
$lte: ["$$activity.timestampValue", 1456802600000]}}}}}
])
Both solutions will have same output.
I have a mongoose Schema looking like this :
var AnswerSchema = new Schema({
author: {type: Schema.Types.ObjectId, ref: 'User'},
likes: [{type: Schema.Types.ObjectId, ref: 'User'}],
date: {type: Date, default: Date.now},
text: String,
....
});
As of now, I query this collection by doing this :
Answer.find({
author: profileId,
date: {$lt: fromDate}
}).sort({date: -1})
.limit(25)
.populate('question.sender', 'username username_display name')
.exec(function (err, answers) {
*code here*
});
But I need to add a computed field in this query to know if the visitor already liked the answers of an author (see this question) thanks to the aggregation pipeline. It would look like this :
Answer.aggregate()
.project({
"author": 1,
"matched": {
"$eq": [
{
"$size": {
"$ifNull": [
{ "$setIntersection": [ "$likes", [userId] ] },
[]
]
}
},
1
]
}
})
.exec(function (err, docs){
console.log(docs);
})
Problem is : I need to run this projection on the documents filtered by the first query only. In other words, I need to combine the find/limit/sort, with the project.
How could I do this ? I've tried chaining the .project() after the first query (before the .exec()), but it doesn't work. Maybe I need to do all the things I do in the first query via the aggregation chain of mongodb ?
EDIT:
Here is an example of data-set to try this :
{
"_id" : ObjectId("56334295e45552c018fc475d"),
"author" : ObjectId("561f9c319cdd94401ae160ef"),
"likeCount" : 1,
"likes" : [
ObjectId("561f9c319cdd94401ae160ef")
],
"text" : "This is the answer content",
"question" : {
"questionId" : ObjectId("56334031e45552c018fc4757"),
"sender" : ObjectId("561f9c319cdd94401ae160ef"),
"date" : ISODate("2015-10-30T10:02:25.323Z")
},
"date" : ISODate("2015-10-30T10:12:37.894Z")
},
{
"_id" : ObjectId("563246cfa04c1b281b6d97bf"),
"author" : ObjectId("561f9c319cdd94401ae160ef"),
"likeCount" : 0,
"likes" : [],
"text" : "Bla bla bla",
"question" : {
"questionId" : ObjectId("562f9a74a16d6fb418752b37"),
"sender" : ObjectId("561f9c319cdd94401ae160ef"),
"date" : ISODate("2015-10-27T15:38:28.337Z")
},
"date" : ISODate("2015-10-29T16:18:23.020Z")
}
Use the aggregate fluent API's chaining methods match(), sort() + limit() before the project() method:
Answer.aggregate()
.match({
"author": profileId,
"date": { "$lt": fromDate }
})
.sort("-date")
.limit(25)
.project({
"author": 1, "question.sender": 1,
"matched": {
"$eq": [
{
"$size": {
"$ifNull": [
{ "$setIntersection": [ "$likes", [userId] ] },
[]
]
}
},
1
]
}
})
.exec(function (err, docs){
if (err) throw err;
console.log(docs);
var answers = docs.map(function(doc) { return new Answer(doc); });
Answer.populate(answers, {
"path": "question.sender",
"select": "username username_display name"
}, function(err, results) {
if (err) throw err;
console.log(JSON.stringify(results, undefined, 4 ));
res.json(results);
});
});
Schema Definitions
Team.js
var TeamSchema = new Schema({
// Team Name.
name: String,
lead: String,
students :type: [{
block : Number,
status : String,
student : {
type: Schema.ObjectId,
ref: 'Student'
}]
});
Student.js
var StudentSchema = new Schema({
name: String,
rollNo : Number,
class : Number
});
How I can populate "student" to get output, as below:
team
{
"__v": 1,
"_id": "5252875356f64d6d28000001",
"students": [
{
"__v": 1,
"_id": "5252875a56f64d6d28000002",
block : 1,
status : joined,
"student": {
"name": Sumeeth
"rollNo" : 2
"class" : 5
}
},
{
"__v": 1,
"_id": "5252875a56f64d6d28000003",
block : 1,
status : joined,
"student": {
"name": Sabari
"rollNo" : 3
"class" : 4
}
}
],
"lead": "Ratha",
}
This is JS I use to get the document using Mongoose:
Team.findOne({
_id: req.team._id
})
.populate('students')
.select('students')
.exec(function(err, team) {
console.log(team);
var options = {
path: 'students.student',
model: 'Student'
};
Student.populate(team.students,options,function(err, students) {
console.log(students);
if (err) {
console.log(students);
res.send(500, {
message: 'Unable to query the team!'
});
} else {
res.send(200, students);
}
});
});
In my console output I get the following:
{ _id: 53aa434858f760900b3f2246,
students
[ { block : 1
status: 'joined'
_id: 53aa436b58f760900b3f2249 },
{ block : 1
status: 'joined'
_id: 53aa436b58f760900b3f2250 }]
}
And the expected output is:
{ _id: 53aa434858f760900b3f2246,
students
[ { block : 1
status: 'joined'
student :{
"name": Sumeeth
"rollNo" : 2
"class" : 5
}
},
{ block : 1
status: 'joined'
student :{
"name": Sabari
"rollNo" : 3
"class" : 4
}
}
]
}
Some one please help me where I am wrong. How should I make use of .populate, so that , I can get the entire student object and not only its id.
Reference :
Populate nested array in mongoose
I have been facing same issue. I have use this code for my rescue :
Team.findOne({_id: req.team._id})
.populate({ path: "students.student"})
.exec(function(err, team) {
console.log(team);
});
Here is a simplified version of what you want.
Basic data to set up, first the "students":
{
"_id" : ObjectId("53aa90c83ad07196636e175f"),
"name" : "Bill",
"rollNo" : 1,
"class" : 12
},
{
"_id" : ObjectId("53aa90e93ad07196636e1761"),
"name" : "Ted",
"rollNo" : 2,
"class" : 12
}
And then the "teams" collection:
{
"_id" : ObjectId("53aa91b63ad07196636e1762"),
"name" : "team1",
"lead" : "me",
"students" : [
{
"block" : 1,
"status" : "Y",
"student" : ObjectId("53aa90c83ad07196636e175f")
},
{
"block" : 2,
"status" : "N",
"student" : ObjectId("53aa90e93ad07196636e1761")
}
]
}
This is how you do it:
var async = require('async'),
mongoose = require('mongoose');
Schema = mongoose.Schema;
mongoose.connect('mongodb://localhost/team');
var teamSchema = new Schema({
name: String,
lead: String,
students: [{
block: Number,
status: String,
student: {
type: Schema.ObjectId, ref: 'Student'
}
}]
});
var studentSchema = new Schema({
name: String,
rollNo: Number,
class: Number
});
var Team = mongoose.model( "Team", teamSchema );
var Student = mongoose.model( "Student", studentSchema );
Team.findById("53aa91b63ad07196636e1762")
.select('students')
.exec(function(err, team) {
console.log( team );
async.forEach(team.students, function(student,callback) {
Student.populate(
student,
{ "path": "student" },
function(err,output) {
if (err) throw err;
callback();
}
);
},function(err) {
console.log( JSON.stringify( team, undefined, 4 ) );
});
});
And it gives you the results:
{
"_id": "53aa91b63ad07196636e1762",
"students": [
{
"block": 1,
"status": "Y",
"student": {
"_id": "53aa90c83ad07196636e175f",
"name": "Bill",
"rollNo": 1,
"class": 12
}
},
{
"block": 2,
"status": "N",
"student": {
"_id": "53aa90e93ad07196636e1761",
"name": "Ted",
"rollNo": 2,
"class": 12
}
}
]
}
You really do not need the "async" module, but I am just "in the habit" as it were. It doesn't "block" so therefore I consider it better.
So as you can see, you initial .populate() call does not do anything as it expects to "key" off of an _id value in the foreign collection from an array input which this "strictly speaking" is not so as the "key" is on "student" containing the "foreign key".
I really did cover this in a recent answer here, maybe not exactly specific to your situation. It seems that your search did not turn up the correct "same answer" ( though not exactly ) for you to draw reference from.
You are overthinking it. Let Mongoose do the work for you.
Team.findOne({
_id: req.team._id
})
.populate({path:'students'})
.exec(function(err, team) {
console.log(team);
});
This will return students as documents rather than just the ids.
TL DR
const team = await Team.findById(req.team._id)
.populate("students");
team.students = await Student.populate(team.students, {path: "student"});
Context
Reading from all the answers I went testing everything and just Neil Lun's answer worked for me. The problem is it was on the path to a cb hell. So I cracked my head a little and 'refactored' to an elegant one-liner.
const foundPost = await Post.findById(req.params.id)
.populate("comments")
.populate("author");
foundPost.comments = await User.populate(foundPost.comments, {path: "author"});
My initial problem:
{
title: "Hello World",
description: "lorem",
author: {/* populated */},
comments: [ // populated
{text: "hi", author: {/* not populated */ }}
]
};
How my models basically are:
User = {
author,
password
};
Post = {
title,
description,
author: {}, //ref User
comments: [] // ref Comment
};
Comment = {
text,
author: {} // ref User
};
The output after problem solved:
{
comments: [
{
_id: "5dfe3dada7f3570b60dd977f",
text: "hi",
author: {_id: "5df2f84d4d9fcb228cd1df42", username: "jo", password: "123"}
}
],
_id: "5da3cfff50cf094c68aa2a37",
title: "Hello World",
description: "lorem",
author: {
_id: "5df2f84d4d9fcb228cd1aef6",
username: "la",
password: "abc"
}
};