How to keep an array member count - node.js

I'm using nodeJS + Express + Mongoose + mongoDB
Here's my mongoDB User Schema:
{
friends: [ObjectId]
friends_count: Number
}
Whenever user adds a friend, a friendId will be pushed into friends array, and friends_count will be increased by 1.
Maybe there are a lot of actions will change the friends array, and maybe I will forgot to increase the friends_count. So I want to make sure that friends_count always equal to friends.length
Is there a good way or framework to make sure all of that?
P.S
I know how to update friends_count. What I mean is what if I forgot to?
Is there a way to automatically keep these two attributes sync?

Use the $ne operator as a "query" argument to .update() and the $inc operator to apply when that "friend" did not exist within the array as you $push the new member:
User.update(
{ "_id": docId, "friends": { "$ne": friendId } },
{
"$push": { "friends": friendId },
"$inc": { "friends_count": 1 }
},
function(err,numberAffected) {
}
)
Or to "remove" a friend from the list, do the reverse case with $pull:
User.update(
{ "_id": docId, "friends": friendId },
{
"$pull": { "friends": friendId },
"$inc": { "friends_count": -1 }
},
function(err,numberAffected) {
}
)
That way your friends_count stays in sync with the number of array elements present.

All you need to do is to update friends_count in both add and remove functions. For example:
User.findById(userId, function (err, user) {
if (user) {
user.friends.push(friendId);
user.friends_count++;
user.save();
}
});
FYI, I don't think it is necessary to add friends_count while you can get total numbers of friends by friends.length.

Related

Mongoose findByIdAndUpdate() with select option [duplicate]

Using Mongoose in Nodejs you can return some fields using find.
eg.
User.findOne({_id:'132324'}, {first_name:1, last_name:1}).exec...
but I can't seem to figure out how to return certain fields using findOneAndUpdate.
User.findOneAndUpdate({_id:'132324'}, {$set : {..bla bla}}, {first_name:1, last_name:1}).exec....
Has anyone achieved this before?
I can't find it in the documentation.
From the manual, the options argument needs a "fields" key in it since there are other details such as "upsert" and "new" where this applies. In your case you also want the "new" option:
User.findOneAndUpdate(
{ "_id": "132324" },
{ "$set": { "hair_color": "yellow" } },
{
"fields": { "first_name":1, "last_name": 1 },
"new": true
}
).exec(...)
Alternately you may use .select()
User.select({ "first_name": 1, "last_name": 1 }).findOneAndUpdate(
{ "_id": "132324" },
{ "$set": { "hair_color": "yellow" } },
{ "new": true }
).exec(...)
Noting that without "new": true the document returned is in the state before the modification of the update was processed. Some times this is what you mean, but most of the time you really want the modified document.
It seems the syntax for findByIdAndUpdate has changed a little.
Its takes the form of Model.findByIdAndUpdate(query, update, options, (optional callback))
According to the docs, to specify which fields are returned you have to specify them in the options parameter. so, using the above example it would be:
User.findOneAndUpdate(
{id}, //query
{ $set: { "fieldToBeChanged": "update" } }, //update
{new:true, select: "fieldIWant anotherFieldIWant"}, //options
})
The new:true options is not necessary. If omitted mongoose defaults to returning the document before the updates were applied. Set to true and it will return the document after the updates have been made.
We can exclude any field while using mongoose update function findByIdAndUpdate with the help of select function,please have a look at the following code it will exclude password and __v from the Update response
User.findByIdAndUpdate({ _id: req.userData.UserId },req.body,{new: true}).select({Password: 0, __v: 0 })
.exec()
.then(result => {})
Try this:
User.findOneAndUpdate({_id:'132324'}, {update: '1235'}, {new: true}).select({ _id: 0 })
The select() will exclude the _id field from the result.
For MongoDb version 3.2+
User.findOneAndUpdate(
{ "_id": "132324" },
{ "$set": { "hair_color": "yellow" } },
{
"projected": { "first_name":1, "last_name": 1 },
"returnNewDocument": true
}
)...
For MongoDB version: 4.0.2, mongoose version: 5.13.7:
userModel.findOneAndUpdate({_id:'132324'},{$set:
{"first_name":"user","last_name":"one"}},
{new:true,select:{first_name:1, last_name:1}},(err,data)=>{}
Use option {new: true} in findOneAndUpdate (default: false)

$push and $set same sub-document in an Array

I'm trying to keep an history of states in a subdocument array with mongoosejs 4.9.5 and mongo 3.2.7
Example of document structure:
company (Schema)
employees (Schema): [ ]
currentState: String
states (Schema): [ ]
state: String
starts: Date
ends: Date
When I change the employee state, I want to change the currentState, add the new state into the states array, and update the last state for define the 'ends' timestamp
// I get the last state position from a previous find request
var lastStateIndex = employee.stateHistory.length - 1;
var changeStateDate = new Date();
// Prepare the update
var query = { _id: companyId, "employees._id": employeeId };
var update = {
$set: {
"employees.$.state": newState,
`employees.$.stateHistory.${lastStateIndex}.ends`: changeStateDate
},
$push: {
"employees.$.stateHistory": {
state: newState,
starts: changeStateDate
}
}
}
Company.findOneAndUpdate(query, update, { multi:false, new:true}, ... )
Mongo is returning the following error
{"name":"MongoError","message":"Cannot update 'employees.0.stateHistory.0.ends' and 'employees.0.stateHistory' at the same time","ok":0,"errmsg":"Cannot update 'employees.0.stateHistory.0.ends' and 'employees.0.stateHistory' at the same time","code":16837}
Any suggestions how to avoid running two updates for that purpose?
Any work around for avoid storing the 'ends' date, but being able to calculate it after based on the 'starts' of the next item in the array?
Thank you,
I expected this to already be answered elsewhere, but no other reasonable response seems to exist. As commented, you cannot actually do this in a single update operation because the operations "conflict" on the same path. But .bulkWrite() allows "multiple updates" to be applied in a single request and response.
Company.bulkWrite([
{ "updateOne": {
"filter": { "_id": companyId, "employees._id": employeeId },
"update": {
"$set": {
"employees.$.state": newState,
[`employees.$.stateHistory.${lastStateIndex}.ends`]: changeStateDate
}
}},
{ "updateOne": {
"filter": { "_id": companyId, "employees._id": employeeId },
"update": {
"$push": {
"employees.$.stateHistory": {
"state": newState,
"starts": changeStateDate
}
}
}
}}
])
Now of course .bulkWrite() does not return the "modified document" like .findOneAndUpdate() does. So if you need to actually return the document, then you need to add to the Promise chain instead:
Company.bulkWrite([
{ "updateOne": {
"filter": { "_id": companyId, "employees._id": employeeId },
"update": {
"$set": {
"employees.$.state": newState,
[`employees.$.stateHistory.${lastStateIndex}.ends`]: changeStateDate
}
}},
{ "updateOne": {
"filter": { "_id": companyId, "employees._id": employeeId },
"update": {
"$push": {
"employees.$.stateHistory": {
"state": newState,
"starts": changeStateDate
}
}
}
}}
]).then( result => {
// maybe inspect the result
return Company.findById(companyId);
})
Of course noting that it is "possible" that another modification can be made to the document in between when the .bulkWrite() is applied and the .findById() is executed. But that is the cost of the operation you are doing.
It is generally best to consider if you actually need the returned document or not. In most instances you simply already have the information and any "updates" you should be aware of because you are "issuing them", and if you want "truly reactive" then you should be listening for other change events on the data through a socket instead.
Note you could simply "chain" the "multiple" .findOneAndUpdate() calls, but this is indeed "multiple" calls and responses from the server, as opposed to the one using .bulkWrite(). So there really isn't anything to gain by doing otherwise.

mongodb remove document if array count zero after $pull in a single query

I have a requirement where my comments schema looks like the following
{
"_id": 1,
"comments": [
{ "userId": "123", "comment": "nice" },
{ "userId": "124", "comment": "super"}
]
}
I would like to pull the elements based on the userId field.
I am doing the following query
comments.update({},{$pull:{comments:{userId:"123"}}})
My requirement is that if the array length became zero after the pull operator I need to remove the entire document for some reason.Is there a away to do this in a single query?
PS:I am using the mongodb driver.Not the mongoose
If I'm reading your question right, after the $pull, if the comments array is empty (zero length), then remove the document ({ _id: '', comments: [] }).
This should remove all documents where the comments array exists and is empty:
comments.remove({ comments: { $exists: true, $size: 0 } })
I had a similar requirement and used this (using mongoose though):
await Attributes.update({}, { $pull: { values: { id: { $in: valueIds } } } }, { multi: true })
await Attributes.remove({ values: { $exists: true, $size: 0 } })
Not sure if it's possible to do this in one operation or not.
You can use middlewares for this.
http://mongoosejs.com/docs/middleware.html
Write a pre/post update method in mongodb to check your condition.

Mongoose select fields to return from findOneAndUpdate

Using Mongoose in Nodejs you can return some fields using find.
eg.
User.findOne({_id:'132324'}, {first_name:1, last_name:1}).exec...
but I can't seem to figure out how to return certain fields using findOneAndUpdate.
User.findOneAndUpdate({_id:'132324'}, {$set : {..bla bla}}, {first_name:1, last_name:1}).exec....
Has anyone achieved this before?
I can't find it in the documentation.
From the manual, the options argument needs a "fields" key in it since there are other details such as "upsert" and "new" where this applies. In your case you also want the "new" option:
User.findOneAndUpdate(
{ "_id": "132324" },
{ "$set": { "hair_color": "yellow" } },
{
"fields": { "first_name":1, "last_name": 1 },
"new": true
}
).exec(...)
Alternately you may use .select()
User.select({ "first_name": 1, "last_name": 1 }).findOneAndUpdate(
{ "_id": "132324" },
{ "$set": { "hair_color": "yellow" } },
{ "new": true }
).exec(...)
Noting that without "new": true the document returned is in the state before the modification of the update was processed. Some times this is what you mean, but most of the time you really want the modified document.
It seems the syntax for findByIdAndUpdate has changed a little.
Its takes the form of Model.findByIdAndUpdate(query, update, options, (optional callback))
According to the docs, to specify which fields are returned you have to specify them in the options parameter. so, using the above example it would be:
User.findOneAndUpdate(
{id}, //query
{ $set: { "fieldToBeChanged": "update" } }, //update
{new:true, select: "fieldIWant anotherFieldIWant"}, //options
})
The new:true options is not necessary. If omitted mongoose defaults to returning the document before the updates were applied. Set to true and it will return the document after the updates have been made.
We can exclude any field while using mongoose update function findByIdAndUpdate with the help of select function,please have a look at the following code it will exclude password and __v from the Update response
User.findByIdAndUpdate({ _id: req.userData.UserId },req.body,{new: true}).select({Password: 0, __v: 0 })
.exec()
.then(result => {})
Try this:
User.findOneAndUpdate({_id:'132324'}, {update: '1235'}, {new: true}).select({ _id: 0 })
The select() will exclude the _id field from the result.
For MongoDb version 3.2+
User.findOneAndUpdate(
{ "_id": "132324" },
{ "$set": { "hair_color": "yellow" } },
{
"projected": { "first_name":1, "last_name": 1 },
"returnNewDocument": true
}
)...
For MongoDB version: 4.0.2, mongoose version: 5.13.7:
userModel.findOneAndUpdate({_id:'132324'},{$set:
{"first_name":"user","last_name":"one"}},
{new:true,select:{first_name:1, last_name:1}},(err,data)=>{}
Use option {new: true} in findOneAndUpdate (default: false)

Mongoose update first five documents

Hello I am trying to update only first five documents from my schema using mongoose. I found a way to update documents by giving condition but cant update only first five.
I got this code
mongoose.model('person').update( {active:false} , {multi: true} , function(err,docs) { ... });
Thank you
The key here is to get the first 5 _id values matching your condition and then pass those using $in to your update selection:
mongoose.model('person').find({ "active": { "$ne": false }}).limit(5)
.exec(function(err,docs) {
var ids = docs.map(function(doc) { return doc._id } );
mongoose.model('person').update(
{ "_id": { "$in": ids } },
{ "$set": { "active": false } },
{ "multi": true },
function(err,numAffected) {
}
);
});
Also notice the correct arguments to the update statement with a "query" and "update" block. You should also be using update operators such as $set, otherwise you are overwriting the existing document content with just the contents of the "update"block.
Also see the .update() method from the core documentation.

Resources