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.
Related
I have this to query in db :
const hasUsedVideo = await db.SessionCard.findOne({ movie: Card.movie, unit: Card.unit, video: Card.video }, { _id: { $ne: Card._id } }, { _id: 1 } ).lean();
But I get this error:
MongoError: Expression $ne takes exactly 2 arguments. 1 were passed
in.
I don't know how to fix this?
#nimrod serok provides the right guidance in the comments, which I'll copy below for completeness. This answer is just going to outline why you are receiving that error.
The solution to the problem is to include the predicate on _id in the rest of the object that comprises the first argument for the findOne() operation. So instead of:
const hasUsedVideo = await db.SessionCard.findOne(
{ movie: Card.movie, unit: Card.unit, video: Card.video },
{ _id: { $ne: Card._id } },
{ _id: 1 }
).lean();
It should be:
const hasUsedVideo = await db.SessionCard.findOne(
{ movie: Card.movie, unit: Card.unit, video: Card.video, _id: { $ne: Card._id } },
{ _id: 1 }
).lean();
As currently written in the question, the separate second argument (of { _id: { $ne: Card._id } }) is being parsed as the projection argument (instead of the intended projection of { _id: 1 }. The projection argument accepts aggregation expressions and syntax as of version 4.4. This is relevant to the error that you were receiving because there are two different $ne operators in MongoDB:
One is the standard query operator which has the syntax of { field: { $ne: value } }.
The other is an aggregation expression with a syntax of { $ne: [ <expression1>, <expression2> ] }.
This is why your usage of _id: { $ne: Card._id } was triggering the error about $ne requiring 2 arguments, because the aggregation variant of the operation does require that.
Mongoplayground will complain about the syntax in the question stating that a maximum of 2 arguments are expected for find() (in the shell implementation). By contrast, the syntax provided by #nimrod serok executes successfully.
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
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")}}
]);
I have a query where I first want to match find the list of matched users and then filter the matches out from the array of external users that was passed in so that I am left with users Id's that have not been matched yet.
Here is a the match Schema:
const mongoose = require('mongoose'); // only match two users at a time.
const Schema = mongoose.Schema;
const MatchSchema = new Schema({
participants: [{
type: String, ref: 'user'
}],
blocked: {
type: Boolean,
default: false
}
});
Here is the query with explanations:
db.getCollection('match').aggregate([
{
'$match': {
'$and': [
{ participants: "599f14855e9fcf95d0fe11a7" }, // the current user.
{ participants: {'$in': [ "598461fcda5afa9e0d2a8a64","598461fcda5afa9e0d111111", "599f14855e9fcf95d0fe5555"] } } // array of external users that I want to check if the current user is matched with.
]
}
},
{
'$project': {
'participants': 1
}
},
This returns the following result:
{
"_id" : ObjectId("59c0d76e66dd407f5efe7112"),
"participants" : [
"599f14855e9fcf95d0fe11a7",
"599f14855e9fcf95d0fe5555"
]
},
{
"_id" : ObjectId("59c0d76e66dd407f5efe75ac"),
"participants" : [
"598461fcda5afa9e0d2a8a64",
"599f14855e9fcf95d0fe11a7"
]
}
what I want to do next it merge the participants array form both results into one array.
Then I want to take away the those matching items from the array of external users so that I am left with user id's that have not been matched yet.
Any help would be much appreciated!
If you don't want those results in the array, then $filter them out.
Either by building the conditions with $or ( aggregation logical version ):
var currentUser = "599f14855e9fcf95d0fe11a7",
matchingUsers = [
"598461fcda5afa9e0d2a8a64",
"598461fcda5afa9e0d111111",
"599f14855e9fcf95d0fe5555"
],
combined = [currentUser, ...matchingUsers];
db.getCollection('match').aggregate([
{ '$match': {
'participants': { '$eq': currentUser, '$in': matchingUsers }
}},
{ '$project': {
'participants': {
'$filter': {
'input': '$participants',
'as': 'p',
'cond': {
'$not': {
'$or': combined.map(c =>({ '$eq': [ c, '$$p' ] }))
}
}
}
}
}}
])
Or use $in ( again the aggregation version ) if you have MongoDB 3.4 which supports it:
db.getCollection('match').aggregate([
{ '$match': {
'participants': { '$eq': currentUser, '$in': matchingUsers }
}},
{ '$project': {
'participants': {
'$filter': {
'input': '$participants',
'as': 'p',
'cond': {
'$not': {
'$in': ['$p', combined ]
}
}
}
}
}}
])
It really does not matter. It's just the difference of using JavaScript to build the expression before the pipeline is sent or letting a supported pipeline operator do the array comparison where it is actually supported.
Note you can also write the $match a bit more efficiently by using an "implicit" form of $and, as is shown.
Also note you have a problem in your schema definition ( but not related to this particular query ). You cannot use a "ref" to another collection as String in one collection where it is going to be ObjectId ( the default for _id, and presumed of the hex values obtained ) in the other. This mismatch means .populate() or $lookup functions cannot work. So you really should correct the types.
Unrelated to this. But something you need to fix as a priority.
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);
}
}
});
}
);