MongoDB: how to pass current value to operator? - mongodb

Here is sample db structure where I'm trying to modify value of every work_starts_at.
{
"_id": 1,
"work_week": [
{'name': 'ponedeljak', 'work_hours': []},
{'name': 'utorak', 'work_hours': []},
]
},
{
"_id": 2,
"work_week": [
{'name': 'monday', 'work_hours': [
{'work_starts_at': 1, 'work_ends_at': 2}
]},
]
},
{
"_id": 3,
"work_week": [
{'name': 'понедельник', 'work_hours': [
{'work_starts_at': 2, 'work_ends_at': 3},
{'work_starts_at': 6, 'work_ends_at': 7},
]},
{'name': 'вторник', 'work_hours': []},
]
}
The best solution I came to is the following (in python), but instead of subtracting 5 from current value of work_week.work_hours.work_starts_at it's traversing I got null. I suppose it's because construction $$CURRENT.work_hours.work_starts_at doesn't point to work_starts_at, so I'm actually subtracting 5 from null.
How can I properly address value of currently traversed element?
collection.update_many(
{},
[
{
'$set': {
'work_week.work_hours.work_starts_at': {
'$subtract': ['$$CURRENT.work_hours.work_starts_at', 5]
}
}
}
]

You can do this way from the mongo shell (it must be very similar in python):
db.collection.update({
"work_week.work_hours.work_starts_at": {
$exists: true
}
},
{
$inc: {
"work_week.$[].work_hours.$[].work_starts_at": -5
}
},
{
multi: true
})
playground

Thanks to #R2D2! I've translated it to Python (pymongo):
collection.update_many(
{'work_week.work_hours.work_starts_at': {'$exists': True}},
{'$inc': {
'work_week.$[].work_hours.$[].work_starts_at': -5
}}
)

Related

MongoDB: Upsert with array filter

I have collection like this:
mc.db.collection.insert_many([
{"key_array": [1], "another_array": ["a"]},
{"key_array": [2, 3], "another_array": ["b"]},
{"key_array": [4], "another_array": ["c", "d"]},
])
And I'm using this kind of updates:
mc.db.collection.update_one(
{"key_array": 5},
{"$addToSet": {"another_array": "f"}},
upsert=True
)
It works good with updates, but I have trouble when trying to upsert:
It creates a document with a non-array key_array field, like this
{
"_id": ObjectId(...)
"key_array": 5,
"another_array": ["f"]
}
while I want to have this one
{
"_id": ObjectId(...)
"key_array": [5],
"another_array": ["f"]
}
Also, I cannot use the {"key_array": [5]} style query, because it won't match the existing array with length > 1.
So, is there any chance to save such behavior on updates, and receive the correct document structure on inserts?
Any help will be appreciated
This should help.
https://www.mongodb.com/docs/manual/reference/operator/update/setOnInsert/
mc.db.collection.update_one(
{"key_array": 5},
{
"$addToSet": {"another_array": "f"},
"$setOnInsert": {"key_array": [5], ...}
},
upsert=True
)
how about this one.
db.collection.update({
"key_array": 5
},
{
"$addToSet": {
"another_array": "f",
},
"$set": {
"key_array": [
5
]
}
},
{
"upsert": true
})
https://mongoplayground.net/p/4YdhKuzr2I6
Ok, finally I had solved this issue with two consecutive updates, the first as specified in the question - upserts with non-array query field, and the second which converts the field to an array if it belongs to another type.
from pymongo import UpdateOne
bulk = [
UpdateOne(
{"key_array": 6},
{"$addToSet": {"another_array": "e"}},
upsert=True
),
UpdateOne(
{
"$and": [{"key_array": 6},
{"key_array": {"$not": {"$type": "array"}}}]
},
[{"$set": { "key_array": ["$key_array"]}}]
)
]
mc.db.collection.bulk_write(bulk)
But I'm still looking for a more elegant way to do such a thing.

How to reverse boolean in mongodb with mongoose

I have a data as below.
[
{
"id": 1,
"exist": true
},
{
"id": 2,
"exist": false
},
{
"id": 3,
"exist": false
}
]
Only one object can have exist true. So when I findOneAndUpdate({_id:2}),{exist:true}), I hope that exist of 'id:1' is changed to false automatically in one query using aggregate or etc.
could you recommend some idea fot it? Thank you so much for reading my question.
Starting in MongoDB 4.2, you can use the aggregation pipeline for updates so you can do something like this:
db.your_collection.update(
{
$or: [
{
id: 2,
exist: false
},
{
id: {$ne: 2},
exist: true
}
]
},
[{$set: {exist: {$eq: [ "$exist", false ] }}}],
{multi: true}
)
Explain:
The filter will find records that has id you want and not exist or don't have the id but exist is true. In this case, it will find:
[
{
"id": 1,
"exist": true
},
{
"id": 2,
"exist": false
}
]
The update reverse exist field of found records.

How to get corresponding property of min value from an array

I apologise if it is duplicated. I have a collection like this:
{
game_name:"ABC",
...,
prices:[
{area: 'US', price_usd: 10},
{area: 'AU', price_usd: 11},
...
]
},
...
I can get:
{game_name:"ABC", min_price:"10"}
...
by:
db.games.aggregate({
$project:{
"game_name":1,
"min_price":{
$min:"$prices.price_usd"
}
}
})
However, the query result what I want is:
{game_name:"ABC", min_price:"10", min_area: "US"}
...
Anyone can help? Thanks
You can use below aggregation
db.games.aggregate([
{ "$project": {
"game_name": 1,
"min_price": {
"$min": "$prices.price_usd"
},
"min_area": {
"$arrayElemAt": [
"$prices.area",
{ "$indexOfArray": ["$prices.price_usd", { "$min": "$prices.price_usd" }] }
]
}
}}
])

How to add every other columns together in Mongo?

I've been cracking my head over the addition of every 'other' columns together during aggregation in Mongo.
A sample of my data:
[
{'item': 'X',
'USA': 3,
'CAN': 1,
'CHN': 1,
'IDN': 1,
:
:
:
},
{'item': 'R',
'USA': 2,
'CAN': 2,
'CHN': 1,
'IDN': 2,
:
:
:
}
]
At the aggregate stage, I would like to have a new field called 'OTHER', which is the resultant of the summation of all the fields that are not specified.
My desired result is this:
[
{'item': 'X',
'NAM': 79,
'IDN': 51,
'OTHER': 32
},
{'item': 'R',
'NAM': 42,
'IDN': 11,
'OTHER': 20
}
]
So far, the closest I could get is using this:
mycoll.aggregate([
{'$addFields':{
'NAM': {'$add':[{'$ifNull':['$CAN', 0]},{'$ifNull':['$USA', 0]}]},
'INDIA': {'$ifNull':['$IDN', 0]},
'OTHER': /* $add all the fields that are not $USA, $CAN, $IDN*/
}},
])
Mongo gurus, please enlighten this poor soul. Deeply appreciate it. Thanks!
In general the idea is converting your document to an array so we could iterate over it while ignoring unwanted fields.
{
'$addFields': {
'NAM': {'$add': [{'$ifNull': ['$CAN', 0]}, {'$ifNull': ['$USA', 0]}]},
'INDIA': {'$ifNull': ['$IDN', 0]},
"OTHER": {
$reduce:
{
input: {"$objectToArray": "$$ROOT"},
initialValue: {sum: 0},
in: {
sum: {
$cond: {
if: {$in: ["$$this.k", ['_id', "item", "CAN", "USA", "IDN"]]},
then: "$$value.sum",
else: {$add: ["$$value.sum", "$$this.v"]}
}
}
}
}
}
}
}
Obivously you should also add any other fields that you have in your document that you do not want to sum up / are not of type number.

MongoDB: match non-empty doc in array

I have a collection structured thusly:
{
_id: 1,
score: [
{
foo: 'a',
bar: 0,
user: {user1: 0, user2: 7}
}
]
}
I need to find all documents that have at least one 'score' (element in score array) that has a certain value of 'bar' and a non-empty 'user' sub-document.
This is what I came up with (and it seemed like it should work):
db.col.find({score: {"$elemMatch": {bar:0, user: {"$not":{}} }}})
But, I get this error:
error: { "$err" : "$not cannot be empty", "code" : 13030 }
Any other way to do this?
Figured it out: { 'score.user': { "$gt": {} } } will match non-empty docs.
I'm not sure I quite understand your schema, but perhaps the most straight forward way would be to not have an "empty" value for score.user ?
Instead purposely not have that field in your document if it has no content?
Then your query could be something like ...
> db.test.find({ "score" : { "$elemMatch" : { bar : 0, "user" : {"$exists": true }}}})
i.e. looking for a value in score.bar that you want (0 in this case) checking for the mear existence ($exists, see docs) of score.user (and if it has a value, then it'll exist?)
editied: oops I missed the $elemMatch you had ...
You probably want to add an auxiliary array that keeps track of the users in the user document:
{
_id: 1,
score: [
{
foo: 'a',
bar: 0,
users: ["user1", "user2"],
user: {user1: 0, user2: 7}
}
]
}
Then you can add new users atomically:
> db.test.update({_id: 1, score: { $elemMatch: {bar: 0}}},
... {$set: {'score.$.user.user3': 10}, $addToSet: {'score.$.users': "user3"}})
Remove users:
> db.test.update({_id: 1, score: { $elemMatch: {bar: 0}}},
... {$unset: {'score.$.user.user3': 1}, $pop: {'score.$.users': "user3"}})
Query scores:
> db.test.find({_id: 1, score: {$elemMatch: {bar: 0, users: {$not: {$size: 0}}}}})
If you know you'll only be adding non-existent users and removing existent users from the user document, you can simplify users to a counter instead of an array, but the above is more resilient.
Look at the $size operator for checking array sizes.
$group: {
_id: '$_id',
tasks: {
$addToSet: {
$cond: {
if: {
$eq: [
{
$ifNull: ['$tasks.id', ''],
},
'',
],
},
then: '$$REMOVE',
else: {
id: '$tasks.id',
description: '$tasks.description',
assignee: {
$cond: {
if: {
$eq: [
{
$ifNull: ['$tasks.assignee._id', ''],
},
'',
],
},
then: undefined,
else: {
id: '$tasks.assignee._id',
name: '$tasks.assignee.name',
},
},
},
},
},
},
},