MongoDB - Update multiple subdocuments in array in multiple documents - arrays

I'm making a node.js website. I have a posts collection in which comments for posts are stored in an array with the comment author's details as nested object.
This is new post's schema:
{
"text": text,
"image": image,
"video": video,
"type": type,
"createdAt": createdAt,
"reactions": [],
"comments": [],
"shares": [],
"user": {
"_id": user._id,
"username": user.username
}
}
This is new comment being pushed to its post:
$push: {
"comments": {
"_id": commentId,
"user": {
"_id": user._id,
"type": type,
"name": user.name,
"profileImage": user.photo,
},
"comment": comment,
"createdAt": createdAt,
"replies": []
}
}
To avoid storing comments in another collection and doing complex multiple lookups(I'm doing 1 lookup to get post author details but couldn't add another to make it work for comments) to consolidate the newsfeed I decided to save comments and their author's details embedded in the posts.
Now when user profile picture is updated all the comments have to be updated to show the new picture.
I included this updateMany query along with the photo updation route in server.js file:
database.collection("posts").updateMany({
"comments.user._id": user._id,
"comments.user.type": "friend"
}, {
$set: {
"comments.$.user.profileImage": photo
}
});
The problem here is that this updates only the first matching comment in all posts.
I need to update all matching comments in all posts.
I'm actually just learning by doing this following youtube videos, so please help me.

You need to use arrayFilters I think.
If I've understand well your question this example should be similar to your DB.
The query is this:
db.collection.update({
"comments.user._id": 1,
"comments.user.type": "friend"
},
{
"$set": {
"comments.$[element].user.profileImage": "new"
}
},
{
"arrayFilters": [
{
"$and": [
{
"element.user._id": 1
},
{
"element.user.type": "friend"
}
]
}
],
"multi": true
})
First part is the same, and almost the second. You have to add element position into the array that is defined in the next step.
Using arrayFilters, you look for those whose match the comaprsion into $and. And only those ones will be updated.
Note that using updateMany() method, is not neccesary using {multi: true}

Related

Graphql + MongoDB : updating a deep nested object [Partial Update the object]

I have a huge database with 1000s of records. There are different types of "Groups" and each child object(group assignments) has different levels of nesting, (refer to the code below)
mutation UpdateGroup($input: GroupUpdateInput!) {
updateGroup(input: $input) {
id
isActive
ChildA{
nameA
ChildB{
id
}
}
}
}
As you can see when I want to update just one nested object. It becomes really hard to manage and keep track of what is being modified. What is a good solution to approach this issue. I know this question is difficult to explain but please let me know if I can make something more clear in the comments. I will try to produce a working sample but it could take time because the codebase is very huge
"input": {
"id": "6231107602d54a291d29",
"name": {
"language": "en-US",
"text": "Test Update Group"
},
"isActive": true,
"code": "127",
"benefit": {
"provider": "Group Provider1",
"benefitOptions": [
{
"id":"6234b20e6a941cdcbd72",
"name": [
{
"language": "en-US",
"text": "Benefit Option 1"
}
],
"assignedProfileIds": ["6231101d02d54d297c6c","6231101e02d54a297c6d"]
}
]
}
}
}

How to update a double nested value inside an array of multiple documents?

Imagine the following collection of city records:
{
"city": "London",
"inhabitants": [
{
"id": "34543534",
"user": {
"name": "Jonathan Deer",
"email": "john#btinternet.com"
}
},
{
"id": "0454534",
"user": {
"name": "Tanya Patel",
"email": "tanya#btinternet.com"
}
},
{
"id": "4345345",
"user": {
"name": "Catherine King",
"email": "catherine#gmail.com"
}
}
]
}
{
"city": "Manchester",
"inhabitants": [
{
"id": "980003",
"user": {
"name": "Benjamin Thaw",
"email": "benny#btinternet.com"
}
},
{
"id": "734488",
"user": {
"name": "Craig Longstone",
"email": "craig#gmail.com"
}
},
{
"id": "4400093",
"user": {
"name": "Arnold Greentree",
"email": "arnold#btinternet.com"
}
},
]
},
What I'm trying to do is loop through each inhabitants array of each city, and see if any of the people there has an email address containing btinternet.com in it. For those users I want to sent a new flag isBT: true and for everyone else (e.g., gmail.com users) isBT: false:
"user": {
"name": "Tanya Patel",
"email": "tanya#btinternet.com"
"isBT" true
}
I tried the following queries - first query sets all of them to isBT: false while the second one searches for "btinternet.com" in email address and sets isBT: true:
db.city.update({ "inhabitants.user.email": {$exists: true}}, {$set: { "inhabitants.$.user.isBT": false}}, {multi: true})
db.city.update({ "inhabitants.user.email": {$regex: "btinternet.com"}}, {$set: { "inhabitants.$.user.isBT": true}}, {multi: true})
The problem is that when I execute the second query, there are several inhabitants records that are left with isBT: false even though they contain the necessary "btinternet.com" email address. It almost seems like only the first user record that matches the criteria gets updated... Is there a way to update ALL user records for all "inhabitants" arrays?
I looked at using the positional operator $[], but our DB is on version 2.6.3 but this operator was introduced only in 3.6...
The short answer is "no".
The long answer is "no, because your MongoDB version doesn't support such an operation". You'll need to either...
1. retrieve all matching documents and perform a full array update through server-side processing of the data (e.g. use the MongoDB cursor.forEach()),
2. extend your match for "inhabitants.user.isBT": true (use
$elemMatch) and repeatedly perform the update query until the
number of modified documents is 0 (i.e. there are no more array
elements to update), or
3. update your MongoDB version and any
server-side code that relies on features of the current version that
have changed between 2.6 and 3.6.
Any solution to this problem will require more effort than a single query. There's no getting around it. It's a tough pill to swallow, but there really isn't a nice alternative.

Update array of subdocuments in MongoDB

I have a collection of students that have a name and an array of email addresses. A student document looks something like this:
{
"_id": {"$oid": "56d06bb6d9f75035956fa7ba"},
"name": "John Doe",
"emails": [
{
"label": "private",
"value": "private#johndoe.com"
},
{
"label": "work",
"value": "work#johndoe.com"
}
]
}
The label in the email subdocument is set to be unique per document, so there can't be two entries with the same label.
My problems is, that when updating a student document, I want to achieve the following:
adding an email with a new label should simply add a new subdocument with the given label and value to the array
if adding an email with a label that already exists, the value of the existing should be set to the data of the update
For example when updating with the following data:
{
"_id": {"$oid": "56d06bb6d9f75035956fa7ba"},
"emails": [
{
"label": "private",
"value": "me#johndoe.com"
},
{
"label": "school",
"value": "school#johndoe.com"
}
]
}
I would like the result of the emails array to be:
"emails": [
{
"label": "private",
"value": "me#johndoe.com"
},
{
"label": "work",
"value": "work#johndoe.com"
},
{
"label": "school",
"value": "school#johndoe.com"
}
]
How can I achieve this in MongoDB (optionally using mongoose)? Is this at all possible or do I have to check the array myself in the application code?
You could try this update but only efficient for small datasets:
mongo shell:
var data = {
"_id": ObjectId("56d06bb6d9f75035956fa7ba"),
"emails": [
{
"label": "private",
"value": "me#johndoe.com"
},
{
"label": "school",
"value": "school#johndoe.com"
}
]
};
data.emails.forEach(function(email) {
var emails = db.students.findOne({_id: data._id}).emails,
query = { "_id": data._id },
update = {};
emails.forEach(function(e) {
if (e.label === email.label) {
query["emails.label"] = email.label;
update["$set"] = { "emails.$.value": email.value };
} else {
update["$addToSet"] = { "emails": email };
}
db.students.update(query, update)
});
});
Suggestion: refactor your data to use the "label" as an actual field name.
There is one straightforward way in which MongoDB can guarantee unique values for a given email label - by making the label a single separate field in itself, in an email sub-document. Your data needs to exist in this structure:
{
"_id": ObjectId("56d06bb6d9f75035956fa7ba"),
"name": "John Doe",
"emails": {
"private": "private#johndoe.com",
"work" : "work#johndoe.com"
}
}
Now, when you want to update a student's emails you can do an update like this:
db.students.update(
{"_id": ObjectId("56d06bb6d9f75035956fa7ba")},
{$set: {
"emails.private" : "me#johndoe.com",
"emails.school" : "school#johndoe.com"
}}
);
And that will change the data to this:
{
"_id": ObjectId("56d06bb6d9f75035956fa7ba"),
"name": "John Doe",
"emails": {
"private": "me#johndoe.com",
"work" : "work#johndoe.com",
"school" : "school#johndoe.com"
}
}
Admittedly there is a disadvantage to this approach: you will need to change the structure of the input data, from the emails being in an array of sub-documents to the emails being a single sub-document of single fields. But the advantage is that your data requirements are automatically met by the way that JSON objects work.
After investigating the different options posted, I decided to go with my own approach of doing the update manually in the code using lodash's unionBy() function. Using express and mongoose's findById() that basically looks like this:
Student.findById(req.params.id, function(err, student) {
if(req.body.name) student.name = req.body.name;
if(req.body.emails && req.body.emails.length > 0) {
student.emails = _.unionBy(req.body.emails, student.emails, 'label');
}
student.save(function(err, result) {
if(err) return next(err);
res.status(200).json(result);
});
});
This way I get the full flexibility of partial updates for all fields. Of course you could also use findByIdAndUpdate() or other options.
Alternate approach:
However the way of changing the schema like Vince Bowdren suggested, making label a single separate field in a email subdocument, is also a viable option. In the end it just depends on your personal preferences and if you need strict validation on your data or not.
If you are using mongoose like I do, you would have to define a separate schema like so:
var EmailSchema = new mongoose.Schema({
work: { type: String, validate: validateEmail },
private: { type: String, validate: validateEmail }
}, {
strict: false,
_id: false
});
In the schema you can define properties for the labels you already want to support and add validation. By setting the strict: false option, you would allow the user to also post emails with custom labels. Note however, that these would not be validated. You would have to apply the validation manually in your application similar to the way I did it in my approach above for the merging.

Generate query results based on tags in PouchDB

I'm new to NoSQL but have decided to use PouchDB for an Angular Application I am creating.
There are going to be a series of questions (about 1000 in total) which each have their own tags. Each object shouldn't have more that 6 or 7 tags. Example data is:
{
"text": "Question?",
"answers": [
{ "text": "Yes", "correct": true },
{ "text": "No", "correct": false }
],
"tags": ["tag1", "tag3"]
},
{
"text": "Question?",
"answers": [
{ "text": "Yes","correct": true },
{ "text": "No", "correct": false }
],
"tags": ["tag2", "tag3"]
}
I'm at a total loss on how I can query the db in order to retrieve only questions that have "tag2" or questions that have "tag1" and "tag3".
I came across the question found at How to query PouchDB with SQL-like operators but can't seem to wrap my head around how it works. I tried to modify it based on my data and I always get 0 results when querying the database.
I guess my biggest struggle is comparing it to SQL when it isn't. Does anyone know how I can go about creating a query based on specific tags?
Yup, you create a map/reduce query like this:
// document that tells PouchDB/CouchDB
// to build up an index on tags
var ddoc = {
_id: '_design/my_index',
views: {
my_index: {
map: function (doc) {
doc.tags.forEach(function (tag) {
emit(tag);
});
}.toString()
}
}
};
// save it
pouch.put(ddoc).then(function () {
// success!
}).catch(console.log.bind(console));
Then you query it:
pouch.query('my_index', {key: myTag, include_docs: true}).then(function (res) {
// got a result
}).catch(console.log.bind(console));
If you want to find multiple tags, you can just keys instead of key.
BTW this will be easier in the future when I add $elemMatch and $in to pouchdb-find.

How to modify a child entry in a mongoDB

I've got a collection of forum posts, looking (roughly) like
{
"author": author,
"date": new Date(),
"content": req.body.content,
"comments":{
"deleted": 0,
"author": author,
"date": new Date(),
"content": req.body.content
}
}
if a user adds a comment, it is simply $pushed on to the current comments.
Now i want a possibility to delete one specific comment.
I inserted the "deleted" flag to review the comments as admin, rather than just completely deleting them.
but the problem is: how do i tell mongoDB which exact comment i want to delete?
I have just now added an "commentID" field, but i can't figure out how to auto-increment it (the documentation is really strange about this),
after having tried a lot of stuff like "comments.toDeleteId" or "comments[toDeleteId]" in the query.
TLDR: how can i query post "34563456354", comment "5", $set "deleted" to "1", where the comments are an array themselves?
thanks
EDIT
this is the console.dir of a post:
{_id: //etc etc
op: 'Anonymous',
postContent: 'testtest',
createdAt: //etc,
comments:
[ { deleted: 0,
posted: //etc,
author: 'Anonymous',
comment: 'test' },
{ deleted: 0,
posted: //etc,
author: 'Anonymous',
comment: 'testtesttest' } ]
Use $elemMatch in update as below :
db.collectioName.update({
"_id": ObjectId("55ba6729eb5f4a21914568df"),
"comments": {
"$elemMatch": {
"comment": "testtesttest"
}
}
}, {
"$set": {
"comments.$.deleted": 1
}
})
Or If you want to increment the deleted then use $inc as :
db.collectionName.update({
"_id": ObjectId("55ba6729eb5f4a21914568df"),
"comments": {
"$elemMatch": {
"comment": "testtesttest"
}
}
}, {
"$inc": {
"comments.$.deleted": 1
}
})
And If you want to push new object in comment then use $push in update as :
db.collectionName.update({
"_id": ObjectId("55ba6729eb5f4a21914568df")
}, {
"$push": {
"comments": {
"deleted": 1,
"posted": new Date(),
"author": "ABC",
"comment": "tests"
}
}
})
i have actually found a way to do this, even though i had searched before and did not find that thread.
the way to do it is actually quite simple, yet hard to grasp at first.
var selector = {};
var operator = {};
selector['comments.' + commentId + '.deleted'] = 1;
operator['$set'] = selector;
this gave me a
{'$set': {'comments.2.deleted': 1 } }
which i could easily insert into the query where
{_id: thisId,
operator,
cb)
(credits to https://stackoverflow.com/a/19115944/4547337 )

Resources