build dynamic MongoDB query to update nested value in dict - mongodb

Considering an object as such:
{
"_id" : ObjectId("5b4dbf3541e5ae6de394cc99"),
"active" : true,
"email" : "admin#something.eu",
"password" : "$2b$12$5qqD3ZulKI7S6j.Wx513POpCNWRMppE.vY4.3EIZedm109VUPqXoi",
"badges" : {
"cnc" : {
"lvl" : "0"
},
"laser" : {
"lvl" : "0"
},
"impression3d" : {
"lvl" : "0"
},
"maker" : {
"lvl" : "0"
},
"electronique" : {
"lvl" : "0"
},
"badge" : {
"lvl" : 1
}
},
"roles" : [
ObjectId("5b4dbf3541e5ae6de394cc97")
]
}
I need to change the lvl value to anywhere between 0 and 5, not necessarily inregular increments. To do this, the parent object name is passed by a variable, thererfor I'm trying to build a dynamic query for a badge lvl to be changed.
Currently the end of my Flask route looks as so:
badge = quiz['badge']
C_user = current_user.get_id()
update = bdd.user.update({"_id": C_user, "badges": { '$elemMatch': {badges : badge}}}, {"$set": { "badges.$.lvl": 3 }}, upsert = True)
But with a query of this sort, I receive an error saying:
pymongo.errors.WriteError: The positional operator did not find the match needed from the query.
Am I totally wrong thinking I need to use the $elemMatch operator? Is there a way to dynamically target the different badges to increment the inner lvl?
Thank you!
Note: I first attempted this with MongoEngine but it seemed impossible to drill down with it.

you cannot do this, badges is an document, not an array. try
...
{"$set": { "badges.badge.lvl": 3 }}
...
But you'll have to explicitely do this for each badges fields.
The way to use your query is to update your scheme in something like
...
"badges" : [
{name:"cnc",
"lvl" : "0"
},
{"name":"laser",
"lvl" : "0"},
...
]
...
EDIT : with some updates on your scheme, here's a way to reach your goal :
New datata Schema :
{
"_id" : ObjectId("5b4dbf3541e5ae6de394cc99"),
"active" : true,
"email" : "admin#something.eu",
"password" : "$2b$12$5qqD3ZulKI7S6j.Wx513POpCNWRMppE.vY4.3EIZedm109VUPqXoi",
"badges" : [
{
"name" : "cnc",
"lvl" : "0"
},
{
"name" : "laser",
"lvl" : "5"
},
{
"name" : "impression3d",
"lvl" : "0"
},
{
"name" : "maker",
"lvl" : "0"
},
{
"name" : "electronique",
"lvl" : "0"
},
{
"name" : "badge",
"lvl" : 1
}
],
"roles" : [
ObjectId("5b4dbf3541e5ae6de394cc97")
]
}
and here an update query :
db['02'].update(
{_id: ObjectId("5b4dbf3541e5ae6de394cc99")}, // <= here you filter your documents
{ $set: { "badges.$[elem].lvl": "10" } }, // <= use of positionnal operator with identifier, needed for arrayFilters. elem is arbitraty
{ arrayFilters: [ { "elem.name": "laser" } ], // <= update only elements in array that match arrayFilters.
upsert: true }
)

Related

MongoDB update :- update multiple objects nested inside array (in multiple documents)

My collection is as follows :
{
"_id" : "",
"team" : "avenger",
"persons" : [
{
"name" : "ABC",
"class" : "10",
"is_new" : true
},
{
"name" : "JHK",
"class" : "12",
"is_new" : true
},
{
"name" : "BNH",
"class" : "10",
"is_new" : true
}
]
},
{
"_id" : "",
"team" : "adrenaline",
"persons" : [
{
"name" : "HTU",
"class" : "11",
"is_new" : true
},
{
"name" : "NVG",
"class" : "10",
"is_new" : true
},
{
"name" : "SED",
"class" : "8",
"is_new" : true
}
]
}
My Goal is to find for all such documents which contain such nested objects in "persons" where "class" is "10" and update "is_new" field to "false"
I am using mongoose update with "multi" set to true
Query :
{
persons: { $elemMatch: { class: "10" } }
}
Options:
{
multi : true
}
Update :
First one I have tried is :
{
$set : {
"persons.$.is_new" : false
}
}
Second one is :
{
$set : {
"persons.$[].is_new" : false
}
}
The issue is : using $ in update operation updates only first match in the "persons" array for all matched documents.
If I use $[], it updates all the objects of "persons" array in the matched documents.
Workaround can be to use a loop for update operation but i believe that should not be the ideal case.
I see nothing in the docs, though have found people reporting this update operation issue.
Is this can't be done using a single query ?? Is looping through documents my only option ?
You need to set the filtered positional operator $[<identifier>] identifier
Model.update(
{ "persons": { "$elemMatch": { "class": "10" }}},
{ "$set" : { "persons.$[e].is_new" : false }},
{ "arrayFilters": [{ "e.class": "10" }], "multi" : true }
)

Mongodb difference in time is returning the current time

{
"_id" : ObjectId("57693a852956d5301b348a99"),
"First_Name" : "Sri Ram",
"Last_Name" : "Bandi",
"Email" : "chinni001sriram#gmail.com",
"Sessions" : [
{
"Class" : "facebook",
"ID" : "1778142655749042",
"Login_Time" : ISODate("2016-06-21T13:00:53.867Z"),
"Logout_Time" : ISODate("2016-06-21T13:01:04.640Z"),
"Duration" : null
}
],
"Count" : 1
}
This is my mongo data. and I want to set the duration as the difference of login and logout time. So, I executed the following query:
db.sessionData.update(
{ "Sessions.ID": "1778142655749042"},
{ $set: {
"Sessions.$.Duration": ISODate("Sessions.$.Logout_Time" - "Sessions.$.Login_Time")
}
}
)
But the result I'm getting is:
{
"_id" : ObjectId("57693a852956d5301b348a99"),
"First_Name" : "Sri Ram",
"Last_Name" : "Bandi",
"Email" : "chinni001sriram#gmail.com",
"Sessions" : [
{
"Class" : "facebook",
"ID" : "1778142655749042",
"Login_Time" : ISODate("2016-06-21T13:00:53.867Z"),
"Logout_Time" : ISODate("2016-06-21T13:01:04.640Z"),
"Duration" : ISODate("2016-06-21T13:02:58.010Z")
}
],
"Count" : 1
}
and duration wast set to current time/date instead of the difference.
You could use the aggregation framework to do the arithmetic operation using the $divide and $subtract operators to give you the difference as duration in seconds. The formula is given by
Duration (sec) = (Logout_Time - Login_Time)/1000
The aggregation pipeline should give you a new field that has this computed value and then you can use the forEach() cursor method on the aggregate() result to iterate the documents in the result and update the collection.
The following example shows this:
db.sessionData.aggregate([
{ "$match": { "Sessions.ID" : "1778142655749042" } },
{ "$unwind": "$Sessions" },
{ "$match": { "Sessions.ID" : "1778142655749042" } },
{
"$project": {
"Duration": {
"$divide": [
{ "$subtract": [ "$Sessions.Logout_Time", "$Sessions.Login_Time" ] },
1000
]
}
}
}
]).forEach(function (doc) {
db.sessionData.update(
{ "Sessions.ID": "1778142655749042", "_id": doc._id },
{
"$set": { "Sessions.$.Duration": doc.Duration }
}
);
});
Query results
{
"_id" : ObjectId("57693a852956d5301b348a99"),
"First_Name" : "Sri Ram",
"Last_Name" : "Bandi",
"Email" : "chinni001sriram#gmail.com",
"Sessions" : [
{
"Class" : "facebook",
"ID" : "1778142655749042",
"Login_Time" : ISODate("2016-06-21T13:00:53.867Z"),
"Logout_Time" : ISODate("2016-06-21T13:01:04.640Z"),
"Duration" : 10.773
}
],
"Count" : 1
}

MongoDB find documents if a property array doesn't contain an object

I have a list of documents like this.
[
{
"name" : "test",
"data" : [
{ "code" : "name", "value" : "Diego" },
{ "code" : "nick", "value" : "Darko" },
{ "code" : "special", "value" : true }
]
},
{
"name" : "another",
"data" : [
{ "code" : "name", "value" : "Antonio" },
{ "code" : "nick", "value" : "Tony" }
]
}
]
now I need to find all the documents that:
a) don't contain a "data" item with code "special"
OR
b) contains a "data" item with code "special" and value false
It's like I needed the opposite of $elemMatch or I'm missing something?
I'm assuming that you're inserting each document in your list of documents as a separate member of a collection test.
For a,
db.test.find({ "data.code" : { "$ne" : "special" } })
For b.,
db.test.find({ "data" : { "$elemMatch" : { "code" : "special", "value" : false } } })
Combining the two with $or,
db.test.find({ "$or" : [
{ "data.code" : { "$ne" : "special" } },
{ "data" : { "$elemMatch" : { "code" : "special", "value" : false } } }
] })
Hope this $nin will solve your issues.
I insertd your docs into "so" collection
db.so.find({}).pretty();
{
"_id" : ObjectId("5489cd4f4cb16307b808d4b2"),
"name" : "test",
"data" : [
{ "code" : "name",
"value" : "Diego"
},
{ "code" : "nick",
"value" : "Darko"
},
{ "code" : "special",
"value" : true
}
]
}
{
"_id" : ObjectId("5489cd674cb16307b808d4b3"),
"name" : "another",
"data" : [
{"code" : "name",
"value" : "Antonio"
},
{ "code" : "nick",
"value" : "Tony"
}
]
}
don't contain a "data" item with code "special"
> db.so.find({"data.code":{$nin:["special"]}}).pretty();
{
"_id" : ObjectId("5489cd674cb16307b808d4b3"),
"name" : "another",
"data" : [
{ "code" : "name",
"value" : "Antonio"
},
{ "code" : "nick",
"value" : "Tony"
}
]
}
contains a "data" item with code "special" and value false
db.so.find({$and:[{"data.code":"special"},{"data.value":false}]}).pretty();

MongodDB retrieve a subdocument by the property name

My document looks like:
{ "entity_id" : 2,
"features" :
[
{ "10" : "name" },
{ "20" : "description" },
... ,
{ "90" : "availability" }
]
}
I would like to, knowing 2 things: the value of "entity_id" (2), and the value of the property in one of the elements of the "features" array, to retrieve only the subdocument
{ "20" : "description" }
in one query. Many thanks!
If you want to get only a part of the whole document, use so called Projection operators
See examples below:
> db.collection.find().pretty()
{
"_id" : ObjectId("52e861617acb7ce761e64a93"),
"entity_id" : 2,
"features" : [
{
"10" : "name"
},
{
"20" : "description"
},
{
"90" : "availability"
}
]
}
Projection operators are specified in find() like here:
> db.collection.find({},{ features : { $elemMatch : { 20 : { $exists: true } }}}).pretty()
{
"_id" : ObjectId("52e861617acb7ce761e64a93"),
"features" : [
{
"20" : "description"
}
]
}
> db.collection.find({},{ entity_id : 1, features : { $elemMatch : { 20 : { $exists: true } }}}).pretty()
{
"_id" : ObjectId("52e861617acb7ce761e64a93"),
"entity_id" : 2,
"features" : [
{
"20" : "description"
}
]
}
> db.collection.find({},{ _id : 0, entity_id : 1, features : { $elemMatch : { 20 : { $exists: true } }}}).pretty()
{ "entity_id" : 2, "features" : [ { "20" : "description" } ] }
$elemMatch for projection is available in MongoDB since version 2.2.
Hope it solves your problem.

Aggregate of different subtypes in document of a collection

abstract document in collection md given:
{
vals : [{
uid : string,
val : string|array
}]
}
the following, partially correct aggregation is given:
db.md.aggregate(
{ $unwind : "$vals" },
{ $match : { "vals.uid" : { $in : ["x", "y"] } } },
{
$group : {
_id : { uid : "$vals.uid" },
vals : { $addToSet : "$vals.val" }
}
}
);
that may lead to the following result:
"result" : [
{
"_id" : {
"uid" : "x"
},
"vals" : [
[
"24ad52bc-c414-4349-8f3a-24fd5520428e",
"e29dec2f-57d2-43dc-818a-1a6a9ec1cc64"
],
[
"5879b7a4-b564-433e-9a3e-49998dd60b67",
"24ad52bc-c414-4349-8f3a-24fd5520428e"
]
]
},
{
"_id" : {
"uid" : "y"
},
"vals" : [
"0da5fcaa-8d7e-428b-8a84-77c375acea2b",
"1721cc92-c4ee-4a19-9b2f-8247aa53cfe1",
"5ac71a9e-70bd-49d7-a596-d317b17e4491"
]
}
]
as x is the result aggregated on documents containing an array rather than a string, the vals in the result is an array of arrays. what i look for in this case is to have a flattened array (like the result for y).
for me it seems like that what i want to achieve by one aggegration call only, is currently not supported by any given operation as e.g. a type conversion cannot be done or unwind expectes in every case an array as input type.
is map reduce the only option i have? if not ... any hints?
thanks!
You can use the aggregation to do the computation you want without changing your schema (though you might consider changing your schema simply to make queries and aggregations of this field easier to write).
I broke up the pipeline into multiple steps for readability. I also simplified your document slightly, again for readability.
Sample input:
> db.md.find().pretty()
{
"_id" : ObjectId("512f65c6a31a92aae2a214a3"),
"uid" : "x",
"val" : "string"
}
{
"_id" : ObjectId("512f65c6a31a92aae2a214a4"),
"uid" : "x",
"val" : "string"
}
{
"_id" : ObjectId("512f65c6a31a92aae2a214a5"),
"uid" : "y",
"val" : "string2"
}
{
"_id" : ObjectId("512f65e8a31a92aae2a214a6"),
"uid" : "y",
"val" : [
"string3",
"string4"
]
}
{
"_id" : ObjectId("512f65e8a31a92aae2a214a7"),
"uid" : "z",
"val" : [
"string"
]
}
{
"_id" : ObjectId("512f65e8a31a92aae2a214a8"),
"uid" : "y",
"val" : [
"string1",
"string2"
]
}
Pipeline stages:
> project1 = {
"$project" : {
"uid" : 1,
"val" : 1,
"isArray" : {
"$cond" : [
{
"$eq" : [
"$val.0",
[ ]
]
},
true,
false
]
}
}
}
> project2 = {
"$project" : {
"uid" : 1,
"valA" : {
"$cond" : [
"$isArray",
"$val",
[
null
]
]
},
"valS" : {
"$cond" : [
"$isArray",
null,
"$val"
]
},
"isArray" : 1
}
}
> unwind = { "$unwind" : "$valA" }
> project3 = {
"$project" : {
"_id" : 0,
"uid" : 1,
"val" : {
"$cond" : [
"$isArray",
"$valA",
"$valS"
]
}
}
}
Final aggregation:
> db.md.aggregate(project1, project2, unwind, project3, group)
{
"result" : [
{
"_id" : "z",
"vals" : [
"string"
]
},
{
"_id" : "y",
"vals" : [
"string1",
"string4",
"string3",
"string2"
]
},
{
"_id" : "x",
"vals" : [
"string"
]
}
],
"ok" : 1
}
If you modify your schema using always "vals.val" field as an array field (even when the record contains only one element) you can do it easily as follows:
db.test_col.insert({
vals : [
{
uid : "uuid1",
val : ["value1"]
},
{
uid : "uuid2",
val : ["value2", "value3"]
}]
});
db.test_col.insert(
{
vals : [{
uid : "uuid2",
val : ["value4", "value5"]
}]
});
Using this approach you only need to use two $unwind operations: one unwinds the "parent" array and the second unwinds every "vals.val" value. So, querying like
db.test_col.aggregate(
{ $unwind : "$vals" },
{ $unwind : "$vals.val" },
{
$group : {
_id : { uid : "$vals.uid" },
vals : { $addToSet : "$vals.val" }
}
}
);
You can obtain your expected value:
{
"result" : [
{
"_id" : {
"uid" : "uuid2"
},
"vals" : [
"value5",
"value4",
"value3",
"value2"
]
},
{
"_id" : {
"uid" : "uuid1"
},
"vals" : [
"value1"
]
}
],
"ok" : 1
}
And no, you can't execute this query using your current schema, since $unwind fails when the field isn't an array field.