I'm trying to add a new field in all documents that contain the sum of an array of numbers.
Here is the Schema (removed irrelevant fields for brevity):
var PollSchema = new Schema(
{
votes: [Number]
}
);
I establish the model:
PollModel = mongoose.model('Poll', PollSchema);
And I use aggregation to create a new field that contains the sum of the votes array.
PollModel.aggregate([
{
$project: {
totalVotes: { $sum: "$votes"}
}
}
]);
When I startup my server, I get no errors; however, the totalVotes field hasn't been created. I used this documentation to help me. It similarly uses the $sum operator and I did it exactly like the documentation illustrates, but no results.
MongoDb aggregation doesn't save its result into database. You just get the result of aggregation inline within a callback.
So after aggregation you would need to do multi update to your database:
PollModel.aggregate([
{
$project: { totalVotes: { $sum: "$votes"} }
}]).exec( function(err, docs){
// bulk is used for updating all records within a single query
var bulk = PollModel.collection.initializeUnorderedBulkOp();
// add all update operations to bulk
docs.forEach(function(doc){
bulk.find({_id: doc._id}).update({$set: {totalVotes: doc.totalVotes}});
});
// execute all bulk operations
bulk.execute(function(err) {
});
})
});
Unfortunately this does not work as you think it does because "votes" is actually an array of values to start with, and then secondly because $sum is an accumulator operator for usage in the $group pipeline stage only.
So in order for you to get the total of the array as another property, first you must $unwind the array and then $group together on the document key to $sum the total of the elements:
PostModel.aggregate(
[
{ "$unwind": "$votes" },
{ "$group": {
"_id": "$_id",
"anotherField": { "$first": "$anotherField" },
"totalVotes": { "$sum": "$votes" }
}}
],
function(err,results) {
}
);
Also noting here another accumulator in $first would be necessary for each additional field you want in results as $group and $project only return the fields you ask for.
Generally though this is better to keep as a property within each document for performance reasons, as it's faster than using aggregate. So to do this just increment a total each time you $push to an array by also using $inc:
PostModel.update(
{ "_id": id },
{
"$push": { "votes": 5 },
"$inc": { "totalVotes": 5 }
},
function(err,numAffected) {
}
);
In that way the "totalVotes" field is always ready to use without the overhead of needing to deconstruct the array and sum the values for each document.
You don't have totalVotes in your schema. Just try the below code.
var PollSchema = new Schema(
{
votes: [Number],
totalVotes: Number
}
);
PollModel.aggregate([
{
$project: {
totalVotes: { $sum: "$votes"}
}
}
]);
or
resultData.toJSON();
#Blakes Seven and #Volodymyr Synytskyi helped me arrive to my solution! I also found this documentation particularly helpful.
PollModel.aggregate(
[
{ '$unwind': '$votes' },
{ '$group': {
'_id': '$_id',
'totalVotes': { '$sum': '$votes' }
}}
],
function(err,results) {
// console.log(results);
results.forEach(function(result){
var conditions = { _id: result._id },
update = { totalVotes: result.totalVotes },
options = { multi: true };
PollModel.update(conditions, update, options, callback);
function callback (err, numAffected) {
if(err) {
console.error(err);
return;
} else {
// console.log(numAffected);
}
}
});
}
);
Related
So I have a collection called Cars that have some fields that are the same, but I want to be able to only get one of the documents based on that field.
[
{
_id:'12345',
model:'Honda'
},
{
_id:'12346',
model:'Honda'
},
{
_id:'12347',
model:'Honda'
},
{
_id:'12348',
model:'Toyota'
},
{
_id:'12349',
model:'Volkswagen'
},
{
_id:'12349',
model:'Volkswagen'
},
]
So here, I want to be able to get the distinct document based on the model field. I just want one document per model field.
first, you want to update mongoose v3 or up
the solution is to use the distinct function
Cars.find().distinct('model', function(error, models) {});
I hope it will help you :)
Use $first to pick a document in $group stage. Then do some wrangling with $unwind and $replaceRoot to retrieve the document.
db.collection.aggregate([
{
$group: {
_id: "$model",
doc: {
$first: "$$ROOT"
}
}
},
{
"$unwind": "$doc"
},
{
"$replaceRoot": {
"newRoot": "$doc"
}
}
])
Here is the Mongo Playground for your reference.
I am working on an app that uses MongoDB (I use Mongoose) as its database.
I have a question, suppose I have this kind of schema:
[{
"user_id":"2328292073"
"username":"Bob",
"subscriptions":[
{
"id":"38271281,
"payments":[
{
"id":"00001",
"amount":"1900"
},
{
"id":"00002",
"amount":"2000"
},
{
"id":"00003",
"amount":"3000"
}
]
}
]
}]
In my case I want to get the payments array for subscription with id = '38271281' of user with id '2328292073', but I just want to retrieve the payment array, nothing else
My query is the following:
Mongoose.findOne({
"user_id": "2328292073",
"subscriptions.id": "38271281"
},
{
"subscriptions.payments": 1
})
But I get the entire document of subscriptions. How can i get the payment array only?
you can try using unwind if you want filteration from db only.
Mongoose.aggregate([
{
'$match': {
'user_id': '2328292093'
}
}, {
'$unwind': {
'path': '$subscriptions'
}
}, {
'$match': {
'subscriptions.id': '38271281'
}
}
])
if you will have multiple documents having same subscription id then you have to group it .
using code level filter function can also be one another approach to do this .
You can try aggregation operators in projection in find method or also use aggregation method,
$reduce to iterate loop of subscriptions and check the condition if id matched then return payment array
db.collection.find({
"user_id": "2328292073",
"subscriptions.id": "38271281"
},
{
payments: {
$reduce: {
input: "$subscriptions",
initialValue: [],
in: {
$cond: [
{ $eq: ["$$this.id", "38271281"] },
"$$this.payments",
"$$value"
]
}
}
}
})
Playground
I am trying to perform a conditional update of MongoDB record with the following transaction.
db.collection(SESSIONS_COLLECTION)
.updateOne({_id: ObjectId(id)},
{
$set: {
end: {
$cond: {
if: { $gt: ["$end", latestActionDate] }, then: "$end", else: latestActionDate
}
}
},
$push: {
actions: {
$each: data.map(action => ({
...action,
time: new Date(action.time)
}))
}
}
}
);
But all my efforts are crushed with.
MongoError: The dollar ($) prefixed field '$cond' in 'end.$cond' is not valid for storage.
It is possible that such operations are simply not allowed, but I prefer to think that I'm missing something here.
Issue is with you're mixing up normal update operation with aggregation operators, to use aggregation operators in update you need to wrap your update part in [] to say it's actually an aggregation pipeline.
Starting MongoDB version 4.2 you can update-with-an-aggregation-pipeline :
Code :
let actionArray = data.map((action) => ({
...action,
time: new Date(action.time),
}));
db.collection(SESSIONS_COLLECTION).updateOne({ _id: ObjectId(id) }, [
{
$set: {
end: {
$cond: [{ $gt: ["$end", latestActionDate] }, "$end", latestActionDate]
}
}
},
/** (fail safe stage) this second stage is optional but needed in general as `$push` will add a new array
* if that field doesn't exists earlier to update operation
* but `$concatArray` doesn't, So this helps to add `[]` prior to below step */
{
$set: { actions: { $ifNull: [ "$actions", [] ] } }
},
{
$set: {
actions: { $concatArrays: ["$actions", actionArray] }
}
}
]);
Note :
As we're using aggregation pipeline here, you need to use aggregation operators and usage of few query/update operators doesn't work.
I am having trouble with the "$in" operator in Mongoose. At a high level I have a User schema, and one of the fields is an array of a Card schema. Within the Card schema there is a 'score' field. I would like to update the 'score' field based on a list of Card ids. Here is what I am trying to use:
User.updateMany(
{
"_id": userId,
"cards._id": {
"$in": cardIds
}
},
{ $inc: {"cards.$.score": 1 }},
(err) => {console.log(err)}
)
When I run this code, only the first Card in the cardIds array is updated instead of all of them. Any idea why this isn't working? Thanks.
You need to use arrayFilters in .updateMany() , Try this query :
User.updateMany({
"_id": userId
},
{ $inc: { "cards.$[element].score": 1 } },
{ arrayFilters: [{ "element._id": { "$in": cardIds } }] },
(err) => {}
)
mongodb aggregate "$_id" return id as string not objectID.
and when i send "$_id" of an document to a function, aggregation can't compare objectID and string. query below (return empty):
db.getCollection('general.users').aggregate([
{
$match: { _id: ObjectId("5a48894e4639fa6d7703c4f4") }
},
{
$addFields: { creadit: fetchUserCreadit("$_id") }
},
])
and that is some of my function :
const succPays = db.financial.successpayrequests.aggregate([
{
$match:
{
$and:
[
{
userID: uid
}
]
}
},
{
$group:
{
_id: null,
amountSum: { $sum: '$amount' }
}
}
]).toArray();
return succPays;
when i send param in hardcode every thing is ok becuse param send as object (return not empty array).
db.getCollection('general.users').aggregate([
{
$match: { _id: ObjectId("5a48894e4639fa6d7703c4f4") }
},
{
$addFields: { creadit: fetchUserCreadit(ObjectId("5a48894e4639fa6d7703c4f4")) }
},
The problem is order of execution of enclosed functions. Javascript is calling fetchUserCreadit before it assembles the $addFields expression and passes the fully baked pipeline to db.foo.aggregate(). Javascript sees $_id and just sends that as a string; it has no idea about MongoDB and indeed, at the point this is called, there is not even a pipeline going. Here is a compact but similar demo of the problem.
function myFunc(id) {
print("myFunc: got " + id);
return db.foo.findOne({_id: id});
}
db.foo.aggregate([
{$match: { _id: ObjectId(s_oid1) }}
,{$addFields: {qq: myFunc("$_id")}}
]);