How to multi update of a nested array in MondoDB? - mongodb

I have documents in a MongoDB 'playground' collection following the below "schema" :
{
"_id": ObjectId("54423b40c92f9fffb486a6d4"),
"ProjectFileId": 1,
"SourceLanguageId": 2,
"TargetSegments": [
{
"LanguageId": 1,
"Segment": "Something",
"Colors": [
1,
2,
3
],
"Heights": [
1,
2,
3
],
"Widths": [
1,
2,
3
]
},
{
"LanguageId": 1,
"Segment": "Something",
"Colors": [
1,
2,
3
],
"Heights": [
1,
2,
3
],
"Widths": [
1,
2,
3
]
}
]
}
And the following update query:
db.playground.update({
$and: [
{
"TargetSegments.Colors": {
$exists: true
}
},
{
"ProjectFileId": 1
},
{
"SourceLanguageId": 2
},
{
"TargetSegments": {
$elemMatch: {
"LanguageId": 1
}
}
}
]
},
{
$set: {
"TargetSegments.$.Segment": null,
"TargetSegments.$.Colors": [],
"TargetSegments.$.Widths": [],
"TargetSegments.$.Heights": []
}
},
false, true)
After the execution of the query the result is:
{
"_id": ObjectId("54423b40c92f9fffb486a6d4"),
"ProjectFileId": 1,
"SourceLanguageId": 2,
"TargetSegments": [
{
"LanguageId": 1,
"Segment": null,
"Colors": [],
"Heights": [],
"Widths": []
},
{
"LanguageId": 1,
"Segment": "Something",
"Colors": [
1,
2,
3
],
"Heights": [
1,
2,
3
],
"Widths": [
1,
2,
3
]
}
]
}
As you can see, only the first element of the "TargetSegments" array is updated.
How can I update all the elements of the TargetSegments array in one update query?

Its because you are using $ operator: The positional $ operator identifies an element (not multi) in an array to update without explicitly specifying the position of the element in the array. To project, or return, an array element from a read operation, see the $ projection operator.
You can use below code to do it:
db.playground.find({
$and: [
{
"TargetSegments.Colors": {
$exists: true
}
},
{
"ProjectFileId": 1
},
{
"SourceLanguageId": 2
},
{
"TargetSegments": {
$elemMatch: {
"LanguageId": 1
}
}
}
]
}).forEach(function(item)
{
var targets = item.TargetSegments;
for(var index = 0; index < targets.length; index++)
{
var target = targets[index];
target.Segment = null,
target.Colors= [],
target.Widths= [],
target.Heights= []
}
db.playground.save(item);
});

Related

Comparing 2 fields from $project in a mongoDB pipeline

In a previous post I created a mongodb query projecting the number of elements matching a condition in an array. Now I need to filter this number of elements depending on another field.
This is my db :
db={
"fridges": [
{
_id: 1,
items: [
{
itemId: 1,
name: "beer"
},
{
itemId: 2,
name: "chicken"
}
],
brand: "Bosch",
size: 195,
cooler: true,
color: "grey",
nbMax: 2
},
{
_id: 2,
items: [
{
itemId: 1,
name: "beer"
},
{
itemId: 2,
name: "chicken"
},
{
itemId: 3,
name: "lettuce"
}
],
brand: "Electrolux",
size: 200,
cooler: true,
color: "white",
nbMax: 2
},
]
}
This is my query :
db.fridges.aggregate([
{
$match: {
$and: [
{
"brand": {
$in: [
"Bosch",
"Electrolux"
]
}
},
{
"color": {
$in: [
"grey",
"white"
]
}
}
]
}
},
{
$project: {
"itemsNumber": {
$size: {
"$filter": {
"input": "$items",
"as": "item",
"cond": {
$in: [
"$$item.name",
[
"beer",
"lettuce"
]
]
}
}
}
},
brand: 1,
cooler: 1,
color: 1,
nbMax: 1
}
}
])
The runnable example.
Which gives me this :
[
{
"_id": 1,
"brand": "Bosch",
"color": "grey",
"cooler": true,
"itemsNumber": 1,
"nbMax": 2
},
{
"_id": 2,
"brand": "Electrolux",
"color": "white",
"cooler": true,
"itemsNumber": 2,
"nbMax": 2
}
]
What I expect is to keep only the results having a itemsNumber different from nbMax. In this instance, the second fridge with _id:2 would not match the condition and should not be in returned. How can I modify my query to get this :
[
{
"_id": 1,
"brand": "Bosch",
"color": "grey",
"cooler": true,
"itemsNumber": 1,
"nbMax": 2
}
]
You can put a $match stage with expression condition at the end of your query,
$ne to check both fields should not same
{
$match: {
$expr: { $ne: ["$nbMax", "$itemsNumber"] }
}
}
Playground

Create array if not exists or append item to existing

I need to add items to a specific subobject in my collection. Is is possible to do this using just the update method considering that the array may not exist in some objects?
Here's a sample structure:
[
{
"key": 1,
"someValue": "abc",
"installments": [
{
"number": 1,
"payments": [
{
"paymentNumber": 1,
"value": 29
},
{
"paymentNumber": 2,
"value": 22
}
]
}
]
},
{
"key": 2,
"someValue": "xyz",
"installments": [
{
"number": 1,
"payments": [
{
"paymentNumber": 3,
"value": 10
},
{
"paymentNumber": 4,
"value": 1
},
{
"paymentNumber": 5,
"value": 5
}
]
}
]
}
]
And here's the syntax I'm using:
db.collection.update({
"installments.payments.paymentNumber": 4
},
{
"$push": {
"installments.payments": [
{
"receipts": [
{
"receiptNumber": 1
}
]
}
]
}
})
This is causing this error:
fail to run update: multiple write errors: [{write errors: [{Cannot create field 'payments' in element {installments: [ { number: 1.0, payments: [ { paymentNumber: 3.0, value: 10.0 }, { paymentNumber: 4.0, value: 1.0 }, { paymentNumber: 5.0, value: 5.0 } ] } ]}}]}, {<nil>}]
Is it possible?
Here's the Mongo Playground link
And here's the expected result:
[
{
"key": 1,
"someValue": "abc",
"installments": [
{
"number": 1,
"payments": [
{
"paymentNumber": 1,
"value": 29
},
{
"paymentNumber": 2,
"value": 22
}
]
}
]
},
{
"key": 2,
"someValue": "xyz",
"installments": [
{
"number": 1,
"payments": [
{
"paymentNumber": 3,
"value": 10
},
{
"paymentNumber": 4,
"value": 1,
"receipts": [{
"receiptNumber": 1
}]
},
{
"paymentNumber": 5,
"value": 5
}
]
}
]
}
]
To update nested arrays, the filtered positional operator $[identifier] identifies the array elements that match the arrayFilters conditions for an update operation.
Try the following query to $push in nested array:
db.collection.update({
"installments.payments.paymentNumber": 4,
},
{
"$push": {
"installments.$.payments.$[payments].receipts": {
"receiptNumber": 1
}
}
},
{
"arrayFilters": [
{
"payments.paymentNumber": 4
}
],
multi: true
})
MongoDB Playground

Updating nested array of documents as object

I have a collection called 'A' with thousands of documents in it. Here grades is the array of sub documents which consist attribute questions which is array of number like below
{ "_id" : 1,
"grades" : [
{ type: "quiz", questions: [ 10, 8, 5 ] },
{ type: "quiz", questions: [ 8, 9, 6 ] },
{ type: "hw", questions: [ 5, 4, 3 ] },
{ type: "exam", questions: [ 25, 10, 23, 0 ] }
]
}
In order to add additional feature we have to modify the format of questions and make it array of documents which has two fields one is number (which will inherit the old data) and another one is attempts(by default it will be empty array and may have value 'P'- Pass or 'F'- Fail). After this transformation the document should look like below
{ "_id" : 1,
"grades" : [
{
type: "quiz",
questions:[
{ number: 10, attempts: ['F', 'P'] },
{ number: 8, attempts: [] },
{ number: 5, attempts: ['P'] }
]
},
{
type: "quiz",
questions:
[
{ number: 8, attempts: ['F', 'P'] },
{ number: 9, attempts: [] },
{ number: 6, attempts: ['P'] }
]
},
{
type: "hw",
questions:
[
{ number: 5, attempts: ['F', 'P'] },
{ number: 4, attempts: [] },
{ number: 3, attempts: ['P'] }
]
},
{
type: "exam",
questions:
[
{ number: 25, attempts: ['F', 'P'] },
{ number: 10, attempts: [] },
{ number: 23, attempts: ['P'] },
{ number: 0, attempts: ['P', 'P'] }
]
}
]
}
Appreciate for all the help
You can test yourself here: https://mongoplayground.net/p/ZEp_vk5hhf7
Brief explanation:
You have to use $map two times.
First you have to loop the $grades field, because he is an array.
As you are looping through $grades array, you have to loop through the $questions field, because he is an array too.
Query:
db.collection.aggregate([
{
"$project": {
"grades": {
"$map": {
"input": "$grades",
"as": "g",
"in": {
"type": "$$g.type",
"questions": {
"$map": {
"input": "$$g.questions",
"as": "q",
"in": {
"number": "$$q",
"attempts": []
}
}
}
}
}
}
}
}
])
Result:
[
{
"_id": 1,
"grades": [
{
"questions": [
{
"attempts": [],
"number": 10
},
{
"attempts": [],
"number": 8
},
{
"attempts": [],
"number": 5
}
],
"type": "quiz"
},
{
"questions": [
{
"attempts": [],
"number": 8
},
{
"attempts": [],
"number": 9
},
{
"attempts": [],
"number": 6
}
],
"type": "quiz"
},
{
"questions": [
{
"attempts": [],
"number": 5
},
{
"attempts": [],
"number": 4
},
{
"attempts": [],
"number": 3
}
],
"type": "hw"
},
{
"questions": [
{
"attempts": [],
"number": 25
},
{
"attempts": [],
"number": 10
},
{
"attempts": [],
"number": 23
},
{
"attempts": [],
"number": 0
}
],
"type": "exam"
}
]
}
]

Mongo query for an array is a subarray

I'm looking for a query that acts as $setIsSubset, except accounting for duplicate values.
For example, [1,1,2,3] is a subset of [1,2,3,4], because sets don't have duplicate values.
How can I write a query such that [1,1,2,3] is not a subset of [1,2,3,4]?
An example of expected outputs:
INPUT | TARGET | RESULT
[1] [1,2,3,4] TRUE
[1,2,3] [1,2,3,4] TRUE
[1,1,2,3] [1,2,3,4] FALSE
[1,2,3,4] [1,2,3,4] TRUE
[1,3] [1,2,3,4] TRUE
[1,11,5] [1,2,3,4] FALSE
[1,2,2,3] [1,2,3,4] FALSE
I would suggest not to do such heavy processing in mongo query as you can do the same task easily in any programming language. But, if you still need it in mongo, the following query can get you the expected output, provided both input and target arrays are sorted.
db.collection.aggregate([
{
$project:{
"modifiedInput":{
$reduce:{
"input":"$input",
"initialValue":{
"data":[],
"postfix":0,
"index":0,
"nextElem":{
$arrayElemAt:["$input",1]
}
},
"in":{
"data":{
$concatArrays:[
"$$value.data",
[
{
$concat:[
{
$toString:"$$this"
},
"-",
{
$toString:"$$value.postfix"
}
]
}
]
]
},
"postfix":{
$cond:[
{
$eq:["$$this","$$value.nextElem"]
},
{
$sum:["$$value.postfix",1]
},
0
]
},
"nextElem": {
$arrayElemAt:["$input", { $sum : [ "$$value.index", 2] }]
},
"index":{
$sum:["$$value.index",1]
}
}
}
},
"modifiedTarget":{
$reduce:{
"input":"$target",
"initialValue":{
"data":[],
"postfix":0,
"index":0,
"nextElem":{
$arrayElemAt:["$target",1]
}
},
"in":{
"data":{
$concatArrays:[
"$$value.data",
[
{
$concat:[
{
$toString:"$$this"
},
"-",
{
$toString:"$$value.postfix"
}
]
}
]
]
},
"postfix":{
$cond:[
{
$eq:["$$this","$$value.nextElem"]
},
{
$sum:["$$value.postfix",1]
},
0
]
},
"nextElem": {
$arrayElemAt:["$target", { $sum : [ "$$value.index", 2] }]
},
"index":{
$sum:["$$value.index",1]
}
}
}
}
}
},
{
$project:{
"_id":0,
"matched":{
$eq:[
{
$size:{
$setDifference:["$modifiedInput.data","$modifiedTarget.data"]
}
},
0
]
}
}
}
]).pretty()
Data set:
{
"_id" : ObjectId("5d6e005db674d5c90f46d355"),
"input" : [
1
],
"target" : [
1,
2,
3,
4
]
}
{
"_id" : ObjectId("5d6e005db674d5c90f46d356"),
"input" : [
1,
2,
3
],
"target" : [
1,
2,
3,
4
]
}
{
"_id" : ObjectId("5d6e005db674d5c90f46d357"),
"input" : [
1,
1,
2,
3
],
"target" : [
1,
2,
3,
4
]
}
{
"_id" : ObjectId("5d6e005db674d5c90f46d358"),
"input" : [
1,
2,
3,
4
],
"target" : [
1,
2,
3,
4
]
}
{
"_id" : ObjectId("5d6e005db674d5c90f46d359"),
"input" : [
1,
3
],
"target" : [
1,
2,
3,
4
]
}
{
"_id" : ObjectId("5d6e005db674d5c90f46d35a"),
"input" : [
1,
5,
11
],
"target" : [
1,
2,
3,
4
]
}
{
"_id" : ObjectId("5d6e005db674d5c90f46d35b"),
"input" : [
1,
2,
2,
3
],
"target" : [
1,
2,
3,
4
]
}
Output:
{ "matched" : true }
{ "matched" : true }
{ "matched" : false }
{ "matched" : true }
{ "matched" : true }
{ "matched" : false }
{ "matched" : false }
Explanation: To avoid elimination of same values, we are adding the postfix counter to each. For example, [1,1,1,2,3,3,4,4] would become ["1-0","1-1","1-2","2-0","3-0","3-1","4-0","4-1","4-2"]. Afrer the conversion of both input and target arrays, the set difference is calculated. It's a match, if the size of set difference is zero.
You can try below aggregation:
let input = [1,2,3];
let inputSize = 3;
db.collection.aggregate([
{
$project: {
uniqueTarget: { $setUnion: [ "$target" ] }
}
},
{
$addFields: {
filtered: {
$reduce: {
input: input,
initialValue: "$uniqueTarget",
in: {
$filter: {
input: "$$value",
as: "current",
cond: { $ne: [ "$$this", "$$current" ] }
}
}
}
}
}
},
{
$project: {
result: {
$eq: [
{ $size: "$filtered" },
{ $subtract: [ { $size: "$uniqueTarget" }, inputSize ] }
]
}
}
}
])
It starts with $setUnion to ensure there are no duplicates in target array. Then you can run $reduce to iterate through input and remove currently processed element from target. Every iteration should remove single element so expected $size of filtered is equal $size of uniqueTarget - inputSize
Mongo Playground (1)
Mongo Playground (2)

MongoDB aggregation: How can I get a count of columns based on a criteria

I have the following documents:
{ "col1": "camera", "fps": 1, "lat": 3 },
{ "col1": "camera", "fps": 3, "lat": 2 }
{ "col1": "camera", "foo": 9, "bar": 7 }
{ "col1": "camera", "bar": 8, "bar": 1 }
{ "col1": "camera", "check": 4, "lat": 3 }
How can I get the following:
{ "fps": 2, "lat": 3, "bar": 3, "check": 1, "foo": 1 }
Where each of these values are the number of occurrences (count) of each key (fps appears 2, foo, appears once, etc)
Aggregate
Here a solution (i named my collection cameras) if keys are known in advance :
db.cameras.aggregate([
{
$project: {
_id: 1,
fps: {
$ifNull: [ "$fps", 0 ]
},
lat: {
$ifNull: [ "$lat", 0 ]
},
bar: {
$ifNull: [ "$bar", 0 ]
},
foo: {
$ifNull: [ "$foo", 0 ]
},
check: {
$ifNull: [ "$check", 0 ]
}
}
},
{
$project: {
_id: 1,
fps: {
$cond: [ { $eq: [ "$fps", 0 ] } , 0, 1 ]
},
lat: {
$cond: [ { $eq: [ "$lat", 0 ] } , 0, 1 ]
},
bar: {
$cond: [ { $eq: [ "$bar", 0 ] } , 0, 1 ]
},
foo: {
$cond: [ { $eq: [ "$foo", 0 ] } , 0, 1 ]
},
check: {
$cond: [ { $eq: [ "$check", 0 ] } , 0, 1 ]
}
}
},
{
$group: {
_id: null,
fps: {
$sum: "$fps"
},
lat: {
$sum: "$lat"
},
bar: {
$sum: "$bar"
},
foo: {
$sum: "$foo"
},
check: {
$sum: "$check"
}
}
}
])
Results :
{
"result" : [
{
"_id" : null,
"fps" : 2,
"lat" : 3,
"bar" : 2,
"foo" : 1
}
],
"ok" : 1
}
MapReduce
Another solution if keys are unknown is mapReduce :
db.cameras.mapReduce(
function() {
var keys = Object.keys(this);
keys.forEach(function(key) {
emit(key, 1);
});
},
function(key, values) {
return Array.sum(values);
},
{
query: {},
out: {
inline: 1
}
}
)
Results :
{
"results" : [
{
"_id" : "_id",
"value" : 5
},
{
"_id" : "bar",
"value" : 2
},
{
"_id" : "check",
"value" : 1
},
{
"_id" : "col1",
"value" : 5
},
{
"_id" : "foo",
"value" : 1
},
{
"_id" : "fps",
"value" : 2
},
{
"_id" : "lat",
"value" : 3
}
],
"timeMillis" : 1,
"counts" : {
"input" : 5,
"emit" : 19,
"reduce" : 5,
"output" : 7
},
"ok" : 1,
}
But the result is not exactly in the same format.