I'm creating a like functionality for my app using MongoDB. I recently stumbled upon a problem.
My code for the liking:
async function like(articleId, userId) {
const db = await makeDb();
return new Promise((resolve, reject) => {
const result = db.collection(collectionName).findOneAndUpdate(
{ articleId: ObjectID(articleId) },
{
$setOnInsert: {
articleId: ObjectID(articleId),
creationDate: new Date()
},
$addToSet: { likes: ObjectID(userId) }
},
{ upsert: true }
);
resolve(result);
});
What it does:
If there is no document in the collection create one and fill in all the fields
If a document exists update only the likes array with a new userID
This works as expected and there are no problems, thanks to $addToSet I don't have any duplicates etc.
However after the above code gets executed and the result is returned I need to do some additional stuff and for it to work I need to know if Mongos $addToSet did change anything.
So in short:
if a new userId has been added to the likes array I want to do something
if the userId is already in the likes array and the $addToSet didnt change anything I don't want to take any action.
Is there a way to distinguish if the $addToSet did something?
The current console.log() of the result looks like this:
{
lastErrorObject: { n: 1, updatedExisting: true },
value: { _id: 5e2c47c57bb5183d80dce14f,
articleId: 5da4d1365217baf52fbcd76a,
creationDate: 2020-01-25T13:51:01.928Z,
likes: [ 5d750b677d8edfc08af8a527 ]
},
ok: 1
}
The one thing that comes to my mind (but is very bad performance-wise) is to compare the likes array before the update if it contains the userID with javascript includes method.
Any better ideas?
I found the solution!
99% of the credit goes to Neil Lunn for this answer:
https://stackoverflow.com/a/44994382/2175228
It brought me on the right track. I ditched the findOneAndUpdate method in favor of the updateOne method. The query parameters etc. everything stayed the same, only the method name changed and obviously the result type of this method.
The problem with the updateOne method is that it does not return the object that was updated (or the old one). But it does return the upsertedId which is the only thing I need in my situation.
So in the end what You get is a very long CommandResult object which starts with:
CommandResult {
result: { n: 1, nModified: 0, ok: 1 },
(...)
}
but when I looked very closely I noticed that the object has exactly those fields that I needed the whole time:
modifiedCount: 0,
upsertedId: null, // if object was inserted, this field will contain it's ID
upsertedCount: 0,
matchedCount: 1
So what You need to do is just take the parts that You need from the CommandResult object and just proceed ;)
My code after the changes:
async function like(articleId, userId) {
const db = await makeDb();
return new Promise((resolve, reject) => {
db.collection(collectionName)
.updateOne(
{ articleId: ObjectID(articleId) },
{
$setOnInsert: {
articleId: ObjectID(articleId),
creationDate: new Date()
},
$addToSet: { likes: ObjectID(userId) }
},
{ upsert: true }
)
.then(data => {
// wrap the content that You need
const result = {
upsertedId: data.upsertedId,
upsertedCount: data.upsertedCount,
modifiedCount: data.modifiedCount
};
resolve(result);
});
});
}
Hopefully this answer will help someone with a similar problem in the future :)
Related
I have a document stored on mongo that, amongst others, contains a property type of an array with a bunch of objects inside. I am trying to push an object to that array. However each object that gets pushed to into the array contains a date object. How I need it to work is if an object with that date already exists then dont push the new object into the array.
I'm been playing with it for a while, trying to run update and elemMatch queries etc, but I can't figure it out. I have a long draw out version below, as you can see I'm making 2 separate requests to the database, can someone help me reduce this to something simpler?
UserModel.findById(req.user._id, (err, doc) => {
if (err) {
return res.sendStatus(401);
}
const dateExists = doc.entries.find((entry) => entry.date === date);
if (dateExists) {
res.sendStatus(401);
} else {
UserModel.findByIdAndUpdate(
req.user._id,
{
$push: {
entries: {
date,
memo,
},
},
},
{ safe: true, upsert: true, new: true },
(err, doc) => (err ? res.sendStatus(401) : res.sendStatus(200))
);
}
});
Thanks.
db.users.updateOne({
_id: req.user._id,
"entries.date": date,
},
{
$push: {
entries: {date,memo},
}
})
https://docs.mongodb.com/manual/reference/method/db.collection.updateOne/index.html
This is how I store each element in my mongodb collection.
{
_id: 'iTIBHxAb8',
title: 'happy birthday',
votesObject: { happy: 0, birthday: 0 }
}
I made a very dirty work around which I am not at all proud of which is this...
//queryObject= {id,chosenvalue};
let queryObject = req.query;
let id = Object.keys(queryObject)[0];
let chosenValue = queryObject[id];
db.collection("voting")
.find({ _id: id })
.toArray((err, data) => {
let { votesObject } = data[0];
votesObject[chosenValue] += 1;
data[0].votesObject = votesObject;
db.collection("voting").replaceOne({ _id: id }, data[0]);
res.redirect("/polls?id=" + id);
});
So basically what this does is It gets the chosen value which may be "happy" or the "birthday" from the above example.
Finding the complete object from the collection which matches the id.
Incrementing the chosen value from the found object.
Using replaceOne() to replace the previous object with the newly changed object.
I am incrementing the value inside chosen value by one everytime this piece of code executes.
This works perfectly fine but I want to know if there is any way to directly update the chosen value without all this mess. I could not find a way to do it else where.
you can use mongoose findOneAndUpdate
It will be something like
const updateKey = "votesObject.$."+ chosenValue
let incQuery = {}
incQuery[updateKey] = 1
Model.findOneAndUpdate(
{ _id: id },
{ $inc: incQuery },
{ new : false },
callback
)
You can use $inc operator.
Try something like this:
db.collection.update({
"_id": id
},
{
"$inc": {
"votesObject.birthday": 1
}
})
This query will increment your field birthday in one.
Check mongo playground exaxmple here
Here's the user schema and the part I want to update is ToDo under User.js (further down). I am attempting to add new data to an array within the db.
data.js
app.post("/data", loggedIn, async (req, res) => {
console.log(req.body.content);
let content = { content: req.body.content };
User.update({ _id: req.user._id }, { $set: req.body }, function (err, user) {
if (err) console.log(err);
if (!content) {
req.flash("error", "One or more fields are empty");
return res.redirect("/");
}
user.ToDo.push(content);
res.redirect("/main");
});
});
User.js
new mongoose.Schema({
email: String,
passwordHash: String,
ToDo: {
type: [],
},
date: {
type: Date,
default: Date.now,
},
})
Originally I was trying the .push() attribute, but I get the error:
user.ToDo.push(content);
^
TypeError: Cannot read property 'push' of undefined
First of all, your problem is the callback is not the user. When you use update the callback is something like this:
{ n: 1, nModified: 1, ok: 1 }
This is why the error is thrown.
Also I recommend specify the array value, something like this:
ToDo: {
type: [String],
}
The second recommendation is to do all you can into mongo query. If you can use a query to push the object, do this instead of store the object into memory, push using JS function and save again the object into DB.
Of course you can do that, but I think is worse.
Now, knowing this, if you only want to add a value into an array, try this query:
var update = await model.updateOne({
"email": "email"
},
{
"$push": {
"ToDo": "new value"
}
})
Check the example here
You are using $set to your object, so you are creating a new object with new values.
Check here how $set works.
If fields no exists, will be added, otherwise are updated. If you only want to add an element into an array from a specified field, you should $push into the field.
Following your code, maybe you wanted to do something similar to this:
model.findOne({ "email": "email" }, async function (err, user) {
//Here, user is the object user
user.ToDo.push("value")
user.save()
})
As I said before, that works, but is better do in a query.
I'm trying to replace an array of sub-documents with a new copy of the array.
Something like...
var products = productUrlsData; //new array of documents
var srid = the_correct_id;
StoreRequest.findOneAndUpdate({_id: srid}, {$set: {products: products}}, {returnNewDocument : true}).then(function(sr) {
return res.json({ sr: sr}); //is not modified
}).catch(function(err) {
return res.json({err: err});
})
The products var has the correct modifications, but the returned object, as well as the document in the db, are not being modified. Is this not the correct way to replace a field which is an array of subdocuments? If not, what is?
I am a bit late to the party plus I am really in a hurry -- but should be:
StoreRequest.updateOne(
{ _id: srid },
{ $set: { 'products.$': products }},
{ new: true });
I couldn't make it work with findOneAndUpdate but the above does work.
Background:
I have an update query on collection called alerts that runs each
time a "flow" is received.
I have an array of objects in my alert's document called
blacklistedCommunication.
What should happen:
When a new flow arrives, then the alerts doc is updated only if the flow's client_addr and server_addr are not already present in blacklistedCommuication. While at the same time, if we do find duplicates, it should only increment the flowCount.
The current query:
The below update query works to push new objects to blacklistedCommunication object if it's not present already.
However, if it is indeed present, it will not update the flowCount
How can I incorporate this logic into the query? Do I need to write a separate update query in case of duplicates?
alerts.update({
alertLevel: "orgLevelAlert",
alertCategory: "blacklistedServersViolationAlert",
alertState: "open",
'blacklistedCommunication.client': {
$ne: flow.netflow.client_addr
},
// 'blacklistedCommunication.server': {
// $ne: flow.netflow.server_addr
// }
}, {
$set: {
"misc.updatedTime": new Date()
},
$inc: {
flowCount: 1
},
$push: {
blacklistedCommunication: {
client: flow.netflow.client_addr,
server: flow.netflow.server_addr
}
}
});
You can use $addToSet instead of $push. It will ensure unique {client:*,server:*} object within blacklistedCommunication and will always update flowCount:
alerts.update({
alertLevel: "orgLevelAlert",
alertCategory: "blacklistedServersViolationAlert",
alertState: "open"
}, {
$set: {
"misc.updatedTime": new Date()
},
$inc: {
flowCount: 1
},
$addToSet: {
blacklistedCommunication: {
client: flow.netflow.client_addr,
server: flow.netflow.server_addr
}
}
});