Mongodb Update Elements from Multiple Arrays on a Document - mongodb

I'd like to update a property from objects located in multiple lists on a MongoDB collection's document.
In a MongoDB collection I have documents like this one:
{
"firstArray": [
{
"name": "john",
"updated": false // I wanna switch that to true
},
{
"name": "bob",
"updated": false
}
],
"secondArray": [
{
"name": "eric",
"updated": false
},
{
"name": "john",
"updated": false // I wanna switch that to true
}
]
}
My goal is to update every element with "name":"john" from document's properties firstArray and secondArray to updated:true.
As they can be parallel accesses to a document, I shall only use update operators (a read and replace method like that one would add the risk of dropping changes made by another process)
It looks like the filtered positional operator $[] as mentioned in that question is the way to go to update an object's property from a document's Array BUT it appears that it doesn't support multiple $set with the same operator nor using two operators (one by array) in two different $set!
As this mongo shell command only update the secondArray:
db['collection'].update(
{},
{
"$set": { "firstArray.$[elem].updated": true },
"$set": { "secondArray.$[elem].updated": true }
},
{ "arrayFilters": [{ "elem.name": "john" }], "multi": true })
And this command returns The array filter for identifier 'a' was not used in the update { $set: { secondArray.$[b].updated: true } }:
db['collection'].update(
{},
{
"$set": { "firstArray.$[a].updated": true },
"$set": { "secondArray.$[b].updated": true }
},
{ "arrayFilters": [{ "a.name": "john" }, { "b.name": "john" }], "multi": true })
So, would you have any idea how to do that update? (Bonus point for any documentation quotes about the above limitations )
Thanks!

The second argument to that function in an object. A given field name can only have one value in an object. To show what is being sent to the server, I assigned the value you are using to a variable:
> var o={
"$set": { "firstArray.$[a].updated": true },
"$set": { "secondArray.$[b].updated": true }
};
> printjson(o)
{ "$set" : { "secondArray.$[b].updated" : true } }
As you can see, the first value was overwritten by the second because both fields were named $set.
Try combining these into a single field:
{
"$set": {
"firstArray.$[elem].updated": true,
"secondArray.$[elem].updated": true
}
}

Related

MongoDB - How to add a field to all object within an array

I have a document with a field called info, and info has a field inside it called data. data is an array of objects. I want to add a new boolean field, isActive: false, to each object in data, with updateMany.
This is how it looks now
{
info: {
data: [{
"name": "Max"
},
{
"name": "Brian"
},
...
]
}
}
This is what I want:
{
info: {
data: [{
"name": "Max",
"isActive": false
},
{
"name": "Brian",
"isActive": false
},
...
]
}
}
How do I do that?
Add the isActive field with all positional operator $[].
db.collection.update({},
{
$set: {
"info.data.$[].isActive": false
}
},
{
multi: true
})
Consider applying { multi: true } if you want to update multiple documents.

Mongodb Update a field in a document and then update another depending on the value of previous field

I have a document like this
{
"_id": ObjectId("626f942bb092f78afd9dad9d"),
"item_id": "external _id222",
"metadata": {
"item_name": "abc",
"quantity": 123,
"state": null
},
}
What I would like to do is, $inc i.e. increment the count of quantity and then update the state to SOLD, if quantity equals 124. I can do this by 2 queries, update quantity, do an if-else check and then update state. Is there a way to do this in one single query by update()? (preferably without aggregation)
With MongoDB v4.2+, you can do this with a single update with an aggregation pipeline to achieve atomic behaviour. Use $add to do the increment and $cond to check for quantity = 123
db.collection.update({
"item_id": "external _id222"
},
[
{
$set: {
"metadata.quantity": {
$add: [
"$metadata.quantity",
1
]
}
}
},
{
$set: {
"metadata.state": {
$cond: {
if: {
$eq: [
"$metadata.quantity",
124
]
},
then: "SOLID",
else: "$metadata.state"
}
}
}
}
],
{
multi: true
})
Here is the Mongo playground for your reference.
You can do this way
Check if quantity is 123
Then increment quantity and set state to SOLD
playground
db.collection.update({
"metadata.quantity": 123
},
{
"$inc": {
"metadata.quantity": 1
},
"$set": {
"metadata.state": "SOLD"
}
},
{
"multi": false,
"upsert": false
})
Here, the trick is that you need to check the value which is before $inc operation.

Aggregate documents where objects in array matches multiple conditions

I have a collection with documents similar to such:
{
"_id": ObjectId("xxxxx"),
"item": [
{ "property": ["attr1", "+1"] },
{ "property": ["attr2", "-1"] }
]
}
{
"_id": ObjectId("xxxxy"),
"item": [
{ "property": ["attr1", "-1"] },
{ "property": ["attr2", "0"] }
]
}
{
"_id": ObjectId("xxxxz"),
"item": [
{ "property": ["attr1", "0"] },
{ "property": ["attr2", "+1"] }
]
}
Preferably using an aggregation pipeline, is there any way to match the document if and only if any one of the properties match more than one condition?
For example, I want a query where one object in the array matches both of these conditions:
("item.property": "attr1") AND ("item.property": /^\+/)
That is, a single property where it contains "attr1" and an element that starts with "+".
However, using my current query that looks like this:
collection.aggregate(
{ $match:
{ $and:
[
{ "item.property": "attr1" },
{ "item.property": /^\+/ }
]
}
}
This would match both the first and last document because both contain a property with "attr1" and an element that stats with "+". However, I do not want this query to match the last document, since the element that starts with "+" does not belong to the same object in the array.
Is there any way to achieve this behavior using the aggregation framework?
You can use the below query with $elemMatch to match the array's both values.
Something like
db.collection_name.aggregate({
"$match": {
"item": {
"$elemMatch": {
"property.0": "attr1",
"property.1": /^\+/
}
}
}
});
Also, you can use $all operator if you don't want to reference array index.
db.collection_name.aggregate({
"$match": {
"item": {
"$elemMatch": {
"property": {
"$all": [
"attr1",
/^\+/
]
}
}
}
}
});

How can I unset all document properties except for one or two in mongodb?

I have a collection of documents about entities that have status property that could be 1 or 0. Every document contains a lot of data and occupies space.
I want to get rid of most of the data on the documents with status equal 0.
So, I want every document in the collection that looks like
{
_id: 234,
myCode: 101,
name: "sfsdf",
status: 0,
and: 23243423.1,
a: "dsf",
lot: 3234,
more: "efsfs",
properties: "sdfsd"
}
...to be a lot smaller
{
_id: 234,
mycode: 101,
status: 0
}
So, basically I can do
db.getCollection('docs').update(
{'statusCode': 0},
{
$unset: {
and: "",
a: "",
lot: "",
more: "",
properties: ""
}
},
{multi:true}
)
But there are about 40 properties which would be a huge list, and also I'm not sure that all the objects follow the same schema.
Is there a way to unset all except two properties?
The best thing to do here is to actually throw all the possible properties to $unset and let it do it's job. You cannot "wildcard" such arguments so there really is not a better way without writing to another collection.
If you don't want to type them all out or even know all of them, then simply perform a process to "collect" all the other top level properties.
You can do this for example with .mapReduce():
var fields = db.getCollection('docs').mapReduce(
function() {
Object.keys(this)
.filter(k => k !== '_id' && k !== 'myCode')
.forEach( k => emit(k,1) )
},
function() {},
{
"out": { "inline": 1 }
}
).results.map( o => o._id )
.reduce((acc,curr) => Object.assign(acc,{ [curr]: "" }),{})
Gives you an object with the full fields list to provide to $unset as:
{
"a" : "",
"and" : "",
"lot" : "",
"more" : "",
"name" : "",
"properties" : "",
"status" : ""
}
And that is taken from all possible top level fields in the whole collection.
You can do the same thing with .aggregate() in MongoDB 3.4 using $objectToArray:
var fields = db.getCollection('docs').aggregate([
{ "$project": {
"fields": {
"$filter": {
"input": { "$objectToArray": "$$ROOT" },
"as": "d",
"cond": {
"$and": [
{ "$ne": [ "$$d.k", "_id" ] },
{ "$ne": [ "$$d.k", "myCode" ] }
]
}
}
}
}},
{ "$unwind": "$fields" },
{ "$group": {
"_id": "$fields.k"
}}
]).map( o => o._id )
.reduce((acc,curr) => Object.assign(acc,{ [curr]: "" }),{});
Whatever way you obtain the list of names, then simply send them to $unset:
db.getCollection('docs').update(
{ "statusCode": 0 },
{ "$unset": fields },
{ "multi": true }
)
Bottom like is that $unset does not care if the properties are present in the document or not, but will simply remove them where they exist.
The alternate case is to simply write everything out to a new collection if that also suits your needs. This is a simple use of $out as an aggregation pipeline stage:
db.getCollection('docs').aggregate([
{ "$match": { "statusCode": 0 } },
{ "$project": { "myCode": 1 } },
{ "$out": "newdocs" }
])

How to update multi property value of an object in array in MongoDB?

The following is my database schema update operation:
db.school_student.update({ _id: "003" }, {
$set: {
"result": [
{
"_id": "001",
"isPassed": false
},
{
"_id": "002",
"isPassed": false,
},
{
"_id": "003",
"isPassed": false
}
]
}
});
I want to change ALL the property values of "isPassed" to true. Is there any way to update this? I have been struggling with this the whole day :(
db.school_student.update({},{$Set :{"result.isPassed" : true}}
this should update all the documents in school_student collection and set isPassed to true.