Self joining query in MongoDB - mongodb

We are exploring the possibility of migrating from relational database to MongoDB and having difficulty dealing with a query, the data schema is like this:
data_store:
id,
userId,
study,
form,
formData: {
element1: value1,
element2: value2,
.....
}
formData is a json array of dynamic element:value pairs, different form has a different list of pre-defined elements.
The requirement is by given 2 forms in a study, we need to display all their elements' data in one row for the same user, i.e. we need to join by userId and display the formData in a flat structure.
Also one user may have multiple data entry for the same form, so if a user has 3 entries for form A and 4 entries for form B, we expect that there are 12 rows for them in the result.
Sample data:
id: "id1",
user: "user1",
study: "study",
form: "f1",
formData: [
{ "e11": "value1" },
{ "e12": "value2" },
{ "e13": "value3" }
]
,
id: "id2",
user: "user1",
study: "study",
form: "f1",
formData: [
{ "e11": "value4" },
{ "e12": "value5" },
{ "e13": "value6" }
]
,
id: "id3",
user: "user1",
study: "study",
form: "f2",
formData: [
{ "e21": "value7" },
{ "e22": "value8" }
]
,
id: "id4",
user: "user1",
study: "study1",
form: "f2",
formData: [
{ "e21": "value9" },
{ "e22": "value10" }
]
,
id: "id2",
user: "user2",
study: "study1",
form: "f2",
formData: [
{ "e21": "value11" },
{ "e22": "value12" }
The expected result for the sample data above is:
Row#
study
user
f1.e11
f1.e12
f1.e13
f2.e21
f2.e22
1
study
user1
value1
value2
value3
value7
value8
2
study
user1
value1
value2
value3
value9
value10
3
study
user1
value4
value5
value6
value7
value8
4
study
user1
value4
value5
value6
value9
value10
5
study
user2
value11
value12
A similar query in relational database may be like this:
select t1.*, t2.*
from data_store t1, data_store t2
where t1.form = 'f1'
and t2.form = 'f2'
and t1.userId = t2.userId;
I am having difficulty of converting it into MongoDB query, any one who can shed some light will be greatly appreciated.

You can use the below Aggregation Query to get the desired result.
db.collection.aggregate([
{
$group: {
_id: {
"user": "$user",
"form": "$form",
},
"formData": {
"$push": {
"$concatArrays": [
"$formData",
[
{
"study": "$study"
}
]
]
},
},
}
},
{
$group: {
_id: {
"user": "$_id.user",
},
formData: {
"$push": "$formData"
}
}
},
{
$project: {
"formData": {
"$map": {
"input": {
"$arrayElemAt": [
"$formData",
0
]
},
"as": "f1",
"in": {
"$cond": {
"if": {
"$gt": [
{
"$size": "$formData"
},
1
]
},
"then": {
"$map": {
"input": {
"$arrayElemAt": [
"$formData",
1
]
},
"as": "f2",
"in": {
"$concatArrays": [
"$$f1",
"$$f2"
],
},
},
},
"else": [
"$$f1"
]
},
},
},
},
}
},
{
$unwind: {
path: "$formData",
}
},
{
$unwind: {
path: "$formData",
}
},
{
$replaceRoot: {
newRoot: {
"$mergeObjects": [
{
"user": "$_id.user"
},
{
"$reduce": {
"input": "$formData",
"initialValue": {},
"in": {
"$mergeObjects": [
"$$this",
"$$value"
],
}
},
},
],
}
}
}
])
Mongo Playground Working Sample
Let me know if you want an explanation of each stage.
Limitations:
This will work only if there are two forms of studies f1 and f2.
I am working on making this dynamic and will update this answer once done.
EDIT:
Make use of this updated query which will work dynamically.
db.collection.aggregate([
{
"$match": {
"form": {
"$in": [
"f1",
"f2"
]
},
},
},
{
"$group": {
"_id": "$user",
"formData": {
"$push": {
"$mergeObjects": [
{
"$reduce": {
"input": "$formData",
"initialValue": {},
"in": {
"$mergeObjects": [
"$$value",
{
"$arrayToObject": {
"$map": {
"input": {
"$objectToArray": "$$this"
},
"as": "formValue",
"in": {
"k": {
"$concat": [
"$form",
"-",
"$$formValue.k"
]
},
"v": "$$formValue.v"
}
},
},
},
]
},
}
},
{
"study": "$study",
"user": "$user",
"form": "$form"
},
],
},
},
},
},
{
"$project": {
"formData": {
"$cond": {
"if": {
"$gt": [
{
"$size": "$formData"
},
1
]
},
"then": {
$reduce: {
input: {
$range: [
0,
{
$size: "$formData"
}
]
},
initialValue: [],
in: {
$concatArrays: [
"$$value",
{
$let: {
vars: {
i: "$$this"
},
in: {
$map: {
input: {
$range: [
{
$add: [
1,
"$$i"
]
},
{
$size: "$formData"
}
]
},
in: [
{
"$let": {
"vars": {
"arrayElem1": {
$arrayElemAt: [
"$formData",
"$$i"
]
},
"arrayElem2": {
$arrayElemAt: [
"$formData",
"$$this"
]
},
},
"in": {
"$cond": {
"if": {
"$and": [
{
"$ne": [
"$$arrayElem1.form",
"$$arrayElem2.form"
]
},
// {
// "$eq": [
// "$$arrayElem1.user",
// "$$arrayElem2.user"
// ]
// },
],
},
"then": {
"$mergeObjects": [
"$$arrayElem2",
"$$arrayElem1",
],
},
"else": "$$REMOVE",
},
},
}
}
]
}
}
}
}
]
}
}
},
"else": [
"$formData"
]
},
},
},
},
{
"$project": {
"formData": {
"$reduce": {
"input": "$formData",
"initialValue": [],
"in": {
"$concatArrays": [
"$$value",
{
"$cond": {
"if": {
"$ne": [
"$$this",
[
null
]
]
},
"then": "$$this",
"else": []
}
}
]
}
}
}
}
},
{
"$unwind": "$formData"
},
{
"$replaceRoot": {
"newRoot": "$formData"
}
},
])
Mongo Playground Working Sample

Related

Update more than one inner document in MongoDb

In my example project, I have employees under manager. Db schema is like this;
{
"employees": [
{
"name": "Adam",
"_id": "5ea36b27d7ae560845afb88e",
"bananas": "allowed"
},
{
"name": "Smith",
"_id": "5ea36b27d7ae560845afb88f",
"bananas": "not-allowed"
},
{
"name": "John",
"_id": "5ea36b27d7ae560845afb88g",
"bananas": "not-allowed"
},
{
"name": "Patrick",
"_id": "5ea36b27d7ae560845afb88h",
"bananas": "allowed"
}
]
}
In this case Adam is allowed to eat bananas and Smith is not. If I have to give the permission of eating bananas from Adam to Smith I need to perform update operation twice like this:
db.managers.update(
{ 'employees.name': 'Adam' },
{ $set: { 'employees.$.bananas': 'not-allowed' } }
);
and
db.managers.update(
{ 'employees.name': 'Smith' },
{ $set: { 'employees.$.bananas': 'allowed' } }
);
Is it possible to handle this in a single query?
You can use $map and $cond to perform conditional update to the array entries depending on the name of the employee. A $switch is used for potential extension of cases.
db.collection.update({},
[
{
"$set": {
"employees": {
"$map": {
"input": "$employees",
"as": "e",
"in": {
"$switch": {
"branches": [
{
"case": {
$eq: [
"$$e.name",
"Adam"
]
},
"then": {
"$mergeObjects": [
"$$e",
{
"bananas": "not-allowed"
}
]
}
},
{
"case": {
$eq: [
"$$e.name",
"Smith"
]
},
"then": {
"$mergeObjects": [
"$$e",
{
"bananas": "allowed"
}
]
}
}
],
default: "$$e"
}
}
}
}
}
}
])
Mongo Playground
db.managers.update(
{
$or: [
{"employees.name": "Adam"},
{"employees.name": "Smith"}
]
},
{
$set: {
"employees.$[e].bananas": {
$cond: [{ $eq: ["$e.name", "Adam"] }, "not-allowed", "allowed"]
}
}
},
{
arrayFilters: [{ "e.name": { $in: ["Adam", "Smith"] } }]
}
)

Mongo addFields get the value from the nested array

I want to sort the collection by the fieldValue based on the given fieldName.
For example:
sort the collection by fieldName = 'Author'
My problem:
I am unable to get the value from the collection, like I want to add a field for authorValue.
{
....
author: 'John'
}, {
author: 'Hengry'}
What I have tried:
.addFields({
author: {
$filter: {
input: "$sections.fieldData",
cond: {
$eq: ["$$this.fieldName", true],
},
},
},
The structure
[
{
"sections": [
{
name: "section1",
fields: [
{
fieldName: "Author",
fieldValue: "John"
},
{
fieldName: "Movie",
fieldValue: "Avenger"
}
]
}
]
},
{
"sections": [
{
name: "section1",
fields: [
{
fieldName: "Author",
fieldValue: "Hengry"
},
{
fieldName: "Movie",
fieldValue: "Test"
}
]
}
]
}
]
You can use $reduce to iterate your array and extract out the fieldValue for comparison.
db.collection.aggregate([
{
"$addFields": {
"sortField": {
"$reduce": {
"input": "$sections",
"initialValue": null,
"in": {
"$reduce": {
"input": "$$this.fields",
"initialValue": null,
"in": {
"$cond": {
"if": {
$eq: [
"$$this.fieldName",
"Author"
]
},
"then": "$$this.fieldValue",
"else": "$$value"
}
}
}
}
}
}
}
},
{
$sort: {
sortField: 1
}
},
{
"$project": {
sortField: false
}
}
])
Here is the Mongo playground for your reference.

MongoDB Trasnform string array to string concatenated in alphabetical order

Playground
Lets say I have this collection:
[
{ "Topics": [ "a", "b" ] },
{ "Topics": [ "x", "a" ] },
{ "Topics": [ "k", "c", "z" ] }
]
I want to transform this string array to a single string with the itens of it in alphabetical order. The result would be:
[
{ Topic: "a/b"},
{ Topic: "a/x"},
{ Topic: "c/k/z"}
]
How can I project this result? Using Map? Reduce?
I have Mongo 5.0
Playground
cheers
just found the solution after some tries...
Just A Unwind, Sort, Group, Project with Reduce made the job...
Data
[
{
"Topics": [
"a",
"b"
]
},
{
"Topics": [
"x",
"a"
]
},
{
"Topics": [
"k",
"c",
"z"
]
}
]
Query
db.collection.aggregate([
{
"$unwind": "$Topics"
},
{
"$sort": {
"Topics": 1
}
},
{
"$group": {
"_id": "$_id",
Topics: {
"$push": "$Topics"
}
}
},
{
"$project": {
Topic: {
$reduce: {
input: "$Topics",
initialValue: "1T1",
in: {
$concat: [
"$$value",
"/",
"$$this"
]
}
}
}
}
}
])
Result:
[
{
"Topic": "1T1/a/x",
"_id": ObjectId("5a934e000102030405000001")
},
{
"Topic": "1T1/c/k/z",
"_id": ObjectId("5a934e000102030405000002")
},
{
"Topic": "1T1/a/b",
"_id": ObjectId("5a934e000102030405000000")
}
]
The common way to do this is
unwind
sort
group by id
reduce to 1 string
Bellow is a way to not unwind all collection but do a "local unwind".
Query
lookup with a dummy collection of 1 empty document [{}]
(this is "trick" that allows us to use stage operators like sort inside 1 document array) you need that collection in your database
unwind topics, sort them, group in 1 array, reduce them and create 1 string
we will have only 1 joined document (the transformed root document),
we replace the root with that
remove the "/" from start (it could be done on the reduce stage also)
added one extra case where topics are empty array to return ""
Test code here
db.topics.aggregate([
{
"$lookup": {
"from": "dummy",
"let": {
"topics": "$Topics"
},
"pipeline": [
{
"$set": {
"Topics": "$$topics"
}
},
{
"$unwind": {
"path": "$Topics"
}
},
{
"$sort": {
"Topics": 1
}
},
{
"$group": {
"_id": null,
"Topics": {
"$push": "$Topics"
}
}
},
{
"$project": {
"_id": 0
}
},
{
"$set": {
"Topics": {
"$reduce": {
"input": "$Topics",
"initialValue": "",
"in": {
"$let": {
"vars": {
"s": "$$value",
"t": "$$this"
},
"in": {
"$concat": [
"$$s",
"/",
"$$t"
]
}
}
}
}
}
}
}
],
"as": "joined"
}
},
{
"$replaceRoot": {
"newRoot": {
"$cond": [
{
"$eq": [
"$joined",
[]
]
},
{
"Topics": ""
},
{
"$arrayElemAt": [
"$joined",
0
]
}
]
}
}
},
{
"$set": {
"Topics": {
"$cond": [
{
"$gt": [
{
"$strLenCP": "$Topics"
},
0
]
},
{
"$substrCP": [
"$Topics",
1,
{
"$strLenCP": "$Topics"
}
]
},
""
]
}
}
}
])

Increment value in document and nested array simultaenously (with upsert)

I want to increment a value both on document root as well as inside a nested array.
A playground example here
The schema
const UserPoints = new Schema(
{
points: {
type: Number,
},
monthly: {
type: [
new Schema(
{
year: {
type: Number,
},
month: {
type: Number,
},
points: {
type: Number,
min: 0,
},
},
{
_id: false,
timestamps: false,
}
),
],
},
},
{
timestamps: false,
}
);
What I have tried
Variables used: (currentYear = 2021, currentMonth = 7, addPoints = 5)
Note: The year, month may not exist in the document yet, so I need to make it work with "upsert".
UserPoints.findOneAndUpdate(
{
_id: userId,
"monthly.year": currentYear,
"monthly.month": currentMonth,
},
{
$inc: {
points: addPoints,
"monthly.$.points": addPoints,
},
},
{
upsert: true,
new: true,
}
).exec()
This does not work. And gives out an error:
{
ok:0
code:2
codeName:"BadValue"
name:"MongoError"
}
I would appreciate if someone can point me at the right direction to increment these values in the same operation.
Update:
The only way that I could make it work is by first making a query to check if the (year, month) exists in the "monthly" array.
Depending if it exists, I either $push the new month, or $inc the existing one's values.
Pipeline update way, requires MongoDB >=4.2
You filter by _id using index, so it will be fast also.
Query
Test code here
(put id=1 will update(add points to member points and to externa point field), id=2 will do nothing,id=3 will insert the member to the empty array and update the points, id=4 will upsert with an array contains only this member,and the points its points)
Cases (this is the order checked in the $cond also)
document doesnt exists, array will have 1 single member, and root points will have the value of the new member points
document exists , member dont exists, monthly empty
adds the member {:year ...} and increases the root point field
document exists, member exists
increase points inside the member and the root point fields
document exists, member dont exist, monthly not empty
does nothing
db.collection.update({
"_id": 4
},
[
{
"$addFields": {
"isupsert": {
"$not": [
{
"$ne": [
{
"$type": "$monthly"
},
"missing"
]
}
]
}
}
},
{
"$addFields": {
"doc": {
"$switch": {
"branches": [
{
"case": "$isupsert",
"then": {
"_id": "$_id",
"points": 5,
"monthly": [
{
"year": 2021,
"month": 7,
"points": 5
}
]
}
},
{
"case": {
"$and": [
{
"$isArray": [
"$monthly"
]
},
{
"$eq": [
{
"$size": "$monthly"
},
0
]
}
]
},
"then": {
"_id": "$_id",
"points": 5,
"monthly": [
{
"year": 2021,
"month": 7,
"points": 5
}
]
}
}
],
"default": {
"$let": {
"vars": {
"found": {
"$not": [
{
"$eq": [
{
"$size": {
"$filter": {
"input": "$monthly",
"as": "m",
"cond": {
"$and": [
{
"$eq": [
"$$m.year",
2021
]
},
{
"$eq": [
"$$m.month",
7
]
}
]
}
}
}
},
0
]
}
]
}
},
"in": {
"$cond": [
"$$found",
{
"$mergeObjects": [
"$ROOT",
{
"points": {
"$add": [
"$points",
5
]
},
"monthly": {
"$map": {
"input": "$monthly",
"as": "m",
"in": {
"$cond": [
{
"$and": [
{
"$eq": [
"$$m.year",
2021
]
},
{
"$eq": [
"$$m.month",
7
]
}
]
},
{
"$mergeObjects": [
"$$m",
{
"points": {
"$add": [
"$$m.points",
5
]
}
}
]
},
"$$m"
]
}
}
}
}
]
},
"$$ROOT"
]
}
}
}
}
}
}
},
{
"$replaceRoot": {
"newRoot": "$doc"
}
},
{
"$unset": [
"isupsert"
]
}
],
{
"upsert": true
})
you should find in array by $elemMatch
UserPoints.findOneAndUpdate(
{
_id: userId,
"monthly":{$elemMatch:{"year": currentYear}},
"monthly":{$elemMatch:{"month": currentMonth}}
},
{
$inc: {
points: addPoints,
"monthly.$.points": addPoints,
},
},
{
upsert: true,
new: true,
}
).exec()

Zip two array and create new array of object

hello all i'm working with a MongoDB database where each data row is like:
{
"_id" : ObjectId("5cf12696e81744d2dfc0000c"),
"contributor": "user1",
"title": "Title 1",
"userhasRate" : [
"51",
"52",
],
"ratings" : [
4,
3
],
}
and i need to change it to be like:
{
"_id" : ObjectId("5cf12696e81744d2dfc0000c"),
"contributor": "user1",
"title": "Title 1",
rate : [
{userhasrate: "51", value: 4},
{userhasrate: "52", value: 3},
]
}
I already try using this method,
db.getCollection('contens').aggregate([
{ '$group':{
'rates': {$push:{ value: '$ratings', user: '$userhasRate'}}
}
}
]);
and my result become like this
{
"rates" : [
{
"value" : [
5,
5,
5
],
"user" : [
"51",
"52",
"53"
]
}
]
}
Can someone help me to solve my problem,
Thank you
You can use $arrayToObject and $objectToArray inside $map to achieve the required output.
db.collection.aggregate([
{
"$project": {
"rate": {
"$map": {
"input": {
"$objectToArray": {
"$arrayToObject": {
"$zip": {
"inputs": [
"$userhasRate",
"$ratings"
]
}
}
}
},
"as": "el",
"in": {
"userhasRate": "$$el.k",
"value": "$$el.v"
}
}
}
}
}
])
Alternative Method
If userhasRate contains repeated values then the first solution will not work. You can use arrayElemAt and $map along with $zip if it contains repeated values.
db.collection.aggregate([
{
"$project": {
"rate": {
"$map": {
"input": {
"$zip": {
"inputs": [
"$userhasRate",
"$ratings"
]
}
},
"as": "el",
"in": {
"userhasRate": {
"$arrayElemAt": [
"$$el",
0
]
},
"value": {
"$arrayElemAt": [
"$$el",
1
]
}
}
}
}
}
}
])
Try below aggregate, first of all you used group without _id that grouped all the JSONs in the collection instead set it to "$_id" also you need to create 2 arrays using old data then in next project pipeline concat the arrays to get desired output:
db.getCollection('contens').aggregate([
{
$group: {
_id: "$_id",
rate1: {
$push: {
userhasrate: {
$arrayElemAt: [
"$userhasRate",
0
]
},
value: {
$arrayElemAt: [
"$ratings",
0
]
}
}
},
rate2: {
$push: {
userhasrate: {
$arrayElemAt: [
"$userhasRate",
1
]
},
value: {
$arrayElemAt: [
"$ratings",
1
]
}
}
}
}
},
{
$project: {
_id: 1,
rate: {
$concatArrays: [
"$rate1",
"$rate2"
]
}
}
}
])