I would like to move an array stored in old_field that looks like this:
[{id: "XXX", ...}, {"id": "YYY", ...}, ...]
Into new_field looking like this:
{"XXX": {id: "XXX", ...}, "YYY":, {id: "YYY", ...}, ...}
As such, I attempted to do a few iterations of the following:
$addFields: {
new_field: {
$reduce: {
input: "$old_field",
initialValue: {},
in: {
{$getField: {field: "id", input: "$$this"}}: "$$this"
}
}
}
}
All of which failed. Note that doing:
$addFields: {
new_field: {
$reduce: {
input: "$old_field",
initialValue: {},
in: {
"1": {$getField: {field: "id", input: "$$this"}}
}
}
}
}
Returns a new_field w/ value 1: {the_correct_id_here}, so I know that the $getField works properly (besides likely using $$this in the wrong context).
Why isn't $getField working in this context? How would I go about doing this transformation?
$addField
1.1. $arrayToObject - Convert the array result from 1.1.1 to object.
1.1.1. $map - Iterate each element in the old_field array and return a new array. Convert each element to an object with k and v properties.
db.collection.aggregate([
{
$addFields: {
new_field: {
$arrayToObject: {
$map: {
input: "$old_field",
in: {
k: "$$this.id",
v: "$$this"
}
}
}
}
}
}
])
Demo # Mongo Playground
Related
I want to rename a field inside a object itself inside a nested array.
As example, I want to rename the all tags m2 to m6 in this document:
{
"_id": 1,
"tagsGroup": [
{
"id": "1234",
"tags": {
"m1": 1,
"m2": 2
}
},
{
"id": "456",
"tags": {
"m3": 1,
"m2": 2
}
},
{
"id": "1234",
"tags": {
"m4": 2,
"m5": 2
}
},
]
}
This is my current state of work:
db.collection.update({},
{
"$set": {"tagsGroup.$[tGp].tags.m6": "$tagsGroup.$[tGp].tags.m2"},
"$unset": {"tagsGroup.$[tGp].tags.m2": ""}
},
{
arrayFilters: [{"tGp.tags.m2": {$exists: 1}}],
multi: true}
)
Unfortunately, the $tagsGroup.$[tGp].tags.m6 is not interpreted.
Do you guys, have a way to do this?
Thanks!
Probably similar to this question MongoDB rename database field within array,
There is no straight way to rename fields within arrays with a single command. You can try update with aggregation pipeline starting from MongoDB v4.2,
$map to iterate loop of tagsGroup array
$map to iterate loop of tags object after converting to array using $objectToArray, it will return in k and v format
$replaceOne will replace specific string on find field, this is starting from MongoDB v4.4
$arrayToObject convert tags array returned by second $map back to object format
$mergeObjects to merge current object with updated tags object
db.collection.update(
{ "tagsGroup.tags.m2": { $exists: true } },
[{
$set: {
tagsGroup: {
$map: {
input: "$tagsGroup",
in: {
$mergeObjects: [
"$$this",
{
tags: {
$arrayToObject: {
$map: {
input: { $objectToArray: "$$this.tags" },
in: {
k: {
$replaceOne: {
input: "$$this.k",
find: "m2",
replacement: "m6"
}
},
v: "$$this.v"
}
}
}
}
}
]
}
}
}
}
}],
{ multi: true }
)
Playground
Imagine I have this collection:
{
id: 1,
b: {
"field1": ['foo'],
"field2": ['bar']
}
}
{
id: 2,
b: {
"field2": ["foobar"],
"field3": ["foofoo"]
}
}
And I want to obtain a new collection with MongoDB:
{
id: 1,
b_grouped: ['foo', 'bar']
}
{
id: 2,
b_grouped: ["foobar", "foofoo"]
}
I don't know all the name of the fields in the documents, anyone would have an idea of how to perform something like this:
db.collection.aggregate(
[
{ "$project": { "b_grouped": { $concatArrays: ["$b.*"] } } }
]
)
You can try,
$reduce input b as a array after converting from object to array using $objectToArray, this will convert object in "k" (key), "v" (value) format array of object,
$concatArrays to concat initialValue ($$value) of $raduce and array of b object's field $$this.v
db.collection.aggregate([
{
$project: {
b_grouped: {
$reduce: {
input: { $objectToArray: "$b" },
initialValue: [],
in: {
$concatArrays: ["$$this.v", "$$value"]
}
}
}
}
}
])
Playground
Is there a way in MongoDB to project all fields of a document that have a specific type?
For example if I have the following document:
{
_id: 5dde4c55c6c36b3bb4f5ad30,
name: "Peter",
age: 45,
division: "marketing"
}
I would like to say: Return only the fields of type string. This way I would end up with:
{
_id: 5dde4c55c6c36b3bb4f5ad30,
name: "Peter",
division: "marketing"
}
You can use $type to check the type of field,
$reduce input $$ROOT object as array using $objectToArray
check condition if field value type is string then concat with initialValue and return
return value will be array we need to convert it to array using $arrayToObject
$replaceWith will replace root to new returned object
db.collection.aggregate([
{
$replaceWith: {
$arrayToObject: {
$reduce: {
input: { $objectToArray: "$$ROOT" },
initialValue: [],
in: {
$concatArrays: [
"$$value",
{
$cond: [
{ $eq: [{ $type: "$$this.v" }, "string"] },
["$$this"],
[]
]
}
]
}
}
}
}
}
])
Playground
I have a document which looks like this
{
name:'John Doe',
phone: 'XXXXXXX',
field1: [
{I:20},
{J:60},
{K:20}
]
}
How do I get about restructuring the field1 to have a name instead of being anonymous. something like this:
field1: [
{
key: I,
value:20
},
{
key: J,
value:60
},
{
key: K,
value:20
}
]
Also is it possible to do in a bulk op coz I have a large number of data.
You can do it using both way,
update with aggregation pipeline from MpngoDB 4.2
using aggregate() and $out to collection
using update with aggregation pipeline updateMany()
first $set pipeline for convert object to array in k and v format
second $set pipeline for changing the name from k => key and v => value
db.collection.updateMany({},
[{
$set: {
field1: {
$reduce: {
input: "$field1",
initialValue: [],
in: {
$concatArrays: [
"$$value",
{
$objectToArray: "$$this"
}
]
}
}
}
}
},
{
$set: {
field1: {
$map: {
input: "$field1",
as: "f",
in: {
key: "$$f.k",
value: "$$f.v"
}
}
}
}
}
]
)
using aggregate() and $out
both $set are same as above method
$out Takes the documents returned by the aggregation pipeline and writes them to a specified collection
db.collection.aggregate([
{
$set: {
field1: {
$reduce: {
input: "$field1",
initialValue: [],
in: {
$concatArrays: [
"$$value",
{
$objectToArray: "$$this"
}
]
}
}
}
}
},
{
$set: {
field1: {
$map: {
input: "$field1",
as: "f",
in: {
key: "$$f.k",
value: "$$f.v"
}
}
}
}
},
{ $out: <"collection name"> }
])
Playground
I have a mongo document similar to following structure:
{
id: '111eef8b94d3e91f4c7d22a37deb4aad',
description: 'Secret Project',
title: 'secret project',
students: [
{ _id: '123', name: 'Alex', primary_subject: 'Math', address: 'xxxxx', dob: '1989-10-10', gender: 'F', nationality: 'German' },
{ _id: '124', name: 'Emanuel', primary_subject: 'Physics', address: 'yyyyyy', dob: '1988-05-07', gender: 'M', nationality: 'French' },
{ _id: '242', name: 'Mike', primary_subject: 'Chemistry', address: 'zzzz', dob: '1990-02-02', gender: 'M', nationality: 'English' }
]
}
I need to fetch specific attributes. For example want to fetch only name, primary_subject, nationality attributes.
Using the below mongo query, I am able to fetch all attributes.
db.student_projects.aggregate({
$project: {
"students": {
$filter: {
input: "$students",
as: "st",
cond: {
$eq: [ "$$st._id", "242" ]
}
},
}
}
},
{ $unwind: { path: "$students", preserveNullAndEmptyArrays: false } }
).pretty();
Above query fetches all attributes of the matching student. But in my case I need just 3 attributes.
Use $map to reshape the output array:
db.student_projects.aggregate({
$project: {
"students": {
$map: {
input: {
$filter: {
input: "$students",
as: "st",
cond: {
$eq: [ "$$st._id", "242" ]
}
}
},
in: {
name: "$$this.name",
primary_subject: "$$this.primary_subject",
nationality: "$$this.nationality"
}
}
}
}
},
{ $unwind: { path: "$students", preserveNullAndEmptyArrays: false } }
).pretty();
Just like it's other language counterparts, "reshaphing" arrays is what $map does.
If you want to get "fancy" with a longer list of "included" fields than "excluded", then there are some modern operators from later releases of MongoDB 3.6 and above which can help here:
db.student_projects.aggregate({
$project: {
"students": {
$map: {
input: {
$filter: {
input: "$students",
as: "st",
cond: {
$eq: [ "$$st._id", "242" ]
}
}
},
in: {
$arrayToObject: {
$filter: {
input: { $objectToArray: "$$this" },
cond: {
"$not": {
"$in": [ "$$this.k", [ "_id", "address", "dob" ] ]
}
}
}
}
}
}
}
}
},
{ $unwind: "$students" }
).pretty();
The $objectToArray transforms to "key/value" pairs of k and v representing the object keys and values. From this "array" you can $filter the results on the k values for those you don't want. The $in allows comparison to a "list", and the $not negates the comparison value.
Finally you can transform the "array" back into the object form via $arrayToObject.
And of course you could always simply $project after the $unwind:
db.student_projects.aggregate({
$project: {
"students": {
$filter: {
input: "$students",
as: "st",
cond: {
$eq: [ "$$st._id", "242" ]
}
},
}
}
},
{ $unwind: "$students" },
{ $project: {
"students": {
"name": "$students.name",
"primary_subject": "$students.primary_subject",
"nationality": "$students.nationality"
}
}
).pretty();
And if you don't want the "students" key, then just remove it:
{ $project: {
"name": "$students.name",
"primary_subject": "$students.primary_subject",
"nationality": "$students.nationality"
}
Or use $replaceRoot from the original $map version:
db.student_projects.aggregate({
$project: {
"students": {
$map: {
input: {
$filter: {
input: "$students",
as: "st",
cond: {
$eq: [ "$$st._id", "242" ]
}
}
},
in: {
name: "$$this.name",
primary_subject: "$$this.primary_subject",
nationality: "$$this.nationality"
}
}
}
}
},
{ $unwind: "$students" },
{ $replaceRoot: { newRoot: "$students" } }
).pretty();
But for that matter you could have also done a $match after the $unwind instead of even using $filter. It's generally more efficient to work with arrays in place though, and a lot of the time you don't need the $unwind at all, so it's good practice to get used to the methods that manipulate arrays.
Of course if the result with $replaceRoot or similar was really what you were looking for, then it would be far more advisable to not use embedded documents in an array at all. If your intended access pattern uses those embedded documents "separately" to the main document most of the time, then you really should consider keeping them in their own collection instead. This avoids the "aggregation overhead" and is a simple query and projection to return the data.
N.B The $unwind operator "defaults" to preserveNullAndEmptyArrays: false. So the original form does not need that specified, nor the path key. It's shorter to write this way unless you specifically intend to preserve those null and empty results.