Update array of subdocuments in MongoDB - arrays

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.

Related

How to remove a field having a specified value from a MongoDB document?

Here is a given document in mongo db as below
{
"emailid": "xxx.gmail",
"user_devices": [],
"devices": [{
"created_time": 1607153351,
"token": 123
},
{
"created_time": 1807153371,
"token": 1345
}]
}
Here i want to remove the field inside devices ie
"created_time": '', "token": '1345'
here token value is known, so need to delete "created_time" along with "token" where output is like
{
"emailid": "xxx.gmail",
"user_devices": [],
"devices": [{
"created_time": 1607153351,
"token": 123
}]
}
I tried unset code like below
var myquery = { 'emailid': emailid , 'devices.token':1345};
var newvalues = { $unset:{
'devices.created_time':'',
'devices.token':1345
} };
and used
updateOne(myquery, newvalues, function(err, res))
but it does not work. Can anyone tell how to remove this specified field. Thanks in advance.
Since the object you want to remove is in an array, you have to use $pull in this way:
db.collection.update({
"emailid": "xxx.gmail"
},
{
"$pull": {
"devices": {
"token": 123
}
}
})
In this query you are telling mongo: "Remove -using $pull- one field from the array devices where token is 123".
Example here
Edit:
Also, if you want to remove only one field into objec within array and not the object itself, the you can use $[<identifier>] to filter and then $unset like this:
db.collection.update({
"emailid": "xxx.gmail"
},
{
"$unset": {
"devices.$[elem].created_time": ""
}
},
{
"arrayFilters": [
{
"elem.token": 123
}
]
})
In this query, using arrayFilters you can $unset only in the object where token is 123. You can use created_time to filter if you want too.
Example here

MongoDB - Update multiple subdocuments in array in multiple documents

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}

mongo update : upsert an element in array

I would like to upsert an element in an array, based on doc _id and element _id. Currently it works only if the element is allready in the array (update works, insert not).
So, these collection:
[{
"_id": "5a65fcf363e2a32531ed9f9b",
"ressources": [
{
"_id": "5a65fd0363e2a32531ed9f9c"
}
]
}]
Receiving this request:
query = { _id: '5a65fcf363e2a32531ed9f9b', 'ressources._id': '5a65fd0363e2a32531ed9f9c' };
update = { '$set': { 'ressources.$': { '_id': '5a65fd0363e2a32531ed9f9c', qt: '153', unit: 'kg' } } };
options = {upsert:true};
collection.update(query,update,options);
Will give this ok result:
[{
"_id": "5a65fcf363e2a32531ed9f9b",
"ressources": [
{
"_id": "5a65fd0363e2a32531ed9f9c",
"qt": 153,
"unit": "kg"
}
]
}]
How to make the same request work with these initial collections:
[{
"_id": "5a65fcf363e2a32531ed9f9b"
}]
OR
[{
"_id": "5a65fcf363e2a32531ed9f9b",
"ressources": []
}]
How to make the upsert work?
Does upsert works with entire document only?
Currently, I face this error:
The positional operator did not find the match needed from the query.
Thanks
I also tried to figure out how to do it. I found only one way:
fetch model by id
update array manually (via javascript)
save the model
Sad to know that in 2018 you still have to do the stuff like it.
UPDATE:
This will update particular element in viewData array
db.collection.update({
"_id": args._id,
"viewData._id": widgetId
},
{
$set: {
"viewData.$.widgetData": widgetDoc.widgetData
}
})
$push command will add new items

Add new object inside array of objects, inside array of objects in mongodb

Considering the below bad model, as I am totally new to this.
{
"uid": "some-id",
"database": {
"name": "nameOfDatabase",
"collection": [
{
"name": "nameOfCollection",
"fields": {
"0": "field_1",
"1": "field_2"
}
},
{
"name": "nameOfAnotherCollection",
"fields": {
"0": "field_1"
}
}
]
}
}
I have the collection name (i.e database.collection.name) and I have a few fields to add to it or delete from it (there are some already existing ones under database.collection.fields, I want to add new ones or delete exiting ones).
In short how do I update/delete "fields", when I have the database name and the collection name.
I cannot figure out how to use positional operator $ in this context.
Using mongoose update as
Model.update(conditions, updates, options, callback);
I don't know what are correct conditions and correct updates parameters.
So far I have unsuccessfully used the below for model.update
conditions = {
"uid": req.body.uid,
"database.name": "test",
"database.collection":{ $elemMatch:{"name":req.body.collection.name}}
};
updates = {
$set: {
"fields": req.body.collection.fields
}
};
---------------------------------------------------------
conditions = {
"uid": req.body.uid,
"database.name": "test",
"database.collection.$.name":req.body.collection.name
};
updates = {
$addToSet: {
"fields": req.body.collection.fields
}
};
I tried a lot more but none did work, as I am totally new.
I am getting confused between $push, $set, $addToSet, what to use what not to?, how to?
The original schema is supposed to be as show below, but running queries on it is getting harder n harder.
{
"uid": "some-id",
"database": [
{ //array of database objects
"name": "nameOfDatabase",
"collection": [ //array of collection objects inside respective databases
{
"name": "nameOfCollection",
"fields": { //fields inside a this particular collection
"0": "field_1",
"1": "field_2"
}
}
]
}
]
}

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.

Resources