Remove array from objects field in MongoDB - javascript

I am trying to remove an element of Array just by finding it via the ObjectId in the array element. Following MongoDB opposite of $addToSet to '$removeFromSet', I should've been able to do:
db.collection.update(
{name: 'nameOfNode'},
{$pull: { activities: ["5e7d16e9736bb64bdd158c13"]}})
or
db.collection.update(
{name: 'nameOfNode'},
{$pull: { activities: ["5e7d16e9736bb64bdd158c13", "Materi"]}})
Yet both of these codes above doesn't work, does if there is another way to remove them by just one single value, or if that isn't possible, remove them with the two values?
Note: I have removed some confidential information from the screenshot, hence the supposed gaps between the letters are not spaces, but covered texts. The strings I've tried to use to refer to them are identical though, no difference.

I see 2 problems with the code:
- The value is not a member of the activities array, it is a member of an array contain in the activities array
- Searching for a string like activities: ["5e7d16e9736bb64bdd158c13"] will not match a value of type ObjectId.
I ran a quick demonstration of matching an ObjectId:
MongoDB Enterprise replset:PRIMARY> db.updtest.find()
{ "_id" : ObjectId("5e7d354ccede22b56b5335bc"), "name" : "nameOfNode", "activities" : [ [ ObjectId("5e7d354ccede22b56b5335b9"), "Check-in" ], [ ObjectId("5e7d354ccede22b56b5335ba"), "Materi" ], [ ObjectId("5e7d354ccede22b56b5335bb"), "Materi" ] ] }
MongoDB Enterprise replset:PRIMARY> db.updtest.update({"name" : "nameOfNode"},{$pull:{"activities.0":ObjectId("5e7d354ccede22b56b5335b9")}})
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })
MongoDB Enterprise replset:PRIMARY> db.updtest.find()
{ "_id" : ObjectId("5e7d354ccede22b56b5335bc"), "name" : "nameOfNode", "activities" : [ [ "Check-in" ], [ ObjectId("5e7d354ccede22b56b5335ba"), "Materi" ], [ ObjectId("5e7d354ccede22b56b5335bb"), "Materi" ] ] }
Note that while this successfully removes the ObjectId value, it does not remove the containing array, which is what I think you wanted.
If you are using MongoDB 4.2, you could use $filter to remove the entire subelement, like:
db.updtest.update(
{"name" : "nameOfNode"},
[ {$set:{
activities:{
$filter:{
input:"$activities",
cond:{$ne:["$this.0",ObjectId("5e7d16e9736bb64bdd158c13")]}
}
}
}}]
)
If you can modify the schema, insert activities as objects instead of as sub-arrays. For example, if each activity were of the form {id:... type: ...}, you could match the specific id using $pull:
MongoDB Enterprise replset:PRIMARY> db.updtest.find()
{ "_id" : ObjectId("5e7d3939cede22b56b5335c4"), "name" : "nameOfNode", "activities" : [ { "id" : ObjectId("5e7d3939cede22b56b5335c1"), "type" : "Check-in" }, { "id" : ObjectId("5e7d3939cede22b56b5335c2"), "type" : "Materi" }, { "id" : ObjectId("5e7d3939cede22b56b5335c3"), "type" : "Materi" } ] }
MongoDB Enterprise replset:PRIMARY> db.updtest.update({"name" : "nameOfNode"},{$pull:{"activities":{id:ObjectId("5e7d3939cede22b56b5335c2")}}})
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })
MongoDB Enterprise replset:PRIMARY> db.updtest.find()
{ "_id" : ObjectId("5e7d3939cede22b56b5335c4"), "name" : "nameOfNode", "activities" : [ { "id" : ObjectId("5e7d3939cede22b56b5335c1"), "type" : "Check-in" }, { "id" : ObjectId("5e7d3939cede22b56b5335c3"), "type" : "Materi" } ] }

Related

change only one field of entire array of embedded document in mongoose

i have a list schema and a question set schema. the quetsionSet schema is embedded inside the list schema. its working fine but how can i update anything inside the array of embedded document i.e. here i want to change the listname of all the documents inside questionSet (array of questionSet documents).
here is an example of my list document model
{ "_id" : ObjectId("60f2cc07275bbb30d8cb268e"),
"listName" : "dsa",
"aboutList" : "dsa queestions",
questionSet" : [ { "solved" : false,
"_id" : ObjectId("60f2cc12275bbb30d8cb2695"),
"topic" : "array",
"name" : "array is best",
"url" : "www.arr.com",
"listname" : "dsa",
"__v" : 0 },
{ "solved" : false,
"_id" : ObjectId("60f2cc1b275bbb30d8cb269d"),
"topic" : "linked list",
"name" : "reverse list",
"url" : "www.list.com",
"listname" : "dsa",
"__v" : 0 }
],
"__v" : 2
}
you can use the following in your case
db.<collection_name>.updateOne(
{ "_id" : ObjectId("60f2cc07275bbb30d8cb268e")},
{
$set: {
'questionSet.$[].listname': "javascript"
}
}
)

What is the difference between $and and $all in this particular case?

These two lines of code simply output the same result, so what is the difference between them? I know I know, documentation... But I mean in this context. Thank you for your answers!
db.someData.find({$and: [{genre: {$eq: "action"}}, {genre: {$eq: "thriller"}}]}).pretty()
db.someData.find({genre: {$all: ["action", "thriller"]}}).pretty()
This is the collection in my mongodb database.
{
"_id" : ObjectId("5d19fe6080fc4d046d99d42b"),
"title" : "The Last Student Returns",
"meta" : {
"rating" : 9.5,
"aired" : 2018,
"runtime" : 100
},
"visitors" : 1300000,
"expectedVisitors" : 1550000,
"genre" : [
"thriller",
"drama",
"action"
]
}
{
"_id" : ObjectId("5d19fe6080fc4d046d99d42c"),
"title" : "Teach me if you can",
"meta" : {
"rating" : 8.5,
"aired" : 2014,
"runtime" : 90
},
"visitors" : 590378,
"expectedVisitors" : 500000,
"genre" : [
"action",
"thriller"
]
}
{
"_id" : ObjectId("5d19fe6080fc4d046d99d42d"),
"title" : "Supercharged Teaching",
"meta" : {
"rating" : 9.3,
"aired" : 2016,
"runtime" : 60
},
"visitors" : 370000,
"expectedVisitors" : 1000000,
"genre" : [
"thriller",
"action"
]
}
Interesting that you mentioned documentation since your exact question is actually answered there:
Behavior
Equivalent to $and Operation
The $all is equivalent to an $and operation of the specified values;
i.e. the following statement:
{ tags: { $all: [ "ssl" , "security" ] } }
is equivalent to:
{ $and: [ { tags: "ssl" }, { tags: "security" } ] }
But overall there are many ways to get the same result with mongo just like there are many ways to get the same exact result with JS etc.

Apply an expression to each array element in document

I've got a sample document that I'm trying to project within a MongoDB aggregate pipeline. I'm testing with a single document that looks roughly like this:
{
"_id" : "",
"title" : "Questions",
"sortIndex" : 0,
"topics" : [
{
"_id" : "",
"title" : "Creating a Question",
"sortIndex" : 1,
"thumbnail" : "CreatingAQuestion.jpg",
"seenBy" : [ "user101", "user202" ],
"pages" : [
{
"visual" : "SelectPlanets.gif",
"text" : "Some Markdown"
}
]
},
{
"_id" : "",
"title" : "Deleting a Question",
"sortIndex" : 0,
"thumbnail" : "DeletingAQuestion.jpg",
"seenBy" : [ "user101" ],
"pages" : [
{
"visual" : "SelectCard.gif",
"text" : "Some Markdown"
}
]
}
]
}
The output I'm trying to obtain is something along these lines:
{
"_id" : "",
"title" : "Questions",
"topics" : [
{
"title" : "Creating a Question",
"thumbnail" : "CreatingAQuestion.jpg",
"seen" : true
},
{
"title" : "Deleting a Question",
"thumbnail" : "DeletingAQuestion.jpg",
"seen" : false
}
]
}
Specifically the bit I'm struggling with is the seen flag.
I've read the docs which state:
When projecting or adding/resetting a field within an embedded document...
... Or you can nest the fields:
contact: { address: { country: <1 or 0 or expression> } }
I wish to use an expression and I took note of the following:
When nesting the fields, you cannot use dot notation inside the embedded document to specify the field, e.g. contact: { "address.country": <1 or 0 or expression> } is invalid.
So I'm trying to work out how to "reference" a subdocument field within an expression. That quote suggests I can't use dot notation but when I can't seem to get it working with nested notation either. Here's what I've got so far:
db
.getCollection('chapters')
.aggregate([
{
$project: {
title: 1,
topics: {
title: 1,
thumbnail: 1,
publishedAt: 1,
test: "$seenBy",
seen: { $in: ["user202", "$seenBy"] },
}
}
}
])
So I've hard coded user202 into my query for now, and expected to see true and false for the 2 documents. I've also put in a test field to map out the seenBy field from the sub-document. What this produces is:
{
"_id" : "",
"title" : "Questions",
"topics" : [
{
"title" : "Creating a Question",
"thumbnail" : "CreatingAQuestion.jpg",
"test" : [
"user101",
"user202"
],
"seen" : true
},
{
"title" : "Deleting a Question",
"thumbnail" : "DeletingAQuestion.jpg",
"test" : [
"user101",
"user202"
],
"seen" : true
}
]
}
So obviously my "$seenBy" isn't accessing the correct topic because the test field contains the data from the 1st document.
So ultimately my question is, how can I access the seenBy field within a subdocument, referring to the current subdocument so I can create an expression?
Note: I have got this working with multiple $project and an $unwind but wanted to try compress/clean it up a bit.
You really need to use $map here. Simply notating the array in projection ( which is a bit of a boon since MongoDB 3.2 ) does not really cut it when you need a localized value for the current element. That is what you need and it's what $map provides:
db.getCollection('chapters').aggregate([
{ $project: {
title: 1,
topics: {
$map: {
input: "$topics",
as: "t",
in: {
title: "$$t.title",
thumbnail: "$$t.thumbnail",
publishedAt: "$$t.publishedAt",
test: "$$t.seenBy",
seen: { $in: ["user202", "$$t.seenBy"] },
}
}
}}
])
So for each element the current value of "seenBy" as a property is being tested by the expression. Without the $map that is not possible, and you can only really notate the "whole" array. Which is really not what you want to test here.

What is causing "The dollar ($) prefixed field '$conditionalHandlers' in 'collaborators..$conditionalHandlers' is not valid for storage."

I am writing a Node/Express/Mongoose (latest versions) application which has "Projects" with a list of "collaborators" which are IDS of "Users". Until now, I've been storing the list of foreign keys as hex strings. This is now making it difficult to perform some slightly more complex aggregation, so I have decided to store them as ObjectId type instead, which makes the joins simpler.
In the function which creates the array, the push(userId) version works fine, adding collaborators to the array. However pushing an ObjectId into the array, or assigning an array containing an ObjectId fails with
"The dollar ($) prefixed field '$conditionalHandlers' in
'collaborators..$conditionalHandlers' is not valid for storage."
function addCollaborator(projectId, userId, fn){
projectModel.findById(projectId, (err, project)=>{
if(err) return fn(err);
project.collaborators.push( new Schema.Types.ObjectId(userId)); // errors
// project.collaborators = [ new Schema.Types.ObjectId(userId) ]; // errors
// project.collaborators.push( userId); // works
project.save((err)=>{
logService.error('Error adding collaborator to project: '+err.toString());
});
fn(null);
});
}
Project model:
const ProjectSchema = new mongoose.Schema({
name: String,
create_date: Date,
administrators: Array, // list of user._id
collaborators: Array, // list of user._id ObjectIds
});
With the text IDs, I get projects looking like:
{ "_id" : ObjectId("594e2222a26ca3505c18c674"),
"name" : "Pips 2nd Project", "create_date" : ISODate("2017-06-24T08:26:10.498Z"),
"collaborators" : [ "5936a3576d6c5a3ef4ee0936" ],
"administrators" : [ "594dbba8186f1a2f5ad7539c" ], "__v" : 1 }
When it breaks, I log the error, and am left with an empty array:
{ "_id" : ObjectId("594e278b6a68a2815b043bd1"),
"name" : "Pips third Project", "create_date" : ISODate("2017-06-24T08:49:15.091Z"),
"collaborators" : [ ],
"administrators" : [ "594dbba8186f1a2f5ad7539c" ], "__v" : 0 }
What I want to achieve is:
{ "_id" : ObjectId("594e2222a26ca3505c18c674"),
"name" : "Pips 2nd Project", "create_date" : ISODate("2017-06-24T08:26:10.498Z"),
"collaborators" : [ Object("5936a3576d6c5a3ef4ee0936") ],
"administrators" : [ "594dbba8186f1a2f5ad7539c" ], "__v" : 1 }
I've seen a few other SO's or github issues, but none seem to explain the problem. This one has the same problem, but "solved" it by using strings - which is the opposite of my issue.
After reading some other posts (e.g.), I realised I was using the wrong method to create an ObjectId from a hex string.
new Schema.Types.ObjectId(userId) // not right!
Should be:
mongoose.Types.ObjectId(userId)
So this works as expected now:
project.collaborators.push( mongoose.Types.ObjectId(userId));
and produces:
{ "_id" : ObjectId("594e278b6a68a2815b043bd1"),
"name" : "Pips third Project", "create_date" : ISODate("2017-06-24T08:49:15.091Z"),
"collaborators" : [ ObjectId("5936a3576d6c5a3ef4ee0936") ],
"administrators" : [ "594dbba8186f1a2f5ad7539c" ], "__v" : 1 }

how to update an array inside a MongoDB collection with another array but update only changed values?

Having the following collection named eshops:
{
"_id" : ObjectId("53e87e239ae974e6a0a81004"),
"name" : "www.example.com",
"products" : [
{
"name" : "apple", //lets say the name key here is primary key of products
"status" : 0
},
{
"name" : "banana",
"status" : 0
},
{
"name" : "iphone",
"status" : 0
}
]
}
and having this array
var products = [
{name: "apple", status: 1}
{name: "notebook", status: 0}
]
What should the update query look like if I wanted the following result?
{
"_id" : ObjectId("53e87e239ae974e6a0a81004"),
"name" : "www.example.com",
"products" : [
{
"name" : "apple",
"status" : 1
},
{
"name" : "banana",
"status" : 0
},
{
"name" : "iphone",
"status" : 0
},
{
"name" : "notebook",
"status" : 0
}
]
}
The full explaination of this is at the end so read on.
That cannot Cannot be done "atomically" in a single operation and the best you will get is "Bulk" operations which is the best way to do it.
var products = [
{name: "apple", status: 1}
{name: "notebook", status: 0}
];
var bulk = db.collection.initializeOrderedBulkOp();
products.forEach(function(product) {
// Try to update
bulk.find({
"_id" : ObjectId("53e87e239ae974e6a0a81004"),
"products.name": product.name
})
.updateOne({
"$set": { "products.$.status": product.status }
});
// Try to "push"
bulk.find({
"_id" : ObjectId("53e87e239ae974e6a0a81004"),
"products.name": { "$ne": product.name }
})
.updateOne({
"$push": { "products": product }
});
});
bulk.execute();
The other alternative is to retrieve the document via a .findOne() or similar operation, then alter the array content in client code and then .save() the altered content back.
That is what you don't want since there is no guarantee the document has not "changed" since it was read into memory. And if other members were added to the array that sort of action would "overwrite" them.
So loop the items with multiple updates. At least "Bulk" operations send these all at once to the server without waiting for responses from individual writes.
But as you point out. What if the value is still the same? For that you need to look at the "WriteResult" response from the "Bulk" operation on .execute():
WriteResult({ "nMatched" : 2, "nUpserted" : 0, "nModified" : 2 })
So there were two (2) actions here depite four (4) operations being sent in total. If the array contained one more item, say "iphone" without changes:
var products = [
{name: "apple", status: 1}
{name: "notebook", status: 0},
{name: "iphone", status: 0 }
];
Then the response would be this:
WriteResult({ "nMatched" : 3, "nUpserted" : 0, "nModified" : 2 })
Since the "Bulk" API is smart enough to see that the value for "status" on the matching "iphone" is not different to the value already present ( assuming nothing else changed that in between ) and does not report this as a modification.
So let the server do the work, because all the smarts you could code up are really already there.

Categories