Update name of a key in nested mongo document - mongodb

I did check on multiple platforms for the solution, however did not get the answer. Here is my problem:
I have a nested Mongo Document of the format:
{
_id: "xxxx",
"Type": <value>,
"Data" : {
"1": { <<< this key might differ for diff documents
"00": { <<< this key might differ for diff documents
"cont": "India"
},
"05": {
"cont": "India"
},
....
}
"32": {
"41": {
"cont": "India"
},
"44": {
"cont": "India"
},
....
}
}
}
I want to rename the key cont to country in all the documents in the collection, however as mentioned above the key 1, 00, 32 might differ for different documents hence I could not reach to a solution for this.
Is there something like:
db.collection.updateMany({Type: "abc"}, { $rename: { "Data.*.*.cont": "Data.*.*.country" }})
because the key cont resides at the third level after the key Data
Can someone please help me out here?
Thanks!

Using dynamic value as field name is generally considered as an anti-pattern. Nevertheless, you can still use $objectToArray to convert the nested objects into k-v tuples and manipulate them. Then converted it back using $arrayToObject
db.collection.update({},
[
{
"$addFields": {
"arr": {
// first conversion
"$objectToArray": "$Data"
}
}
},
{
"$addFields": {
"arr": {
// second conversion
"$map": {
"input": "$arr",
"as": "a",
"in": {
k: "$$a.k",
v: {
"$objectToArray": "$$a.v"
}
}
}
}
}
},
{
"$addFields": {
"arr": {
"$map": {
"input": "$arr",
"as": "a",
"in": {
k: "$$a.k",
v: {
"$map": {
"input": "$$a.v",
"as": "a2",
"in": {
k: "$$a2.k",
// rename the field `country`
v: {
country: "$$a2.v.cont"
}
}
}
}
}
}
}
}
},
{
"$addFields": {
"arr": {
"$map": {
"input": "$arr",
"as": "a",
"in": {
k: "$$a.k",
v: {
// 1st backward conversion
"$arrayToObject": "$$a.v"
}
}
}
}
}
},
{
"$project": {
_id: 1,
Type: 1,
"Data": {
// 2nd backward conversion
"$arrayToObject": "$arr"
}
}
}
])
Here is the Mongo playground for your reference.

Related

Mongodb aggregation convert array of pairs to key and list of values

Trying to condense an array with key value pairs into an array of objects with the key and all the unique values for that key.
I have a structure like:
{
fruits: [
{fruit: apple, type: gaja},
{fruit: apple, type: honey-crisp},
{fruit: apple, type: fuji},
{fruit: cherry, type: black},
{fruit: cherry, type: red},
{fruit: cherry, type: red},
]
}
How can I convert it to:
{
fruits: [
{fruit: apple, types: [gaja, honey-crisp, fuji]},
{fruit: cherry, types: [black, red]}
]
}
Using mongo aggregations I managed to get the first structure from my data using $group and $addToSet. Not sure how to map the array to new object with a key and list of values
Here's another way to do it by using "$reduce". Comments are in the aggregation pipeline.
db.collection.aggregate([
{
"$set": {
// rewrite fruits
"fruits": {
"$reduce": {
"input": "$fruits",
"initialValue": [],
"in": {
"$let": {
"vars": {
// get fruit index in $$value : will be -1 if not there
"idx": {"$indexOfArray": ["$$value.fruit", "$$this.fruit"]}
},
"in": {
"$cond": [
// is fruit not in $$value yet
{"$eq": ["$$idx", -1]},
// new fruit so put in $$value and make "type" an array
{
"$concatArrays": [
"$$value",
[{"$mergeObjects": ["$$this", {"type": ["$$this.type"]}]}]
]
},
// fruit already in $$value, so map $$value with "type" update
{
"$map": {
"input": "$$value",
"as": "val",
"in": {
"$cond": [
// is this array element not the right fruit?
{"$ne": ["$$val.fruit", "$$this.fruit"]},
// nope, leave the element as-is
"$$val",
// this element needs to be updated
{
"$mergeObjects": [
"$$val",
{
"type": {
"$cond": [
// is this "type" already in array?
{"$in": ["$$this.type", "$$val.type"]},
// yes, so leave it as-is
"$$val.type",
// this is a new "type", so add it to array
{"$concatArrays": ["$$val.type", ["$$this.type"]]}
]
}
}
]
}
]
}
}
}
]
}
}
}
}
}
}
}
])
Try it on mongoplayground.net.
Maybe something like this:
db.collection.aggregate([
{
$unwind: "$fruits"
},
{
$group: {
_id: "$fruits.fruit",
type: {
$push: "$fruits.type"
}
}
},
{
$project: {
fruit: "$_id",
type: 1,
_id: 0
}
},
{
$group: {
_id: "",
fruits: {
$push: "$$ROOT"
}
}
}
])
Explained:
Unwind the array
Group to form the type array ( you can use $push or $addToSet in case you need only unique )
Project the necessary fields
Group all documents inside single final one
Playground
Here's another, another way using a multiple "$map" and "$setUnion" to get unique array members.
db.collection.aggregate([
{
"$set": {
// rewrite fruits
"fruits": {
"$map": {
// map over unique fruits
"input": {"$setUnion": "$fruits.fruit"},
"as": "theFruit",
"in": {
// set fruit
"fruit": "$$theFruit",
// "type" are unique elements of fruits.type
// where fruits.fruit == theFruit
"type": {
"$setUnion": {
"$map": {
"input": {
"$filter": {
"input": "$fruits",
"as": "obj",
"cond": {"$eq": ["$$obj.fruit", "$$theFruit"]}
}
},
"in": "$$this.type"
}
}
}
}
}
}
}
}
])
Try it on mongoplayground.net.

How to rename mongodb field using regex expression

I'm trying to update the name of a field in the mongodb document using the regex expression on the name of that field but I can't, can someone help me to do it?
I have a person document that contains a field with the email of the person, the name of that field is like that "firstname_lastname#gmail.com" and I want to replace "_" character in all email fields by "_dot_".
Here is what I did
:
db.getCollection("person").updateMany(
{
{'email.'+'.*_.*': { $exists: true }
},
{
$rename:{'email.'+'.*_.*': 'email.'+'.*_dot_.*'
});
and here the structure of my document:
person {
name: { .... }
email:{
firstname_lastname1#gmail.com : {
.... other fields
},
firstname_lastname2#gmail.com : {
.... other fields
}
}
}
thanks in advance
As #Wernfried Domscheit suggested in the comment, you should change your schema to avoid using dynamic email address as field name.
Nevertheless, you can use $objectToArray to convert the emails into an array of k-v tuples. Then, $split them by _ and rejoin by _dot_. Finally do a $arrayToObject to convert back to original structure.
db.collection.aggregate([
{
"$addFields": {
"email": {
"$objectToArray": "$email"
}
}
},
{
"$addFields": {
"email": {
"$map": {
"input": "$email",
"as": "e",
"in": {
k: {
"$ltrim": {
"input": {
"$reduce": {
"input": {
"$split": [
"$$e.k",
"_"
]
},
"initialValue": "",
"in": {
"$concat": [
"$$value",
"_dot_",
"$$this"
]
}
}
},
"chars": "_dot_"
}
},
v: "$$e.v"
}
}
}
}
},
{
"$addFields": {
"email": {
"$arrayToObject": "$email"
}
}
}
])
Here is the Mongo Playground for your reference.

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"
}
}
}
])

How to check if a key exists in a mongodb object where the key is a value of some another field in the document while doing aggregation?

First of all I know we can check if a key exists using the dot operator but in my case it is not working and I dont know why.
So far in the aggregation pipeline I have the following records.
{
"my_key":"1234"
"data":{
1234:"abc"
4567:"xyz"
}
}
{
"my_key":"6666"
"data":{
1234:"abc"
4567:"xyz"
}
}
I want to return the document where the my_key value does not exists in the data object. So according to the above example it should return the 2nd document.
I was trying using the $match operator as following but it does not seem to work.
$match :
{
"data.$my_key":{$exists:false}
}
This does not work and I dont get why :(
Is it because the my_key value is a string and the keys in the data object are not strings?
playground
db.collection.aggregate([
{
"$project": {//Reshape the data
"data": {
"$objectToArray": "$data"
},
"my_key": 1
}
},
{
"$unwind": "$data"
},
{
"$match": {//matching
"$expr": {
"$eq": [
"$data.k",
"$my_key"
]
}
}
}
])
Another way
Wihtout unwind
db.collection.aggregate([
{
"$project": {
"data": {
"$objectToArray": "$data"
},
"my_key": 1
}
},
{
$project: {
"output": {
"$map": {
"input": "$data",
"as": "data",
"in": {
"$eq": [
"$$data.k",
"$my_key"
]
}
}
},
"data": 1,
"my_key": 1
}
},
{
$match: {
output: true
}
}
])
If you need original format of data, you can add the below as last stage
{
$project: {
"data": {
"$arrayToObject": "$data"
},
"my_key": 1
}
}

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

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;
})