How to query and update nested arrays - mongodb

I am building a course system. Each course has multiple sections, each section has multiple steps. My datastructure is as follows:
{
"_id" : "Mtz4DMTwMMKWTWbzE",
"slug" : "how-to-be-awesome",
"title" : "How to be awesome",
"description" : "In 4 easy lessons.",
"createdAt" : ISODate("2014-08-25T13:33:24.675Z"),
"sections" : [
{
"title" : "Be cool",
"description" : "Title says it all really",
"steps" : [
{
"title" : "Wear sunglasses",
"description" : "Always works."
},
{
"title" : "Be funny",
"description" : "Make an occasional joke. But no lame ones."
}
]
}
]
}
This worked while adding steps;
Course._collection.update( { _id: course._id, sections: section }, {
"$push": {
"sections.$.steps": step
}
})
But I can't figure out how to update a step. I tried to give the steps an ID and do it like that, but it's not working, apparently because it's two arrays deep, and you can't have two positionals ($) in a query. I tried something like this:
Course._collection.update( { _id: course._id, 'sections.steps._id': step._id }, {
"$set": {
"sections.steps.$.title": "test updated title"
}
})
But this gave the following error:
can't append to array using string field name: steps
Is there a way to do this? Or is my schema design off?
Thanks!

Related

MongoDB: Is it possible to index(unique) subarrays from documents in an isolated way?

I recently encountered an issue, and I'd like to solve it. If anyone would give any suggestion I'll be grateful.
I have documents that represent "users" and each document has a subarray that is responsible to save some codes, they can be many for each user. The matter is, each user cannot have duplicate codes in its specific array, but at the same time, in this case, each document should be isolated, for example, being possible to have two or more identical codes but since they are from different documents(users).
In short, the subarray("codes") cannot have individually duplicated codes(code), but that shouldn't interfere with other documents
I could do that in the application part, but I think doing that guarantee directly on DB, it's safer.
Is it possible to create indexes for this specific situation?
Example of two documents representing their respective users:
{ // Document of user 1
"_id" : "1", //user 1 and its codes
"codes" : [
{
"code" : "1111",
"description" : "code 1",
},
{
"code" : "2222",
"description" : "code 2",
},
{
"code" : "3333",
"description" : "code 3",
}
]
},
{ // Document of user 2
"_id" : "2", //user 2 and its codes
"codes" : [
{
"code" : "1111",
"description" : "code 1",
},
{
"code" : "4444",
"description" : "code 2",
},
{
"code" : "2222",
"description" : "code 3",
}
]
}
Thank you!
Use https://docs.mongodb.com/manual/reference/operator/update/addToSet/ to maintain uniqueness of code subdocuments. You will need to ensure that you always specify code fields in the same order (e.g. code, description).

Updating matched array by identifier with multiple names [duplicate]

I have a large DB with various inconsistencies. One of the items I would like to clear up is changing the country status based on the population.
A Sample of the data is:
{ "_id" : "D", "name" : "Deutschland", "pop" : 70000000, "country" : "Large Western" }
{ "_id" : "E", "name" : "Eire", "pop" : 4500000, "country" : "Small Western" }
{ "_id" : "G", "name" : "Greenland", "pop" : 30000, "country" : "Dependency" }
{ "_id" : "M", "name" : "Mauritius", "pop" : 1200000, "country" : "Small island"}
{ "_id" : "L", "name" : "Luxembourg", "pop" : 500000, "country" : "Small Principality" }
Obviously I would like to change the country field go something more uniform, based on population size.
I've tried this approach, but obviously missing some way of tying into an update of the country field.
db.country.updateMany( { case : { $lt : ["$pop" : 20000000] }, then : "Small country" }, { case : { $gte : ["$pop" : 20000000] }, then : "Large country" }
Edit: Posted before I was finished writing.
I was thinking to use $cond functionality, to basically return if true, do X, if false, do y, while using the updateMany.
Is this possible, or is there a workaround?
You really want want bulkWrite() using two "updateMany" statements within it instead. Aggregation expressions cannot be used to do "alternate selection" in any form of update statement.
db.country.bulkWrite([
{ "updateMany": {
"filter": { "pop": { "$lt": 20000000 } },
"update": { "$set": { "country": "Small Country" } }
}},
{ "updateMany": {
"filter": { "pop": { "$gt": 20000000 } },
"update": { "$set": { "country": "Large Country" } }
}}
])
There is still an outstanding "feature request" on SERVER-6566 for "conditional syntax", but this is not yet resolved. The "bulk" API was actually introduced after this request was raised, and really can be adapted as shown to do more or less the same thing.
Also using $out in an aggregation statement as was otherwise suggested is not an option to "update" and can only write to a "new collection" at present. The slated change from MongoDB 4.2 onwards would allow $out to actually "update" an existing collection, however this would only be where the collection to be updated is different from any other collection used within the gathering of data from the aggregation pipeline. So it is not possible to use an aggregation pipeline to update the same collection as what you are reading from.
In short, use bulkWrite().

Meteor/MongoDB Reference specific subdocument from another document

I'm working on prototyping a note-taking application in Meteor; functional requirements include:
users have access to shared notes
notes contain distinct sections
each user needs to be able to add notations to notes/sections
notations can be preserved over time (e.g. add to existing notations without updating or deleting previously created notation)
notations should be private between users
Given the above, each document has a data key that contains the array of subdocuments - each section of the note. Something like this:
{
"_id" : ObjectId("someObjectID"),
"owner" : "Q5mpJZnAtFN5EMWT9",
"createdAt" : "2018-01-05T22:56:03.257Z",
"updatedAt" : "2018-01-06T12:07:03.123Z",
"parent" : null,
"title" : "Note Title",
"data" : [
{
"date" : "2018-01-05T22:56:03.257Z",
"title" : "Section 1 Title",
"text" : "Section content goes here..."
},
{
"date" : "2018-01-05T22:56:03.257Z",
"title" : "Section 2 Title",
"text" : "Section content goes here..."
}
]
}
For the main notes documents, the data array stores the sections as subdocuments; for user notations, the data array stores their personal notations as subdocuments. My thinking is to use the parent key to distinguish between shared notes and user notations:
parent : null for "top level", shared notes
something like parent : "yG8xrh6KiZXv7e8MD" to point back to the "top level" note or subdocument for user notations. (Hopefully this makes sense).
Two questions. First and foremost - is this a valid design?
If it IS a valid design, how do I then reference a specific subdocument? For example, in the above document, if a user wants to add a notation to Section 2 only? Can I add an _id to the subdocument and then use that value for the parent key in the notation document?
This not the complete solution, but just an example:
I would do it something like this. I'd modify your document a bit, adding notations field in every section:
{
"_id" : ObjectId("someObjectID"),
"owner" : "Q5mpJZnAtFN5EMWT9",
"createdAt" : "2018-01-05T22:56:03.257Z",
"updatedAt" : "2018-01-06T12:07:03.123Z",
"parent" : null,
"title" : "Note Title",
"data" : [
{
"date" : "2018-01-05T22:56:03.257Z",
"title" : "Section 1 Title",
"text" : "Section content goes here...",
"notations": [
{
_id: "some id",
version:1
userId: "fsajksffhj",
date: "2018-01-05T22:56:06",
note: "some note about this sectioon"
},
{
_id: "some id2",
version:1,
userId: "fsajksffhj",
date: "2018-01-05T22:56:06",
note: "some note about this sectioon"
},
{
_id: "some id1",
version:1,
userId: "fsajksffhj",
date: "2018-02-06T00:56:06",
note: "edited the first notation"
}
]
},
{
"date" : "2018-01-05T22:56:03.257Z",
"title" : "Section 2 Title",
"text" : "Section content goes here..."
}
]
}
notations should be private between users
This is harder part. I'd use Meteor Methods to do this. Another way could be to use MongoDB's aggregation functionality with match, unwind, re-match, group and create document again. You are using reactivity if using either of these.
Meteor.methods({
'notes.singleNote: function(noteId, notationsUserId) {
check(noteId, String);
check(notationsUserId);
let note = Notes.findOne(noteId);
// remove other users' notations
note.data = note.data.map(function(data) {
if (data.notations) {
data.notations = data.notations.filter(function(d) {
return d.userId === notationsUserId;
});
}
return data
});
});
return note;
}
});

Perform a search on main collection field and array of objects simultaneously

I have my document structure as below:
{
"codeId" : 8.7628945723895E13, // long numeric value stored in scientific notation by Mongodb
"problemName" : "Hardware Problem",
"problemErrorCode" : "97695686856",
"status" : "active",
"problemDescription" : "ghdsojgnhsdjgh sdojghsdjoghdghd i0dhgjodshgddsgsdsdfghsdfg",
"subProblems" : [
{
"codeId" : 8.76289457238896E14,
"problemName" : "Some problem",
"problemErrorCode" : "57790389503490249640",
"problemDescription" : "This is edited",
"status" : "active",
"_id" : ObjectId("589476eeae39b20b1c15535b")
},
...
]
}
I have a search field which should search by codeId which basically serves as parentCodeID in search fields as shown below
Now, along with parentIdCode I want to search for codeId, problemCode, problemName and problemDescription as well.
How do I query the submodules with a regex search and at same time tag some parent field with "$or" clause etc. to achieve this ?
You can try something like this.
query = {
'$or': [{
"codeId":somevalue
}, {
"subProblems.codeId": {
"$regex": searchValue,
"$options": "i"
}
}, {
//rest of sub modules fields
}]
};

Using $addToSet to update an array field using another array field

I should start with: I'm knew to MongoDB, and document-style databases in general.
I have a collection that looks something like this:
{
"_id" : ObjectId("554a5e72b16f31ff0894310e"),
"title" : "ABC",
"admins" : [
"personA",
"personB",
],
"email_address" : "ABC#mysite.com"
}
{
"_id" : ObjectId("554a5e72b16f31ff0894310f"),
"title" : "Junk Site",
"admins" : [
"personA",
"personB"
],
"email_address" : "garbage#mysite.com"
}
{
"_id" : ObjectId("554a5e72b16f31ff08943110"),
"title" : "Company Three Site",
"admins" : [
"personC"
"personD",
],
"email_address" : "company2plus1#mysite.com"
}
What I need to do, is append the admins list from Company One, to Company Three such that Company Three now has four admins (A, B, C, D).
I tried the following, because it seemed pretty straight forward to me - get the data from the origin and append to destination directly:
db.runCommand({
findAndModify : 'sites',
query : {'title' : 'Company Three Site'},
update : { '$addToSet' :
{'admins' :
db.projects.find({'title' : 'ABC'}, {'_id' : 0, 'admins' : 1}
}
}
})
However, this does not work correctly.
I am still trying to figure out ways I could do this directly, but questions...
1) Is this even possible by using single command, or do I need to split this up?
2) Does my train of logical thought make sense, or should I be doing this some other/easier way that is more conventional for MongoDB style databases?
db.projects.find actually returns a cursor, which you definitely don't want to add to your set. Since you know ahead of time that you will be only finding one value, you can get the properties out of the cursor specifically by using .next().admin -- but remember that this will only work with the first value returned from .find. Otherwise, I think you will have to use a loop.
$addToSet will also add the array as a whole, so instead you have to append multiple values using $each
All together:
db.runCommand({
findAndModify: 'sites',
query: {'title': 'Company Three Site'},
update: {
$addToSet: {
"admins": {
$each: db.projects.find(
{"title": "ABC"},
{"_id": 0, "admins": 1}
).next().admins
}
}
}
})
This is not possible with an atomic update. However, a workaround is to query the source collection using the find() method and use the cursor's forEach() method to iterate over the results, get the array and update the destination collection using the $addToSet operator and the $each modifier.
Let's demonstrate this with the above sample documents inserted to a test collection:
db.test.insert([
{
"title" : "ABC",
"admins" : [
"personA",
"personB"
],
"email_address" : "ABC#mysite.com"
},
{
"title" : "Junk Site",
"admins" : [
"personA",
"personB"
],
"email_address" : "garbage#mysite.com"
},
{
"title" : "Company Three Site",
"admins" : [
"personC",
"personD"
],
"email_address" : "company2plus1#mysite.com"
}
])
The following operation will add the admins array elements from company "ABC" to the company "Company Three Site" admin array:
db.test.find({"title" : "ABC"}).forEach(function (doc){
var admins = doc.admins;
db.test.update(
{"title" : "Company Three Site"},
{
"$addToSet": {
"admins": { "$each": admins }
}
},
{ "multi": true }
);
});
Querying the collection for the document with company "Company Three Site" db.collection.find({"title" : "Company Three Site"});
will yield:
/* 0 */
{
"_id" : ObjectId("554a7dc35c5e0118072dd885"),
"title" : "Company Three Site",
"admins" : [
"personC",
"personD",
"personA",
"personB"
],
"email_address" : "company2plus1#mysite.com"
}