Mongoose Model populate callback is not called when there are references - javascript

I am trying to use Mongoose's Model.populate() to convert a user ID into a User, within a substructure of a document which has been fetched through an aggregation and an unwind. I'm guessing there's either something wrong with my schema or the perhaps unwind is breaking the connection to the sub-schema.
The problem is: when there is a valid reference, the populate callback simply isn't called. When there is no substructure the callback is called, with the original document unchanged.
Structure:
I have Articles which can have none or many ArticleRatings each by a User.
I'm using two references in the ArticleRating, to relate to the Article and the User who made the rating.
Process:
The process is actually exporting the articles to a (legacy) CSV format and flattening the structure, into duplicate article rows with their user-specific ratings. Unwind is perfect for this operation, and preserve nulls retains articles with no ratings.
Debugging:
I've tried stepping deep into the Model.populate code. It gets pretty complex with all the promises and callback wrappers, but I can see that the underlying populate call isn't calling the internal callbacks either. I'm not using the promise variant - but not 100% sure if I should be? (the Mongoose docs are a bit vague on the use-cases between callbacks and promises).
I've double checked my schema, tried explicitly adding the model to the populate call (which shouldn't be needed as it's in the schema). There are no errors or exceptions, it doesn't crash.
Stepping through the code in the Chrome debugger shows the model just as I'd expect: the first few Articles have a rating.userId with a valid ObjectId, but in those cases the populate callback simply doesn't get called. The next hundred Articles have no "rating" set, and the callback gets called reliably for all of them.
So I'm guessing something I'm doing wrong is leading Model.populate down a path where it's not erroring properly?
Note: I know I could rewrite the code to use aggregate $lookup or other embedding structures rather than the foreign reference, but I'm at the final piece of a feature jigsaw and would like to get this working as-is.
This is the simplified schema:
const ArticleRatingSchema = new Schema({
articleId: {type: Schema.Types.ObjectId, ref:'Article'},
userId: {type: Schema.Types.ObjectId, ref:'User'},
rating: String,
comment: String,
});
const ArticleSchema = new Schema({
title: String,
rating: ArticleRatingSchema,
});
And this is the lookup
// Find all articles relating to this project, and their ratings.
// Unwind does the duplicate per-user, and preserve keeps un-rated articles.
articleModel.aggregate([
{$match: {projectId: projectId}},
{$lookup:{from:'articleratings', localField:'_id', foreignField:'articleId', as:'rating' }},
{$unwind: {path:'$rating', preserveNullAndEmptyArrays:true}}
], (err, models) =>
{
if (!err) {
models.map((article) => {
articleModel.populate(article, {path:'rating.userId', model:'User'}, (err, article)=> {
// Process the article...
// this callback only gets called where there is NO rating in the article.
});
});
}

I realised it's because I'm processing the set in a synchronous map() loop, and the two cases must be different in that the non-matching populate is a synchronous callback whereas the matching replacements callback later.
If I console.log() in the callback, I find that the the four matching cases are processed last after the CSV has already been formatted and downloaded.
So the answer is: populate IS being called, but asynchronously.
I need to rework the map() loop to accomodate the usual async pattern.

I'm personally more than a bit baffled by where you think you are going with using $lookup in an aggregation pipeline and then wanting .populate() the results. Because asking to use .populate() essentially means that additional queries are being issued to the server in order to "emulate a join".
Therefore since $lookup actually does the "join on the server" then you really should be just using $lookup for this.
You can use .populate() and I'll show some code to show that it can be done. But it really is redundant here since you may as well just do all the work on the server.
So my best "approximation" of what you seem to have as a structure is:
articles
{
"_id" : ObjectId("5962104312246235cdcceb16"),
"title" : "New News",
"ratings" : [ ],
"__v" : 0
}
articleratings
{
"_id" : ObjectId("5962104312246235cdcceb17"),
"articleId" : ObjectId("5962104312246235cdcceb16"),
"userId" : ObjectId("5962104312246235cdcceb13"),
"rating" : "5",
"comment" : "Great!",
"__v" : 0
}
{
"_id" : ObjectId("5962104312246235cdcceb18"),
"articleId" : ObjectId("5962104312246235cdcceb16"),
"userId" : ObjectId("5962104312246235cdcceb14"),
"rating" : "3",
"comment" : "Okay I guess ;)",
"__v" : 0
}
{
"_id" : ObjectId("5962104312246235cdcceb19"),
"articleId" : ObjectId("5962104312246235cdcceb16"),
"userId" : ObjectId("5962104312246235cdcceb15"),
"rating" : "1",
"comment" : "Hated it :<",
"__v" : 0
}
users
{
"_id" : ObjectId("5962104312246235cdcceb13"),
"name" : "Bill",
"email" : "bill#example.com",
"__v" : 0
}
{
"_id" : ObjectId("5962104312246235cdcceb14"),
"name" : "Fred",
"email" : "fred#example.com",
"__v" : 0
}
{
"_id" : ObjectId("5962104312246235cdcceb15"),
"name" : "Ted",
"email" : "ted#example.com",
"__v" : 0
}
And then the aggregate statement:
Article.aggregate(
[
{ "$lookup": {
"from": ArticleRating.collection.name,
"localField": "_id",
"foreignField": "articleId",
"as": "ratings"
}},
{ "$unwind": "$ratings" },
{ "$lookup": {
"from": User.collection.name,
"localField": "ratings.userId",
"foreignField": "_id",
"as": "ratings.userId",
}},
{ "$unwind": "$ratings.userId" },
{ "$group": {
"_id": "$_id",
"title": { "$first": "$title" },
"ratings": { "$push": "$ratings" }
}}
],
(err,articles) => {
if (err) callback(err);
log(articles);
callback();
}
)
With the result:
{
"_id": "5962126f3ef2fb35efeefd94",
"title": "New News",
"ratings": [
{
"_id": "5962126f3ef2fb35efeefd95",
"articleId": "5962126f3ef2fb35efeefd94",
"userId": {
"_id": "5962126f3ef2fb35efeefd91",
"name": "Bill",
"email": "bill#example.com",
"__v": 0
},
"rating": "5",
"comment": "Great!",
"__v": 0
},
{
"_id": "5962126f3ef2fb35efeefd96",
"articleId": "5962126f3ef2fb35efeefd94",
"userId": {
"_id": "5962126f3ef2fb35efeefd92",
"name": "Fred",
"email": "fred#example.com",
"__v": 0
},
"rating": "3",
"comment": "Okay I guess ;)",
"__v": 0
},
{
"_id": "5962126f3ef2fb35efeefd97",
"articleId": "5962126f3ef2fb35efeefd94",
"userId": {
"_id": "5962126f3ef2fb35efeefd93",
"name": "Ted",
"email": "ted#example.com",
"__v": 0
},
"rating": "1",
"comment": "Hated it :<",
"__v": 0
}
]
}
Where it does not make sense to "populate" the references to the "articleId" on the "ratings" themselves. But we have indeed "populated" the "ratings" to the article, and the "user" for each rating.
Example Listing
Shows it both ways, using .populate() ( after $lookup ) like you are trying, and also just using the plain $lookup.
Methods use both "plain promises" and alternate with async.map:
const async = require('async'),
mongoose = require('mongoose'),
Schema = mongoose.Schema;
mongoose.Promise = global.Promise;
mongoose.set('debug',true);
mongoose.connect('mongodb://localhost/publication');
const userSchema = new Schema({
name: String,
email: String
});
const User = mongoose.model('User', userSchema);
const articleRatingSchema = new Schema({
articleId: {type: Schema.Types.ObjectId, ref:'Article'},
userId: {type: Schema.Types.ObjectId, ref:'User'},
rating: String,
comment: String,
});
const articleSchema = new Schema({
title: String,
ratings: [articleRatingSchema]
})
const Article = mongoose.model('Article', articleSchema);
const ArticleRating = mongoose.model('ArticleRating', articleRatingSchema);
function log(data) {
console.log(JSON.stringify(data,undefined,2))
}
const userData = [
{ name: 'Bill', rating: 5, comment: 'Great!' },
{ name: 'Fred', rating: 3, comment: 'Okay I guess ;)' },
{ name: 'Ted', rating: 1, comment: 'Hated it :<' }
];
async.series(
[
// Clean data
(callback) =>
async.each(mongoose.models,(model,callback) =>
model.remove({},callback),callback),
// Insert data
(callback) =>
async.waterfall(
[
// User and article
(callback) =>
async.parallel(
{
"users": (callback) =>
User.create(
["Bill", "Fred", "Ted"].map( name =>
({ name, email: `${name.toLowerCase()}#example.com` })
),
callback
),
"article": (callback) =>
Article.create({ title: "New News" },callback)
},
callback
),
// Article Ratings
(data,callback) =>
ArticleRating.create(
data.users.map( u => ({
articleId: data.article._id,
userId: u._id,
rating: userData.find( ud => ud.name === u.name ).rating,
comment: userData.find( ud => ud.name === u.name ).comment
})),
callback
)
],
callback
),
// $lookup and populate async.map
(callback) =>
Article.aggregate(
[
{ "$lookup": {
"from": ArticleRating.collection.name,
"localField": "_id",
"foreignField": "articleId",
"as": "ratings"
}}
],
(err,articles) => {
if (err) callback(err);
async.map(
articles.map( a => new Article(a) ),
(article,callback) =>
async.map(
article.ratings,
(rating,callback) =>
ArticleRating.populate(rating,{ path: 'userId' },callback),
(err,ratings) => {
if (err) callback(err);
article.ratings = ratings
callback(null,article)
}
),
(err,articles) => {
if (err) callback(err);
log(articles);
callback();
}
)
}
),
// $look and populate Promise
(callback) =>
Article.aggregate(
[
{ "$lookup": {
"from": ArticleRating.collection.name,
"localField": "_id",
"foreignField": "articleId",
"as": "ratings"
}}
]
)
.then(articles =>
Promise.all(
articles.map( a => new Article(a) ).map(article =>
new Promise((resolve,reject) => {
Promise.all(
article.ratings.map( rating =>
ArticleRating.populate(rating,{ path: 'userId' })
)
).then(ratings => {
article.ratings = ratings;
resolve(article);
}).catch(reject)
})
)
)
)
.then(articles => {
log(articles);
callback();
})
.catch(err => callback(err)),
// Plain $lookup
(callback) =>
Article.aggregate(
[
{ "$lookup": {
"from": ArticleRating.collection.name,
"localField": "_id",
"foreignField": "articleId",
"as": "ratings"
}},
{ "$unwind": "$ratings" },
{ "$lookup": {
"from": User.collection.name,
"localField": "ratings.userId",
"foreignField": "_id",
"as": "ratings.userId",
}},
{ "$unwind": "$ratings.userId" },
{ "$group": {
"_id": "$_id",
"title": { "$first": "$title" },
"ratings": { "$push": "$ratings" }
}}
],
(err,articles) => {
if (err) callback(err);
log(articles);
callback();
}
)
],
(err) => {
if (err) throw err;
mongoose.disconnect();
}
);
Full Output
Mongoose: users.remove({}, {})
Mongoose: articles.remove({}, {})
Mongoose: articleratings.remove({}, {})
Mongoose: users.insert({ name: 'Bill', email: 'bill#example.com', _id: ObjectId("596219ff6f73ed36d868ed40"), __v: 0 })
Mongoose: users.insert({ name: 'Fred', email: 'fred#example.com', _id: ObjectId("596219ff6f73ed36d868ed41"), __v: 0 })
Mongoose: users.insert({ name: 'Ted', email: 'ted#example.com', _id: ObjectId("596219ff6f73ed36d868ed42"), __v: 0 })
Mongoose: articles.insert({ title: 'New News', _id: ObjectId("596219ff6f73ed36d868ed43"), ratings: [], __v: 0 })
Mongoose: articleratings.insert({ articleId: ObjectId("596219ff6f73ed36d868ed43"), userId: ObjectId("596219ff6f73ed36d868ed40"), rating: '5', comment: 'Great!', _id: ObjectId("596219ff6f73ed36d868ed44"), __v: 0 })
Mongoose: articleratings.insert({ articleId: ObjectId("596219ff6f73ed36d868ed43"), userId: ObjectId("596219ff6f73ed36d868ed41"), rating: '3', comment: 'Okay I guess ;)', _id: ObjectId("596219ff6f73ed36d868ed45"), __v: 0 })
Mongoose: articleratings.insert({ articleId: ObjectId("596219ff6f73ed36d868ed43"), userId: ObjectId("596219ff6f73ed36d868ed42"), rating: '1', comment: 'Hated it :<', _id: ObjectId("596219ff6f73ed36d868ed46"), __v: 0 })
Mongoose: articles.aggregate([ { '$lookup': { from: 'articleratings', localField: '_id', foreignField: 'articleId', as: 'ratings' } } ], {})
Mongoose: users.find({ _id: { '$in': [ ObjectId("596219ff6f73ed36d868ed40") ] } }, { fields: {} })
Mongoose: users.find({ _id: { '$in': [ ObjectId("596219ff6f73ed36d868ed41") ] } }, { fields: {} })
Mongoose: users.find({ _id: { '$in': [ ObjectId("596219ff6f73ed36d868ed42") ] } }, { fields: {} })
[
{
"_id": "596219ff6f73ed36d868ed43",
"title": "New News",
"__v": 0,
"ratings": [
{
"_id": "596219ff6f73ed36d868ed44",
"articleId": "596219ff6f73ed36d868ed43",
"userId": {
"_id": "596219ff6f73ed36d868ed40",
"name": "Bill",
"email": "bill#example.com",
"__v": 0
},
"rating": "5",
"comment": "Great!",
"__v": 0
},
{
"_id": "596219ff6f73ed36d868ed45",
"articleId": "596219ff6f73ed36d868ed43",
"userId": {
"_id": "596219ff6f73ed36d868ed41",
"name": "Fred",
"email": "fred#example.com",
"__v": 0
},
"rating": "3",
"comment": "Okay I guess ;)",
"__v": 0
},
{
"_id": "596219ff6f73ed36d868ed46",
"articleId": "596219ff6f73ed36d868ed43",
"userId": {
"_id": "596219ff6f73ed36d868ed42",
"name": "Ted",
"email": "ted#example.com",
"__v": 0
},
"rating": "1",
"comment": "Hated it :<",
"__v": 0
}
]
}
]
Mongoose: articles.aggregate([ { '$lookup': { from: 'articleratings', localField: '_id', foreignField: 'articleId', as: 'ratings' } } ], {})
Mongoose: users.find({ _id: { '$in': [ ObjectId("596219ff6f73ed36d868ed40") ] } }, { fields: {} })
Mongoose: users.find({ _id: { '$in': [ ObjectId("596219ff6f73ed36d868ed41") ] } }, { fields: {} })
Mongoose: users.find({ _id: { '$in': [ ObjectId("596219ff6f73ed36d868ed42") ] } }, { fields: {} })
[
{
"_id": "596219ff6f73ed36d868ed43",
"title": "New News",
"__v": 0,
"ratings": [
{
"_id": "596219ff6f73ed36d868ed44",
"articleId": "596219ff6f73ed36d868ed43",
"userId": {
"_id": "596219ff6f73ed36d868ed40",
"name": "Bill",
"email": "bill#example.com",
"__v": 0
},
"rating": "5",
"comment": "Great!",
"__v": 0
},
{
"_id": "596219ff6f73ed36d868ed45",
"articleId": "596219ff6f73ed36d868ed43",
"userId": {
"_id": "596219ff6f73ed36d868ed41",
"name": "Fred",
"email": "fred#example.com",
"__v": 0
},
"rating": "3",
"comment": "Okay I guess ;)",
"__v": 0
},
{
"_id": "596219ff6f73ed36d868ed46",
"articleId": "596219ff6f73ed36d868ed43",
"userId": {
"_id": "596219ff6f73ed36d868ed42",
"name": "Ted",
"email": "ted#example.com",
"__v": 0
},
"rating": "1",
"comment": "Hated it :<",
"__v": 0
}
]
}
]
Mongoose: articles.aggregate([ { '$lookup': { from: 'articleratings', localField: '_id', foreignField: 'articleId', as: 'ratings' } }, { '$unwind': '$ratings' }, { '$lookup': { from: 'users', localField: 'ratings.userId', foreignField: '_id', as: 'ratings.userId' } }, { '$unwind': '$ratings.userId' }, { '$group': { _id: '$_id', title: { '$first': '$title' }, ratings: { '$push': '$ratings' } } } ], {})
[
{
"_id": "596219ff6f73ed36d868ed43",
"title": "New News",
"ratings": [
{
"_id": "596219ff6f73ed36d868ed44",
"articleId": "596219ff6f73ed36d868ed43",
"userId": {
"_id": "596219ff6f73ed36d868ed40",
"name": "Bill",
"email": "bill#example.com",
"__v": 0
},
"rating": "5",
"comment": "Great!",
"__v": 0
},
{
"_id": "596219ff6f73ed36d868ed45",
"articleId": "596219ff6f73ed36d868ed43",
"userId": {
"_id": "596219ff6f73ed36d868ed41",
"name": "Fred",
"email": "fred#example.com",
"__v": 0
},
"rating": "3",
"comment": "Okay I guess ;)",
"__v": 0
},
{
"_id": "596219ff6f73ed36d868ed46",
"articleId": "596219ff6f73ed36d868ed43",
"userId": {
"_id": "596219ff6f73ed36d868ed42",
"name": "Ted",
"email": "ted#example.com",
"__v": 0
},
"rating": "1",
"comment": "Hated it :<",
"__v": 0
}
]
}
]

Related

How to update nested object in array (Mongoose/MongoDB)

I've been struggling to get my around how to update a object in a nested array with a particular id. I've attempted to implement $set as shown below. I want to be able to update the task with an _id of 62ff74bfe80b11ade2d34455 with the data from the request body.
{
"_id": "62fa5aa25778ec97bc6ee231",
"user": "62f0eb5ebebd0f236abcaf9d",
"name": "Marketing Plan",
"columns": [
{
"name": "todo",
"_id": "62fa5aa25778ec97bc6ee233",
"tasks": [
{ ====> here
"title": "Task Four",
"description": "This is task four",
"subtasks": [
{
"name": "wash dshes",
"completed": false,
"_id": "62ff74bfe80b11ade2d34456"
},
{
"name": "do homework",
"completed": false,
"_id": "62ff74bfe80b11ade2d34457"
}
],
"_id": "62ff74bfe80b11ade2d34455"
}
]
},
{
"name": "doing",
"_id": "62fa5aa25778ec97bc6ee234",
"tasks": []
},
{
"name": "done",
"_id": "62fa5aa25778ec97bc6ee235",
"tasks": []
}
],
"__v": 0
}
const updatedTask = await Board.findOneAndUpdate(
{
"columns.tasks._id": req.params.id,
},
{ $set: { "columns.$.tasks": req.body } },
{ new: true }
);
You can use the positional operator in combination with an arrayfilter. Here's an example how you'd update a specific field of the relevant task:
db.collection.update({
"columns.tasks._id": req.params.id
},
{
"$set": {
"columns.$[].tasks.$[t].title": "it works"
}
},
{
"arrayFilters": [
{
"t._id": req.params.id
}
]
})
You can also try this on mongoplayground.
If you're looking for a way to replace the matching task object itself you can do:
db.collection.update({
"columns.tasks._id": req.params.id
},
{
"$set": {
"columns.$[].tasks.$[t]": req.body
}
},
{
"arrayFilters": [
{
"t._id": req.params.id
}
]
})

How to pull from array of objects with array of objects in MongoDB?

I was trying to pull multiple objects from an array of objects, and I found this article
Using MongoDB $pull to delete documents within an Array
so here's my schema and how I did,but it does'nt work
{
"_id": "61fed9b89763c1c3b886b74f",
"TeamName": "Hogwarts",
"TeamImage": "Avatar ",
"createdAt": "2022-02-05T20:10:32.885Z",
"updatedAt": "2022-02-05T20:59:51.359Z",
"__v": 0,
"TeamMember": [
{
"Name": "Ronne",
"Email": "test4#gmail.com",
"_id": "61fee1807df64451141f08df"
},
{
"Name": "Kai",
"Email": "school021195#gmail.com",
"_id": "61fee1e3fffd0f55ed92caee"
},
{
"Name": "Selina",
"Email": "test#gmail.com",
"_id": "61fee1e3fffd0f55ed92caef"
},
{
"Name": "Jessica Wu",
"Email": "test1#gmail.com",
"_id": "61fee1e3fffd0f55ed92caf0"
},
{
"Name": "Hormione",
"Email": "test3#gmail.com",
"_id": "61fee1807df64451141f08de"
}
]
},
Delete method
const team = await Team.findOneAndUpdate(
{_id: TeamId},
{
$pull: { TeamMember: [ {Name: "Ronne" },{ Name : "Hormione"} ] },
// $pull: { TeamMember: {$in:[ {Name: "Ronne" },{ Name : "Hormione"} ]}},
},
{ new: true,multi: true}
);
I also try the $in method, but both don't work, did I miss something? or what's the right way to do multiple pulls using MongoDB?
BTW, I am using Mongoose for my schema, I don't know if it matters.
Try using this way
const team = await Team.findOneAndUpdate(
{_id: TeamId},
{
$pull: { TeamMember: {Name: {$in: ["Ronne","Hormione"] } } },
},
{ new: true,multi: true}
);
https://mongoplayground.net/p/Sqo83w8YvVU
DO NOT Include [ ] Brackets in $pull. It will not work
const team = await Team.findOneAndUpdate(
{_id: TeamId},
{
$pull: { TeamMember: {Name: "Ronne} },
},
{ new: true,multi: true}
);

How to list unique values ​in a MongoDB collection if the values ​are arrays of objects?

I need to output a list of unique values in a collection. I've used the distinct method when the values are either a string or a number. But in this situation, the values are an array of objects.
The simplified model looks like this:
const mongoose = require('mongoose');
const ItemModel = mongoose.Schema({
category: [{
lang: String,
text: String
}],
discount: {
type: String,
required: true
},
date: {
type: Date,
default: Date.now
}
});
module.exports = mongoose.model('Items', ItemModel);
A typical find() query without parameters produces this result:
[
{
"_id": "5fd8712b374a9a1410f786bf",
"category": [
{
"_id": "5fd8712b374a9a1410f786c2",
"lang": "RU",
"text": "Домашняя одежда"
},
{
"_id": "5fd8712b374a9a1410f786c3",
"lang": "EN",
"text": "Homewear"
}
],
"discount": "45%",
"date": "2020-12-12T11:12:37.811Z",
"__v": 0
},
{
"_id": "5fd4a5b95e1a251ac96b2e08",
"category": [
{
"_id": "5fd4a5b95e1a251ac96b2e0b",
"lang": "RU",
"text": "Домашняя одежда"
},
{
"_id": "5fd4a5b95e1a251ac96b2e0c",
"lang": "EN",
"text": "Homewear"
}
],
"discount": "35%",
"date": "2020-12-12T11:12:37.811Z",
"__v": 0
},
{
"_id": "5fd49e415e1a251ac96b2dfc",
"category": [
{
"_id": "5fd49e415e1a251ac96b2dff",
"lang": "RU",
"text": "Активный отдых"
},
{
"_id": "5fd49e415e1a251ac96b2e00",
"lang": "EN",
"text": "Active"
}
],
"discount": "50%",
"date": "2020-12-12T10:06:53.120Z",
"__v": 0
}
]
I need to output a list of unique "category.text" values where "lang" equals "EN". At the output, we should get an array like this:
[ "Active", "Homewear" ]
How to do it the right way, with good performance?
Regards.
You can use aggregation pipeline,
$unwind to deconstruct category array
$match category.lang is EN
$group by null, and get unique text using $addToSet
StockModel.aggregate([
{ $unwind: "$category" },
{ $match: { "category.lang": "EN" } },
{
$group: {
_id: null,
test: { $addToSet: "$category.text" }
}
}
])
Playground
use aggregation :
take a look at the example below :
StockModel.aggregate([
{
$group: {
_id: 0,
title: { $addToSet: '$title' },
stock_id: { $addToSet: '$stock_id' },
},
},
]);
with aggregation $group you can distinct your collections via filter.

How to findOneAndUpdate single field with multi nested array documents

I'm stuck on how to update single value in multi nested array documents value with findOneAndUpdate.
My condition goes like this:
Update warehouse amount where the productCode is "abc123", size "41" in warehouse "Hamburg".
I just get back null or bot sizes 41 and 42.
Here is the part of the doc:
{
"_id": ObjectId("xxxx636309f84479ec0c7b"),
"productCode": "abc123",
"brand": "Nike",
"name": "aaa",
"model": "Runner",
"color": "Brown",
"image": "shoe.jpg",
"sizes": [{
"_id": ObjectId("xxxxc636309f84479ec0c7e"),
"size": "41",
"wares": [{
"_id": ObjectId("xxxx2c636309f84479ec0c80"),
"ware": "Hamburg",
"amount": 7
},
{
"_id": ObjectId("5db72c636309f84479ec0c7f"),
"ware": "Berlin",
"amount": 7
}
]
},
{
"_id": ObjectId("5db72c636309f84479ec0c7c"),
"size": "42",
"wares": [{
"_id": ObjectId("5db72c636309f84479ec0c7d"),
"ware": "Hamburg",
"amount": 16
}]
}
],
"__v": 0
}
This is what I've tried:
Product.findOneAndUpdate({
"productCode": "abc123",
"sizes.size": 41,
"sizes.wares.ware": "Hamburg"
}, {
"$set": {
"sizes.0.wares.amount": 99
}
}, {
useFindAndModify: false
},
(err, products) => {
if (err) {
return res.status(422).send(err)
}
return res.json(products)
}
);
How can I solve this?
And to fulfill #ambianBeing, this is how it would be done with findOneAndUpdate:
Product.findOneAndUpdate({
"productCode": "abc123",
"sizes": {
$elemMatch: {
$and: [
{ size: "41" },
{
wares: {
$elemMatch: {
ware: "Hamburg"
}
}
}]
}
}
}, {
$set: {
"sizes.$[theSize].wares.$[theWare].amount": 99
}
}, {
arrayFilters: [{
"theSize.size": "41"
}, {
"theWare.ware": "Hamburg"
}]
})
Can be done using filtered positional operator $[<identifier>] which is nifty in use cases of nested array updation.
Query (Mongo Shell):
db.collection.update(
{ productCode: "abc123" },
{ $set: { "sizes.$[outer].wares.$[inner].amount": 99 } },
{
arrayFilters: [{ "outer.size": "41" }, { "inner.ware": "Hamburg" }],
multi: false
}
);
Query with Mongoose Model:
Product.update(
{ productCode: "abc123" },
{ "sizes.$[outer].wares.$[inner].amount": 99 },
{
arrayFilters: [{ "outer.size": "41" }, { "inner.ware": "Hamburg" }],
multi: false
},
(err, rawDoc) => {
if (err) {
console.error(err);
}
console.info(rawDoc);
}
);

Mongoose find with multiple matches

I'm new to this technology and working with Node and Express server that uses Mongoose. I have following schema for a document collection.
var empSchema = new mongoose.Schema({
_id: String,
orgName: {type: String, required: true},
locName: {type: String, required: true},
empName: {type: String, required: true}
});
Here I get a list of location names like "NewYork", "London", "Paris" etc... in a request and needs to return the documents in the response as following....
{
result:[{locName:"NewYork",
empList:[
{orgName:"abc", empName:"emp1"},
{orgName:"xyz", empName:"emp2"}]
},
{locName:"London",
empList:[
{orgName:"pkq", empName:"emp13"},
{orgName:"mns", empName:"emp23"}]
}]
}
What would be the best way to use mongoose from Node. I think making multiple queries (each one with a location) to mongodb is a bad idea.
Is there a way to get the expected json response with single call to mongoose? Thanks.
Yes, use the aggregation framework to get the desired output. The aggregation pipeline will consist of a $group operator pipeline stage which groups the documents by the locName field and the $addToSet accumulator operator to add the orgName and empName fields to an array empList. The last pipeline stage $project operator then replaces the _id field from the previous aggregation stream with a new field locName.
To demonstrate this concept, suppose you have a sample collection which you insert with mongo shell:
db.employees.insert([
{
_id: "1",
orgName: "abc",
locName: "New York",
empName: "emp1"
},
{
_id: "2",
orgName: "xyz",
locName: "New York",
empName: "emp2"
},
{
_id: "3",
orgName: "pkq",
locName: "London",
empName: "emp13"
},
{
_id: "4",
orgName: "mns",
locName: "London",
empName: "emp23"
}
])
The following aggregation produces the desired result:
db.employees.aggregate([
{
"$group": {
"_id": "$locName",
"empList": {
"$addToSet": {
"orgName": "$orgName",
"empName": "$empName"
}
}
}
},
{
"$project": {
"_id": 0,
"locName": "$_id",
"empList": 1
}
}
])
Output:
/* 0 */
{
"result" : [
{
"empList" : [
{
"orgName" : "mns",
"empName" : "emp23"
},
{
"orgName" : "pkq",
"empName" : "emp13"
}
],
"locName" : "London"
},
{
"empList" : [
{
"orgName" : "xyz",
"empName" : "emp2"
},
{
"orgName" : "abc",
"empName" : "emp1"
}
],
"locName" : "New York"
}
],
"ok" : 1
}
In Mongoose, you can use the aggregation pipeline builder like this:
Employee.aggregate()
.group({
"_id": "$locName",
"empList": {
"$addToSet": {
"orgName": "$orgName",
"empName": "$empName"
}
}
})
.project({
"_id": 0,
"locName": "$_id",
"empList": 1
})
.exec(function (err, res) {
if (err) return handleError(err);
console.log(res);
});
// Or the simple aggregate method
var pipeline = [
{
"$group": {
"_id": "$locName",
"empList": {
"$addToSet": {
"orgName": "$orgName",
"empName": "$empName"
}
}
}
},
{
"$project": {
"_id": 0,
"locName": "$_id",
"empList": 1
}
}
]
Employee.aggregate(pipeline, function (err, res) {
if (err) return handleError(err);
console.log(res);
});
All queries, when you need to group by sum values called aggregate. You can read about it in the mongo docs and same methods have model in Mongoose. To produce your query, you can use code like this:
Employee
.aggregate()
.group({ _id: '$locName', empList: { $push: "$$ROOT" }})
.exec(function (err, res) {
});
If you need not to query all table, there is also have a match method.

Categories