MongoDB: Select element from array based on another property in the document - mongodb

I have a MongoDB collection with documents of the following structure (non-interesting bits left out):
{
displayFieldId: "abcd",
fields: [
{
fieldId: "efgh",
value: "cake"
},
{
fieldId: "abcd",
value: "cheese"
},
....
],
....
}
I would like to run a query on this collection to fetch only the element in the fields array which fieldId matches the document's displayFieldId. The result of the query on the document above should thus be:
{
fields: [
{
fieldId: "abcd",
value: "cheese"
}
],
....
}
I constructed the following query. It does what I want, with the exception that the displayFieldValue is hard coded
db.containers.find({}, {
fields: {
$elemMatch: {
fieldId: "abcd"
}
}
});
Is there a way to make it look at the document's displayFieldId and use that value instead of the hard coded "abcd"?
The server is running MongoDB 3.2.6
If possible, I would like to do this without aggregation, but if that can't be done, then aggregation will have to do

With aggregation framework:
db.containers.aggregate([
{
"$redact": {
"$cond": [
{
"$anyElementTrue": [
{
"$map": {
"input": "$fields",
"as": "el",
"in": {
"$eq": ["$$el.fieldId", "$displayFieldId"]
}
}
}
]
},
"$$KEEP",
"$$PRUNE"
]
}
},
{
"$project": {
"displayFieldId": 1,
"fields": {
"$filter": {
"input": "$fields",
"as": "el",
"cond": {
"$eq": ["$$el.fieldId", "$displayFieldId"]
}
}
},
"otherfields": 1,
....
}
}
])
MongoDB 3.4:
db.containers.aggregate([
{
"$redact": {
"$cond": [
{
"$anyElementTrue": [
{
"$map": {
"input": "$fields",
"as": "el",
"in": {
"$eq": ["$$el.fieldId", "$displayFieldId"]
}
}
}
]
},
"$$KEEP",
"$$PRUNE"
]
}
},
{
"$addFields": {
"fields": {
"$filter": {
"input": "$fields",
"as": "el",
"cond": {
"$eq": ["$$el.fieldId", "$displayFieldId"]
}
}
}
}
}
])
Without aggregation framework - using $where (the slow query):
db.containers.find({
"$where": function() {
var self = this;
return this.fields.filter(function(f){
return self.displayFieldId === f.fieldId;
}).length > 0;
}
}).map(function(doc){
var obj = doc;
obj.fields = obj.fields.filter(function(f){
return obj.displayFieldId === f.fieldId;
});
return obj;
})

Related

mongodb aggregation with multidimensional array

How can i aggregate two collections if i have the index in multidimensional array?
//collection 1 item example:
{
_id: ObjectId('000'),
trips: [ // first array
{ // trip 1
details: [ // array in first array
{ // detail 1
key: ObjectId('111') // ==> _id i want aggregate
}
]
}
]
}
//collection 2 item exampe:
{
_id: ObjectId('111'),
description: 'any info here...'
}
what is the procedure to do in order to obtain a valid result?
I don't want to reprocess every data after doing a "find".
This is the result I would like to get
//collection 1 after aggregate:
{
_id: ObjectId('000'),
trips: [
{ // trip 1
details: [
{ // detail 1
key: ObjectId('111') // id i want aggregate
_description_obj: {
_id: ObjectId('111'),
description: 'any info here...'
}
}
]
}
]
}
Here's one way you could do it.
db.coll1.aggregate([
{
"$match": {
_id: ObjectId("000000000000000000000000")
}
},
{
"$lookup": {
"from": "coll2",
"localField": "trips.details.key",
"foreignField": "_id",
"as": "coll2"
}
},
{
"$set": {
"trips": {
"$map": {
"input": "$trips",
"as": "trip",
"in": {
"$mergeObjects": [
"$$trip",
{
"details": {
"$map": {
"input": "$$trip.details",
"as": "detail",
"in": {
"$mergeObjects": [
"$$detail",
{
"_description_obj": {
"$first": {
"$filter": {
"input": "$coll2",
"cond": {"$eq": ["$$this._id", "$$detail.key"]}
}
}
}
}
]
}
}
}
}
]
}
}
},
"coll2": "$$REMOVE"
}
}
])
Try it on mongoplayground.net.

mongodb. how map strings to integers from another collection

I have collection with documents, for example:
[
{
'id':'1'
'some_field':'test',
'rates':[
{'user_id':'12','rate':'very_good'},
{'user_id':'13','rate':'very_good'}
{'user_id':'14','rate':'bad'},
{'user_id':'15','rate':'normal'}
]
}
]
And i have collection with values of rates in string:
[
{
"rate_name" : "bad",
"rate_value" : 1
},
{
"rate_name" : "normal",
"rate_value" : 2
},
{
"rate_name" : "very_good",
"rate_value" : 3
},
]
I need map data from first collection from array rates with value from second collection and group this values to new field.
For example:
[
{
'id':'1'
'some_field':'test',
'rates':[3,3,1,2]
]
}
]
How i can do this?
Here's one way you could do it if "$getField" is available (MongoDB server version 5.0 and above). See below for another option.
db.ratings.aggregate([
{
"$lookup": {
"from": "ratingScale",
"as": "ratingScale",
"pipeline": []
}
},
{
"$set": {
"rates": {
"$map": {
"input": "$rates",
"as": "rate",
"in": {
"$getField": {
"field": "rate_value",
"input": {
"$first": {
"$filter": {
"input": "$ratingScale",
"as": "nameValue",
"cond": {"$eq": ["$$rate.rate", "$$nameValue.rate_name"]}
}
}
}
}
}
}
}
}
},
{"$unset": "ratingScale"}
])
Try it on mongoplayground.net.
If "$getField" is unavailable, here's another way you could do it.
db.ratings.aggregate([
{
"$lookup": {
"from": "ratingScale",
"as": "ratingScale",
"pipeline": []
}
},
{
"$set": {
"rates": {
"$map": {
"input": "$rates",
"as": "rate",
"in": {
"$first": {
"$filter": {
"input": "$ratingScale",
"as": "nameValue",
"cond": {"$eq": ["$$rate.rate", "$$nameValue.rate_name"]}
}
}
}
}
}
}
},
{
"$set": {
"rates": {
"$map": {
"input": "$rates",
"as": "rate",
"in": "$$rate.rate_value"
}
}
}
},
{"$unset": "ratingScale"}
])
Try it on mongoplayground.net.
You can do a plain $unwind then $lookup approach.
db.col1.aggregate([
{
$unwind: "$rates"
},
{
"$lookup": {
"from": "col2",
"localField": "rates.rate",
"foreignField": "rate_name",
"as": "rates"
}
},
{
"$unwind": "$rates"
},
{
$group: {
_id: "$_id",
some_field: {
$first: "$some_field"
},
rates: {
$push: "$rates.rate_value"
}
}
}
])
Mongo Playground

MongoDB 5 version Aggregation convert to 4.4 version

I have the following aggregation that is supported by MongoDB 5 but not 4.4. How can I write this in v4.4?
Aggregation Pipeline (V5):
"$ifNull": [
{
"$getField": {
"field": "prices",
"input": {
"$first": "$matchedUsers"
}
}
},
[]
]
Here's a MongoDB Playground for the same.
This pipeline should work in version 4.4:
db.datasets.aggregate([
{
"$lookup": {
"from": "users",
"localField": "assignedTo",
"foreignField": "id",
"as": "matchedUsers"
}
},
{
"$addFields": {
"cgData": {
"$first": "$matchedUsers"
}
}
},
{
"$addFields": {
"cgData": {
"$first": {
"$filter": {
"input": {
"$ifNull": [
"$cgData.prices",
[]
]
},
"as": "currentPrice",
"cond": {
"$and": [
{
"$gte": [
"$firstBillable",
"$$currentPrice.beginDate"
]
},
{
$or: [
{
$eq: [
{
$type: "$$currentPrice.endDate"
},
"missing"
]
},
{
"$lt": [
"$firstBillable",
"$$currentPrice.endDate"
]
}
]
}
]
}
}
}
}
}
},
{
"$addFields": {
cgPrice: "$cgData.price"
}
},
{
"$project": {
cgData: 0,
"matchedUsers": 0
}
}
])
In this, a new $addFields stage is added, to get first element of matchedUsers array.
{
"$addFields": {
"cgData": {
"$first": "$matchedUsers"
}
}
}
Then we use $ifNull like this:
{
"$ifNull": [
"$cgData.prices",
[]
]
}
See it working here.

$mergeObjects requires object inputs, but input is of type array

I am trying to add one property if current user has permission or not based on email exists in array of objects.
My input data looks like below.
[
{
nId: 0,
children0: [
{
nId: 3,
access: [
{
permission: "view",
email: "user1#email.com"
}
]
},
{
nId: 4,
access: [
{
permission: "view",
email: "user2#email.com"
}
]
}
]
}
]
https://mongoplayground.net/p/xZmRGFharAb
[
{
"$addFields": {
"children0": {
"$map": {
"input": "$children0.access",
"as": "accessInfo",
"in": {
"$cond": [
{
"$eq": [
"$$accessInfo.email",
"user1#email.com"
]
},
{
"$mergeObjects": [
"$$accessInfo",
{
"hasAccess": true
}
]
},
{
"$mergeObjects": [
"$$accessInfo",
{
"hasAccess": false
}
]
},
]
}
}
}
}
}
]
I also tried this answer as following, but that is also not merging the object.
https://mongoplayground.net/p/VNXcDnXl_sZ
Try this:
db.collection.aggregate([
{
"$addFields": {
"children0": {
"$map": {
"input": "$children0",
"as": "accessInfo",
"in": {
nId: "$$accessInfo.nId",
access: "$$accessInfo.access",
hasAccess: {
"$cond": {
"if": {
"$ne": [
{
"$size": {
"$filter": {
"input": "$$accessInfo.access",
"as": "item",
"cond": {
"$eq": [
"$$item.email",
"user1#email.com"
]
}
}
}
},
0
]
},
"then": true,
"else": false
}
}
}
}
}
}
}
])
Here, we use one $map to loop over children0 and then we filter the access array to contain only elements with matching emails. If the filtered array is non-empty, we set hasAccess to true.
Playground link.

Mongodb aggregate use field reference

Data:
{
"_id": "test1",
"orderStatus": "shipped",
"history": {
"pending": {startAt: '2021/03/16'},
"shipped": {startAt: '2021/03/18'},
}
}
Is it possible to access sub document by another field?
I'd like to get current order status startAt in an aggregation pipeline, for example:
db.aggregate([{$addFields: { currentStartAt: "history.$orderStatus" }}])
but it doesn't work.
Try this one:
db.collection.aggregate([
{ $set: { history: { $objectToArray: "$history" } } },
{ $set: { history: { $filter: { input: "$history", cond: { $eq: ["$orderStatus", "$$this.k"] } } } } },
{ $project: { currentStartAt: { $first: "$history.v.startAt" } } }
])
Within a mongo shell you can also do this one:
var field = db.collection.findOne({}, { orderStatus: 1 }).orderStatus;
var field = "$history." + field + ".startAt";
db.collection.aggregate([
{ $project: { currentStartAt: field } }
])
This also works but I have no idea about performance, let me know how it performs.
db.collection.aggregate([
{
"$addFields": {
"currentStartAt": {
"$arrayElemAt": [
{
"$map": {
"input": {
"$filter": {
"input": {
"$objectToArray": "$history"
},
"as": "el",
"cond": {
"$eq": [
"$orderStatus",
"$$el.k"
]
}
}
},
"in": "$$this.v.startAt"
}
},
0
]
}
}
},
{
"$project": {
"currentStartAt": 1
}
}
])
Another query doing same thing
db.collection.aggregate([
{
"$addFields": {
"currentStartAt": {
"$filter": {
"input": {
"$objectToArray": "$history"
},
"cond": {
"$eq": [
"$orderStatus",
"$$this.k"
]
}
}
}
}
},
{
"$project": {
"currentStartAt": {
"$first": "$currentStartAt.v.startAt"
}
}
}
])