MongoDB insert object into array if not exists - mongodb

I am working on an app that manages the voting on polls by users. I use MongoDB as DB. (I am a newbie)
My DB is structured as follows:
[
{
"question_id": "0001",
"text": "What's your favourite color ? ",
"answers": [
{
"_id": "872ffaskdfba23jdafs",
"text": "Blue",
"user": {
"user_id": "u0001",
"nickname": "Paul"
}
},
{
"_id": "7832ab28b879293fabb2",
"text": "Red",
"user": {
"user_id": "u0002",
"nickname": "Eric"
}
},
{
"_id": "664oahh229s0f829323av",
"text": "Red",
"user": {
"user_id": "u0003",
"nickname": "Johhny"
}
}
]
}
]
Users can leave a maximum of answer for each question; so, for example, users Paul,Eric and Johhny can't vote anymore since their Id's are already present inside 'answers' array.
How could achieve this kind of control with MongoDB ?

You can use $reduce to achieve the behaviour.
create a union of the original array and the element you want to add
iterate the created union through $reduce, append the current element if the user_id is not already exists in our accumulator result; otherwise keep the accumulator result
db.collection.update({}, [
{
"$addFields": {
"answers": {
"$reduce": {
"input": {
"$concatArrays": [
"$answers",
[
<answer object that you want to insert>
]
]
},
"initialValue": [],
"in": {
"$cond": {
"if": {
"$not": {
"$in": [
"$$this.user.user_id",
"$$value.user.user_id"
]
}
},
"then": {
"$concatArrays": [
"$$value",
[
"$$this"
]
]
},
"else": "$$value"
}
}
}
}
}
}
])
Here is the Mongo playground for your reference

Related

Lookup in second table but exclude users from first

The bounty expires in 7 days. Answers to this question are eligible for a +150 reputation bounty.
bflemi3 wants to draw more attention to this question.
I have a collection called permissions that I'm joining with another collection called groups.
permissions
[
{
"_id": 1,
"resource": "resource:docs/61",
"permissions": [
{
"permission": "role:documentOwner",
"users": [
"user:def",
"group:abc",
]
},
{
"permission": "document.read",
"users": [
"user:abc",
"user:xxx",
"group:abc"
]
},
{
"permission": "document.update",
"users": [
"user:xxx"
]
}
]
},
{
"_id": 2,
"resource": "resource:docs/38",
"permissions": [
{
"permission": "role:documentOwner",
"users": [
"user:abc",
"user:def",
"group:abc",
"group:bff"
]
},
{
"permission": "document.read",
"users": [
"user:xxx"
]
},
{
"permission": "document.update",
"users": [
"user:xxx"
]
}
]
}
]
groups
[
{
"_id": 1,
"id": "abc",
"name": "Test Group",
"users": [
"abc",
"cpo",
"yyy",
"xxx"
]
},
{
"_id": 2,
"id": "bff",
"name": "Something",
"users": [
"xxx"
]
}
]
I'm trying to do two things:
Get all permissions and have any entries in the users array that are prefixed with group: resolved so that the respective group's users are included in the users array.
If a permission document has a user that is listed specifically and also contained in a listed group, then that user is not included from the group. In other words, permissions that are granted to a group's users, can be overridden for a member of the group if they are specifically granted permissions. For instance, I grant group:abc with document.read permission on a resource, but I want user:abc (which is a part of the group) to have document.read and document.update for that resource, so I specifically grant user:abc with those permissions.
For example, here's what permissions._id = 1 would look like...
[
{
"_id": 1,
"resource": "resource:docs/61",
"permissions": [
{
"permission": "role:documentOwner",
"users": [
"user:def",
"user:cpo", // inherited from group:abc
"user:yyy", // inherited from group:abc
]
},
{
"permission": "document.read",
"users": [
"user:abc", // not inherited even though part of group:abc because they're specifically listed in the original document
"user:xxx", // not inherited even though part of group:abc because they're specifically listed in the original document
"user:cpo", // inherited from group:abc
"user:yyy", // inherited from group:abc
]
},
{
"permission": "document.update",
"users": [
"user:xxx" // not inherited even though part of group:abc because they're specifically listed in the original document
]
}
]
},
...
]
I created a Mongo Playground to use for testing. I'm failing miserably though 😞
I appreciate the help!
You can do the followings in an aggregation pipeline:
$unwind to process at permission-resource level
"split" the users array into "user" and "groups" respectively. Locate the array entries using $indexOfCP equal to 0 to check for the prefix.
$lookup to the groups collection
perform $setUnion to union the permissions for individual users and permissions granted from groups
$group again to get back original form
db.permissions.aggregate([
{
$match: {
"_id": 1
}
},
{
"$unwind": "$permissions"
},
{
$set: {
// process users entries
u: {
"$reduce": {
"input": "$permissions.users",
"initialValue": [],
"in": {
"$cond": {
"if": {
$eq: [
0,
{
"$indexOfCP": [
"$$this",
"user:"
]
}
]
},
"then": {
"$concatArrays": [
"$$value",
[
{
"$replaceAll": {
"input": "$$this",
"find": "user:",
"replacement": ""
}
}
]
]
},
"else": "$$value"
}
}
}
},
// process groups entries
g: {
"$reduce": {
"input": "$permissions.users",
"initialValue": [],
"in": {
"$cond": {
"if": {
$eq: [
0,
{
"$indexOfCP": [
"$$this",
"group:"
]
}
]
},
"then": {
"$concatArrays": [
"$$value",
[
{
"$replaceAll": {
"input": "$$this",
"find": "group:",
"replacement": ""
}
}
]
]
},
"else": "$$value"
}
}
}
}
}
},
{
"$lookup": {
"from": "groups",
"localField": "g",
"foreignField": "id",
"as": "g"
}
},
{
$project: {
resource: 1,
permissions: {
permission: 1,
// flatten and union users and looked up groups
users: {
"$setUnion": [
"$u",
{
"$reduce": {
"input": "$g",
"initialValue": [],
"in": {
"$setUnion": [
"$$value",
"$$this.users"
]
}
}
}
]
}
}
}
},
{
// cosmetics
$set: {
"permissions.users": {
"$map": {
"input": "$permissions.users",
"as": "u",
"in": {
"$concat": [
"user:",
"$$u"
]
}
}
}
}
},
{
$group: {
_id: "$_id",
resource: {
$first: "$resource"
},
permissions: {
$push: "$permissions"
}
}
}
])
Mongo Playground

Find specific field in MongoDB document based on condition

I have the following MongoDB documents like this one:
{
"_id": "ABC",
"properties":
[
{
"_id": "123",
"weight":
{
"$numberInt": "0"
},
"name": "Alice"
},
{
"_id": "456",
"weight":
{
"$numberInt": "1"
},
"name": "Bob"
},
{
"_id": "789",
"weight":
{
"$numberInt": "1"
},
"name": "Charlie"
}
]
}
And I would like to find the _id of the property with name "Alice", or the _id of the property with "$numberInt": "0".
I'm using pymongo.
The following approach:
from pymongo import MongoClient
mongo_client = MongoClient("mymongourl")
mongo_collection = mongo_client.mongo_database.mongo_collection
mongo_collection.find({'properties.name': 'Alice'}, {'properties': 1})[0]['_id']
Gives the very first _id ("123")
But since I filtered for the document, if Alice was in the second element of the properties array (_id: "456") I would have missed her.
Which is the best method to find for the specific _id associated with the element with the specified name?
You can simply use $reduce to iterate through the properties array. Conditionally store the _id field if it matches your conditions.
db.collection.aggregate([
{
"$addFields": {
"answer": {
"$reduce": {
"input": "$properties",
"initialValue": null,
"in": {
"$cond": {
"if": {
$or: [
{
$eq: [
"$$this.name",
"Alice"
]
},
{
$eq: [
"$$this.weight",
0
]
}
]
},
"then": "$$this._id",
"else": "$$value"
}
}
}
}
}
}
])
Mongo Playground

MongoDB pull first matching nested array item

I have the following documents...
{ "_id": 2, "name": "Jane Doe", "phones": [ { "type": "Mobile", "digits": [ { "val": 1 }, { "val": 2 } ] }, { "type": "Mobile", "digits": [ { "val": 3 }, { "val": 4 } ] }, { "type": "Land", "digits": [ { "val": 5 }, { "val": 6 } ] } ] }
{ "_id": 1, "name": "John Doe", "phones": [ { "type": "Land", "digits": [ { "val": 1 }, { "val": 2 } ] }, { "type": "Mobile", "digits": [ { "val": 0 }, { "val": 3 }, { "val": 4 } ] }, { "type": "Mobile", "digits": [ { "val": 3 }, { "val": 4 }, { "val": 9 } ] } ] }
...and the following MongoDB query...
db.getCollection("persons").updateOne({"name": "John Doe"},
{
"$pull":
{
"phones.$[condition1].digits":
{
"val: { $in: [ 3, 4 ] }
}
}
},
{
arrayFilters:
[
{ "condition1.type": "Mobile" }
]
})
My problem is that the query removes the last two elements of the array: "phones" of the second document (John Doe) and I want to remove only the first one (and not the last one that have a "9" among the digits). How I can delete only the first matching nested array item?
Query
pipeline update
reduce on phones, starting with {"phones": [], "found": false}
if [3,4] subset of digits.val and not found => ignore it
else keep it (concat arrays to add the member)
$getField to get the phones from the reduced {"phones" : [...]}
*$pull removes all elements that satisfy the condition, maybe there is a way with update operators and not pipeline update, but this works if you dont find more compact way
*alternative to reduce, could be 2 filters, one to keep the values that dont contain the [3,4] and one to keep those that contain, from those that contained, and then concat those arrays removing only one of those that contain the [3,4]
Playmongo
update(
{"name": {"$eq": "John Doe"}},
[{"$set":
{"phones":
{"$getField":
{"field": "phones",
"input":
{"$reduce":
{"input": "$phones",
"initialValue": {"phones": [], "found": false},
"in":
{"$cond":
[{"$and":
[{"$not": ["$$value.found"]},
{"$setIsSubset": [[3, 4], "$$this.digits.val"]}]},
{"phones": "$$value.phones", "found": true},
{"phones": {"$concatArrays": ["$$value.phones", ["$$this"]]},
"found": "$$value.found"}]}}}}}}}])
I have no real sense of motivation for this update, so I am unsure about the details of the logic. I think I have taken the OP's words and partial demonstration literally and I've implemented an update pipeline to fix the stated problem. Given the number of possibilities, this may not be what you are looking for. My pipeline is very similar to the #Takis answer, but the logic is slightly different and therefore the output is different. I look forward to the OP's comments/questions to identify/clarify any discrepancies and/or ambiguities.
db.collection.update({
"name": "John Doe"
},
[
{
"$set": {
"phones": {
"$getField": {
"field": "phones",
"input": {
"$reduce": {
"input": "$phones",
"initialValue": { "phones": [], "pullDone": false },
"in": {
"$cond": [
{
"$and": [
{ "$eq": [ "$$this.type", "Mobile" ] },
{ "$not": "$$value.pullDone" }
]
},
{
"pullDone": true,
"phones": {
"$concatArrays": [
"$$value.phones",
[
{
"$mergeObjects": [
"$$this",
{
"digits": {
"$filter": {
"input": "$$this.digits",
"as": "digit",
"cond": {
"$not": [ { "$in": [ "$$digit.val", [ 3, 4 ] ] } ]
}
}
}
}
]
}
]
]
}
},
{
"pullDone": "$$value.pullDone",
"phones": {
"$concatArrays": [ "$$value.phones", [ "$$this" ] ]
}
}
]
}
}
}
}
}
}
}
])
Try it on mongoplayground.net.

Can I get the count of subdocuments that match a filter?

I have the following document
[
{
"_id": "624713340a3d2901f2f5a9c0",
"username": "fotis",
"exercises": [
{
"_id": "624713530a3d2901f2f5a9c3",
"description": "Sitting",
"duration": 60,
"date": "2022-03-24T00:00:00.000Z"
},
{
"_id": "6247136a0a3d2901f2f5a9c6",
"description": "Coding",
"duration": 999,
"date": "2022-03-31T00:00:00.000Z"
},
{
"_id": "624713a00a3d2901f2f5a9ca",
"description": "Sitting",
"duration": 999,
"date": "2022-03-30T00:00:00.000Z"
}
],
"__v": 3
}
]
And I am trying to get the count of exercises returned with the following aggregation (I know it is way easier to do it in my code, but I am trying to understand how to use mongodb queries)
db.collection.aggregate([
{
"$match": {
"_id": "624713340a3d2901f2f5a9c0"
}
},
{
"$project": {
"username": 1,
"exercises": {
"$slice": [
{
"$filter": {
"input": "$exercises",
"as": "exercise",
"cond": {
"$eq": [
"$$exercise.description",
"Sitting"
]
}
}
},
1
]
},
"count": {
"$size": "exercises"
}
}
}
])
When I try to access the exercises field using "$size": "exercises", I get an error query failed: (Location17124) Failed to optimize pipeline :: caused by :: The argument to $size must be an array, but was of type: string.
But when I access the subdocument exercises using "$size": "$exercises" I get the count of all the subdocuments contained in the document.
Note: I know that in this example I use $slice and I set the limit to 1, but in my code it is a variable.
You are actually on the right track. You don't really need the $slice. You can just use $reduce to perform the filtering. The reason that your count is not working is that the filtering and the $size are in the same stage. In such case, it will take the pre-filtered array to do the count. You can resolve this by adding a $addFields stage.
db.collection.aggregate([
{
"$match": {
"_id": "624713340a3d2901f2f5a9c0"
}
},
{
"$project": {
"username": 1,
"exercises": {
"$filter": {
"input": "$exercises",
"as": "exercise",
"cond": {
"$eq": [
"$$exercise.description",
"Sitting"
]
}
}
}
}
},
{
"$addFields": {
"count": {
$size: "$exercises"
}
}
}
])
Here is the Mongo playground for your reference.

MongoDB nested query using aggregate function

I have a collection "superpack", which has the nested objects. The sample document looks like below.
{
"_id" : ObjectId("56038c8cca689261baca93eb"),
"name": "Test sub",
"packs": [
{
"id": "55fbc7f6b0ce97a309b3cead",
"name": "Classic",
"packDispVal": "PACK",
"billingPts": [
{
"id": "55fbc7f6b0ce97a309b3ceab",
"name": "Classic 1 month",
"expiryVal": 1,
"amount": 20,
"topUps": [
{
"id": "55fbc7f6b0ce97a309b3cea9",
"name": "1 extra",
"amount": 8
},
{
"id": "55fbc7f6b0ce97a309b3ceaa",
"name": "2 extra",
"amount": 12
}
]
},
{
"id": "55fbc7f6b0ce97a309b3ceac",
"name": "Classic 2 month",
"expiryVal": 1,
"amount": 30,
"topUps": [
{
"id": "55fbc7f6b0ce97a309b3cea8",
"name": "3 extra",
"amount": 16
}
]
}
]
}
]
}
I need to query for the nested object topups with the id field and result should have only the selected topup object and its associated parent. I am expecting the output to like below, when i query it on topup id 55fbc7f6b0ce97a309b3cea9.
{
"_id" : ObjectId("56038c8cca689261baca93eb"),
"name": "Test sub",
"packs": [
{
"id": "55fbc7f6b0ce97a309b3cead",
"name": "Classic",
"packDispVal": "PACK",
"billingPts": [
{
"id": "55fbc7f6b0ce97a309b3ceab",
"name": "Classic 1 month",
"expiryVal": 1,
"amount": 20,
"topUps": [
{
"id": "55fbc7f6b0ce97a309b3cea9",
"name": "1 extra",
"amount": 8
}
]
}
]
}
]
}
I tried with the below aggregate query for the same. However its not returning any result. Can you please help me, what is wrong in the query?
db.superpack.aggregate( [{ $match: { "id": "55fbc7f6b0ce97a309b3cea9" } }, { $redact: {$cond: { if: { $eq: [ "$id", "55fbc7f6b0ce97a309b3cea9" ] }, "then": "$$KEEP", else: "$$PRUNE" }}} ])
Unfortunately $redact is not a viable option here based on the fact that with the recursive $$DESCEND it is basically looking for a field called "id" at all levels of the document. You cannot possibly ask to do this only at a specific level of embedding as it's all or nothing.
This means you need alternate methods of filtering the content rather than $redact. All "id" values are unique so their is no problem filtering via "set" operations.
So the most efficient way to do this is via the following:
db.docs.aggregate([
{ "$match": {
"packs.billingPts.topUps.id": "55fbc7f6b0ce97a309b3cea9"
}},
{ "$project": {
"packs": {
"$setDifference": [
{ "$map": {
"input": "$packs",
"as": "pack",
"in": {
"$let": {
"vars": {
"billingPts": {
"$setDifference": [
{ "$map": {
"input": "$$pack.billingPts",
"as": "billing",
"in": {
"$let": {
"vars": {
"topUps": {
"$setDifference": [
{ "$map": {
"input": "$$billing.topUps",
"as": "topUp",
"in": {
"$cond": [
{ "$eq": [ "$$topUp.id", "55fbc7f6b0ce97a309b3cea9" ] },
"$$topUp",
false
]
}
}},
[false]
]
}
},
"in": {
"$cond": [
{ "$ne": [{ "$size": "$$topUps"}, 0] },
{
"id": "$$billing.id",
"name": "$$billing.name",
"expiryVal": "$$billing.expiryVal",
"amount": "$$billing.amount",
"topUps": "$$topUps"
},
false
]
}
}
}
}},
[false]
]
}
},
"in": {
"$cond": [
{ "$ne": [{ "$size": "$$billingPts"}, 0 ] },
{
"id": "$$pack.id",
"name": "$$pack.name",
"packDispVal": "$$pack.packDispVal",
"billingPts": "$$billingPts"
},
false
]
}
}
}
}},
[false]
]
}
}}
])
Where after digging down to the innermost array that is being filtered, that then the size of each resulting array going outwards is tested to see if it is zero, and omitted from results where it is.
It's a long listing but it is the most efficient way since each array is filtered down first and within each document.
A not so efficient way is to pull apart with $unwind and the $group back the results:
db.docs.aggregate([
{ "$match": {
"packs.billingPts.topUps.id": "55fbc7f6b0ce97a309b3cea9"
}},
{ "$unwind": "$packs" },
{ "$unwind": "$packs.billingPts" },
{ "$unwind": "$packs.billingPts.topUps"},
{ "$match": {
"packs.billingPts.topUps.id": "55fbc7f6b0ce97a309b3cea9"
}},
{ "$group": {
"_id": {
"_id": "$_id",
"packs": {
"id": "$packs.id",
"name": "$packs.name",
"packDispVal": "$packs.packDispVal",
"billingPts": {
"id": "$packs.billingPts.id",
"name": "$packs.billingPts.name",
"expiryVal": "$packs.billingPts.expiryVal",
"amount": "$packs.billingPts.amount"
}
}
},
"topUps": { "$push": "$packs.billingPts.topUps" }
}},
{ "$group": {
"_id": {
"_id": "$_id._id",
"packs": {
"id": "$_id.packs.id",
"name": "$_id.packs.name",
"packDispVal": "$_id.packs.packDispVal"
}
},
"billingPts": {
"$push": {
"id": "$_id.packs.billingPts.id",
"name": "$_id.packs.billingPts.name",
"expiryVal": "$_id.packs.billingPts.expiryVal",
"amount": "$_id.packs.billingPts.amount",
"topUps": "$topUps"
}
}
}},
{ "$group": {
"_id": "$_id._id",
"packs": {
"$push": {
"id": "$_id.packs.id",
"name": "$_id.packs.name",
"packDispVal": "$_id.packs.packDispVal",
"billingPts": "$billingPts"
}
}
}}
])
The listing looks a lot more simple but of course there is a lot of overhead introduced by $unwind here. The process of grouping back is basically keeping a copy of everything outside of the current array level being reconstructed, and then push that content back into the array in the next stage, until you get back to the root _id.
Please note that unless you intend such a search to match more than one document or if you are going to have significant gains from reduced network traffic by effectively reducing down the response size from a very large document, then it would be advised to do neither of these but follow much of the same design as the first pipeline example but in client code.
Whilst the first example would be still okay performance wise, it's still a mouthful to send to the server and as a general listing, that is typically written with the same operations in a cleaner way in client code to process and filter the resulting structure.
{
"_id" : ObjectId("56038c8cca689261baca93eb"),
"packs" : [
{
"id" : "55fbc7f6b0ce97a309b3cead",
"name" : "Classic",
"packDispVal" : "PACK",
"billingPts" : [
{
"id" : "55fbc7f6b0ce97a309b3ceab",
"name" : "Classic 1 month",
"expiryVal" : 1,
"amount" : 20,
"topUps" : [
{
"id" : "55fbc7f6b0ce97a309b3cea9",
"name" : "1 extra",
"amount" : 8
}
]
}
]
}
]
}