Mongodb $ (dollar sign) project query for arrays - arrays

I have the following data:
{
_id: "1"
transitions: [
{
"_id" : "11"
"name" : "Tr1"
"checkLists" : [
{ _id: "111", name: "N1"},
{ _id: "112", name: "N2"}
]
}
]
}
I used the following code to get the name N2 by query of _id:112
db.collection.findOne({ 'transitions.checkLists._id: new ObjectId("112") } }}, { 'transitions.checkLists.$': 1 })
but the result returns back both of them:
{ _id: ObjectId("1"),
transitions:
[ { checkLists:
[ { name: 'N1', _id: ObjectId("111") },
{ name: 'N2', _id: ObjectId("112") } ] } ] }
I would like to find and get only the name N2 by query of _id:112
Expected Result:
{ _id: ObjectId("1"),
transitions:
[ { checkLists:
[ { name: 'N2', _id: ObjectId("112") } ] } ] }

You can do it via the aggregation framework as follow:
db.collection.aggregate([
{
$match: {
"transitions.checkLists._id": "111"
}
},
{
"$addFields": {
"transitions": {
"$map": {
"input": "$transitions",
"as": "t",
"in": {
"$mergeObjects": [
"$$t",
{
"checkLists": {
"$filter": {
"input": "$$t.checkLists",
"as": "c",
"cond": {
$eq: [
"$$c._id",
"111"
]
}
}
}
}
]
}
}
}
}
},
{
"$addFields": {
transitions: {
$filter: {
input: "$transitions",
as: "elem",
cond: {
"$ne": [
"$$elem.checkLists",
[]
]
}
}
}
}
}
])
Explained:
Match the transitions.checkLists._id element in first stage.
Map/mergeobjects with filtered checklists to filter only the needed object from the transitions.checkLists array.
Remove the transitions elements where no checkList exists.
( this need to be done to remove elements for same document where there is no matching _id's )
Playground

Related

How to query two collections with related data?

I have 2 collections, collection A has some documents like {'id':1,'field':'name'},{'id':1,'field':'age'},and collection B has some documents like
{'_id':1,'name':'alice','age':18,'phone':123},{'_id':2,'name':'bob','age':30,'phone':321}
and I want to find all the document whose '_id' is in collectionA, and just project the corresponding field.
for example:
collection A
{'id':1,'field':'name'},
{'id':1,'field':'age'}
collection B
{'_id':1,'name':'alice','age':18,'phone':123},
{'_id':2,'name':'bob','age':30,'phone':321}
the result is:
{'name':'alice','age':18},
I don't know if there is an easy way to do that?
You can use $lookup to join two collection
db.col1.aggregate([
{
$match: {
id: 1
}
},
{
"$lookup": {
"from": "col2",
"localField": "id",
"foreignField": "_id",
"as": "listNames"
}
},
{
$project: {
listNames: {
$first: "$listNames"
}
}
},
{
$project: {
_id: 0,
name: "$listNames.name",
age: "$listNames.age"
}
}
])
Mongo Playground: https://mongoplayground.net/p/E-0WvK_SUS_
So the idea is:
Convert the documents in to key, value pair for both the collections using $objectToArray.
Then perform a join operation based on key k and (id <-> _id) using $lookup.
Replace the result as root element using $replaceRoot.
Convert array to object using $arrayToObject and again $replaceRoot.
Query:
db.colB.aggregate([
{
$project: {
temp: { $objectToArray: "$$ROOT" }
}
},
{
$lookup: {
from: "colA",
let: { temp: "$temp", colB_id: "$_id" },
pipeline: [
{
$addFields: {
temp: { k: "$field", v: "$id" }
}
},
{
$match: {
$expr: {
$and: [
{ $in: ["$temp.k", "$$temp.k"] },
{ $eq: ["$temp.v", "$$colB_id"] }
]
}
}
},
{
$replaceRoot: {
newRoot: {
$first: {
$filter: {
input: "$$temp",
as: "item",
cond: { $eq: ["$field", "$$item.k"] }
}
}
}
}
}
],
as: "array"
}
},
{
$replaceRoot: {
newRoot: { $arrayToObject: "$array" }
}
}
]);
Output:
{
"name" : "alice",
"age" : 18
}

Update value in nested array of mongo 3.5

I want to make an upsert call to update as well as insert my data in nested array of mongo db.
This is my mongo document.
{
"_id" : "575",
"_class" : "com.spyne.sharing.SpyneShareUserProject",
"spyneSharePhotoList" : [
{
"_id" : "fxLO68XyMR",
"spyneShareUsers" : [
{
"_id" : "chittaranjan#eventila.com",
"selectedByClient" : false
},
{
"_id" : "chittaranjan#gmail.com",
"selectedByClient" : false
}
]
},
{
"_id" : "nVpD0KoQAI",
"spyneShareUsers" : [
{
"_id" : "chittaranjan#eventila.com",
"selectedByClient" : true
}
]
},
{
"_id" : "Pm0B3Q9Igv",
"spyneShareUsers" : [
{
"_id" : "chittaranjan#gmail.com",
"selectedByClient" : true
}
]
}
]
}
Here my requirement is,
lets say i have an ID i.e. 575 (_id)
Then i will have the nested array ID i.e. fxLO68XyMR (spyneSharePhotoList._id)
Then i will have nested email id as ID i.e. chittaranjan#eventila.com (spyneSharePhotoList.spyneShareUsers._id) and selectedByClient (boolean)
Now i want is to check if this ID (spyneSharePhotoList.spyneShareUsers._id) is already present in the desired location i want to update the boolean value i.e. selectedByClient (true / false) according to that email id.
If the id is not present in the array, the it will make a new entry. as
{
"_id" : "chittaranjan#gmail.com",
"selectedByClient" : false
}
in spyneShareUsers list.
Please help me to do this task. Thank you
Most likely this is not the smartest solution, but it should work:
shareUserProject = {
id: "575",
PhotoListId: "fxLO68XyMR",
ShareUserId: "chittaranjan_new#eventila.com"
}
db.collection.aggregate([
{ $match: { _id: shareUserProject.id } },
{
$facet: {
root: [{ $match: {} }],
update: [
{ $unwind: "$spyneSharePhotoList" },
{ $match: { "spyneSharePhotoList._id": shareUserProject.PhotoListId } },
{
$set: {
"spyneSharePhotoList.spyneShareUsers": {
$concatArrays: [
{
$filter: {
input: "$spyneSharePhotoList.spyneShareUsers",
cond: { $ne: ["$$this._id", shareUserProject.ShareUserId] }
}
},
[{
_id: shareUserProject.ShareUserId,
selectedByClient: { $in: [shareUserProject.ShareUserId, "$spyneSharePhotoList.spyneShareUsers._id"] }
}]
]
}
}
}
]
}
},
{ $unwind: "$root" },
{ $unwind: "$update" },
{
$set: {
"root.spyneSharePhotoList": {
$concatArrays: [
["$update.spyneSharePhotoList"],
{
$filter: {
input: "$root.spyneSharePhotoList",
cond: { $ne: ["$$this._id", shareUserProject.PhotoListId] }
}
}
]
}
}
},
{ $replaceRoot: { newRoot: "$root" } }
]).forEach(function (doc) {
db.collection.replaceOne({ _id: doc._id }, doc);
})
I did not check whether all operators are available in MongoDB 3.5
My goal was to process everything in aggregation pipeline and run just a single replaceOne() at the end.
Here another solution based on $map operator:
db.collection.aggregate([
{ $match: { _id: shareUserProject.id } },
{
$set: {
spyneSharePhotoList: {
$map: {
input: "$spyneSharePhotoList",
as: "photoList",
in: {
$cond: {
if: { $eq: [ "$$photoList._id", shareUserProject.PhotoListId ] },
then: {
"_id": "$$photoList._id",
spyneShareUsers: {
$cond: {
if: { $in: [ shareUserProject.ShareUserId, "$$photoList.spyneShareUsers._id" ] },
then: {
$map: {
input: "$$photoList.spyneShareUsers",
as: "shareUsers",
in: {
$cond: {
if: { $eq: [ "$$shareUsers._id", shareUserProject.ShareUserId ] },
then: { _id: shareUserProject.ShareUserId, selectedByClient: true },
else: "$$shareUsers"
}
}
}
},
else: {
$concatArrays: [
"$$photoList.spyneShareUsers",
[ { _id: shareUserProject.ShareUserId, selectedByClient: false } ]
]
}
}
}
},
else: "$$photoList"
}
}
}
}
}
}
]).forEach(function (doc) {
db.collection.replaceOne({ _id: doc._id }, doc);
})
You can achieve the same result also with two updates:
shareUserProject = {
id: "575",
PhotoListId: "fxLO68XyMR_x",
ShareUserId: "chittaranjan_new#gmail.com"
}
ret = db.collection.updateOne(
{ _id: shareUserProject.id },
{ $pull: { "spyneSharePhotoList.$[photoList].spyneShareUsers": { _id: shareUserProject.ShareUserId } } },
{ arrayFilters: [{ "photoList._id": shareUserProject.PhotoListId }] }
)
db.collection.updateOne(
{ _id: shareUserProject.id },
{ $push: { "spyneSharePhotoList.$[photoList].spyneShareUsers": { _id: shareUserProject.ShareUserId, selectedByClient: ret.modifiedCount == 1 } } },
{ arrayFilters: [{ "photoList._id": shareUserProject.PhotoListId }] }
)

Updating data type to an Object in mongoDB

I have changed one of the fields of my collection in mongoDB from an array of strings to an array of object containing 2 strings. New documents get inserted without any problem, but when a get method is called to get , querying all the documents I get this error:
Failed to decode 'Students'. Decoding 'photoAddresses' errored
with: readStartDocument can only be called when CurrentBSONType is
DOCUMENT, not when CurrentBSONType is STRING.
photoAddresses is the field that was changed in Students.
I was wondering is there any way to update all the records so they all have the same data type, without losing any data.
The old version of photoAdresses:
"photoAddresses" : ["something","something else"]
This should be updated to the new version like this:
"photoAddresses" : [{photoAddresses:"something"},{photoAddresses:"something else"}]
The following aggregation queries update the string array to object array, only if the array has string elements. The aggregation operator $map is used to map the string array elements to objects. You can use any of the two queries.
db.test.aggregate( [
{
$match: {
$expr: { $and: [ { $isArray: "$photo" },
{ $gt: [ { $size: "$photo" }, 0 ] }
]
},
"photo.0": { $type: "string" }
}
},
{
$project: {
photo: {
$map: {
input: "$photo",
as: "ph",
in: { addr: "$$ph" }
}
}
}
},
] ).forEach( doc => db.test.updateOne( { _id: doc._id }, { $set: { photo: doc.photo } } ) )
The following query works with MongoDB version 4.2+ only. Note the update operation is an aggregation instead of an update. See updateMany.
db.test.updateMany(
{
$expr: { $and: [ { $isArray: "$photo" },
{ $gt: [ { $size: "$photo" }, 0 ] }
]
},
"photo.0": { $type: "string" }
},
[
{
$set: {
photo: {
$map: {
input: "$photo",
as: "ph",
in: { addr: "$$ph" }
}
}
}
}
]
)
[EDIT ADD]: The following query works with version MongoDB 3.4:
db.test.aggregate( [
{
$addFields: {
matches: {
$cond: {
if: { $and: [
{ $isArray: "$photoAddresses" },
{ $gt: [ { $size: "$photoAddresses" }, 0 ] },
{ $eq: [ { $type: { $arrayElemAt: [ "$photoAddresses", 0 ] } }, "string" ] }
] },
then: true,
else: false
}
}
}
},
{
$match: { matches: true }
},
{
$project: {
photoAddresses: {
$map: {
input: "$photoAddresses",
as: "ph",
in: { photoAddresses: "$$ph" }
}
}
}
},
] ).forEach( doc => db.test.updateOne( { _id: doc._id }, { $set: { photoAddresses: doc.photoAddresses } } ) )

Simple calculation on array of arrays

I am having the following collection:
{
"price" : [
55800000,
62800000
],
"surface" : [
81.05,
97.4
],
}
I would want to calculate the price/m2. I have tried the following but got the following error: "$divide only supports numeric types, not array and array".
db.entries.aggregate(
[
{ $project: { url: 1, pricePerSquareMeter: { $divide: [ "$price", "$surfaces" ] } } }
]
)
Would you know how to solve this? Ultimately would want to have an array like this:
{
"price" : [
55800000,
62800000
],
"surface" : [
81.05,
97.4
],
"pricePerSquareMeter" : [
688463.91,
644763.86
]
}
Important: The price and surface should also be ordered so that the calculation is valid.
You can use below aggregation
db.collection.aggregate([
{ "$addFields": {
"pricePerSquareMeter": {
"$map": {
"input": { "$range": [0, { "$size": "$price" }] },
"in": {
"$divide": [
{ "$arrayElemAt": ["$price", "$$this"] },
{ "$arrayElemAt": ["$surface", "$$this"] }
]
}
}
}
}}
])
MongoPlayground
You can use below aggregation
db.entries.aggregate([
{ $addFields: {
pricePerSquareMeter: {
$map: {
input: '$price',
as: 'item',
in: {
$divide: [
"$$item",
{ $arrayElemAt: [
"$surface",
{ "$indexOfArray": ["$price", "$$item"] }
]}
]
}
}
},
}}])

Unable to use $elemMatch in aggregation

As the title suggests I am unable to use $elemMatch inside aggregate. I have attached my code below, and I would like to know if there is an alternative to this code. I'm trying to match and get all the elements of an array.
db.collection.aggregate([
{
$match: {
s: "A",
$or: [
{
id: "123"
},
{
name: "Raj"
}
],
condition: {
$elemMatch: {
$and: [
{
"depend.id": "123",
"depend.ques": "test ques"
},
{
depend: {
$size: 1
}
}
]
}
}
}
},
{
$lookup: {
from: "categories",
localField: "category",
foreignField: "_id",
as: "category"
}
},
{
$unwind: "$category"
}
])
Sample document:
{
"_id" : ObjectId("5d723e34ef5e6630fde5b71d"),
"id" : 1,
"name" : "Raj",
"category" : 123,
"condition" : [
{
"depend" : [
{
"id" : 1,
"ques" : "test ques"
}
]
},
{
"depend" : [
{
"id" : 2,
"ques" : "test 2nd ques"
}
]
}
]
}
The sample document should be matched with the above query.
Issues:
There is no s field in the sample document and you are applying a filter on in i.e. s: "A"
Both id and depend.id are numeric but are matched with
string literals
There is no document in condition array with depend.id as 123
Please refer the following query:
db.collection.aggregate([
{
$match:{
$or:[
{
"id":123
},
{
"name":"Raj"
}
],
"condition":{
$elemMatch:{
$and:[
{
"depend.id":1,
"depend.ques": "test ques"
},
{
"depend":{
$size:1
}
}
]
}
}
}
}
]).pretty()
This Query is perfectly working on your data.
db.getCollection('stackans').aggregate([
{
$match: {
$or: [
{
id: 1
},
{
name: "Raj"
}
],
condition: {
$elemMatch: {
$and: [
{
"depend.id": 1,
"depend.ques": "test ques"
},
{
depend: {
$size: 1
}
}
]
}
}
}
} ,{
$lookup: {
from: "categories",
localField: "category",
foreignField: "_id",
as: "category"
}
}
])

Resources