$replaceRoot of embedded document inside an array in MongoDB - mongodb

I am fairly new to MongoDB and I came across the $replaceRoot(aggregation) which I need to output the document the way I need it.
Document Schema
{
name: "Apple",
image: "",
is_fruit: true,
other: [
{
url: "",
quotes: { // saved as ObjectId and is then lookedup or populated
text: "An apple a day keeps the doctor away"
}
}
]
}
Wanted Output
{
name: "Apple",
image: "",
is_fruit: true,
other: [
{
text: "An apple a day keeps the doctor away"
},
...
]
}
i.e: To make quotes field as the root of the other array and not the root of entire document.
Thank you.

There is no $replaceRoot like for embedded array but you can achieve similar effect using $map to transform the array into new format.
Something like
db.col.aggregate([
{
"$addFields": {
"other": {
"$map": {
"input": "$other",
"as": "res",
"in": {
"text": "$$res.text"
}
}
}
}
}
])
You can easily do something similar in client side code.

By using $replaceRoot(aggregation) here is the output.
P.S: The collection name I used is 'demoStack' replace it with your specific collections.
Query is :
db.demoStack.aggregate([
{ $replaceRoot: { newRoot: { $mergeObjects: [
{name:"$name"},
{ image: "$image" },
{is_fruit:"$is_fruit"},
{other:"$other.quotes"}
] } } } ])

Related

how to query through hashmap data using mongo terminal

I have data stored in following format in mongodb .. help me in knowing how can I write a query to find if the database has a particular id stored in one of the sheet or not
the structure of data is like the following :-
{
"name": "abc",
"linked_data": {
"sheet 1": [
"7d387e05d3f8180a",
"8534sfjog904395"
],
"sheet 2": [
"7d387e05d3f8180a",
"54647sgdsevey67r34"
]
}
}
for example if id "8534sfjog904395" is mapped with "sheet 1".. then it should return me this data where id is mapped with the sheet.
I am passing id in the query and want to find inside linked_data and then all the sheets
Using dynamic value as field name is considered as an anti-pattern and introduce unnecessary complexity to queries. Nevertheless, you can use $objectToArray to convert the linked_data into array of k-v tuples. $filter to get only the sheet you want. Finally, revert back to original structure using $arrayToObject
db.collection.aggregate([
{
"$set": {
"linked_data": {
"$objectToArray": "$linked_data"
}
}
},
{
$set: {
linked_data: {
"$filter": {
"input": "$linked_data",
"as": "ld",
"cond": {
"$in": [
"8534sfjog904395",
"$$ld.v"
]
}
}
}
}
},
{
"$set": {
"linked_data": {
"$arrayToObject": "$linked_data"
}
}
},
{
$match: {
linked_data: {
$ne: {}
}
}
}
])
Mongo Playground

How can I $unset a sub-document that's part of another sub-document in MongoDB?

I have a collection containing documents of the following form:
{
tokens: {
name: {
value: "...",
},
...
}
}
name can be anything, and there can be multiple embedded documents assigned to different keys, all of which have a value field.
How do I $unset all embedded documents that have a certain value? { $unset: { "tokens.value": "test" } } doesn't work, and nor does "tokens.$[].value" or "tokens.$.value".
You can use pipelined form of update, like this:
db.collection.update({},
[
{
"$set": {
"tokens": {
$arrayToObject: {
"$filter": {
"input": {
"$objectToArray": "$$ROOT.tokens"
},
"as": "item",
"cond": {
"$ne": [
"$$item.v.value",
"test"
]
}
}
}
}
}
}
],
{
multi: true
})
Playground link.
In this query, we do the following:
We convert tokens object to an array, using $objectToArray.
We filter out the array elements which have a value key present in embedded documents and have a value of test using $filter.
Finally we convert the filtered array back again to object, and assign it to tokens key.

MongoDB - Unable to add timestamp fields to subdocuments in an array

I recently updated my subschemas (called Courses) to have timestamps and am trying to backfill existing documents to include createdAt/updatedAt fields.
Courses are stored in an array called courses in the user document.
// User document example
{
name: "Joe John",
age: 20,
courses: [
{
_id: <id here>,
name: "Intro to Geography",
units: 4
} // Trying to add timestamps to each course
]
}
I would also like to derive the createdAt field from the Course's Mongo ID.
This is the code I'm using to attempt adding the timestamps to the subdocuments:
db.collection('user').updateMany(
{
'courses.0': { $exists: true },
},
{
$set: {
'courses.$[elem].createdAt': { $toDate: 'courses.$[elem]._id' },
},
},
{ arrayFilters: [{ 'elem.createdAt': { $exists: false } }] }
);
However, after running the code, no fields are added to the Course subdocuments.
I'm using mongo ^4.1.1 and mongoose ^6.0.6.
Any help would be appreciated!
Using aggregation operators and referencing the value of another field in an update statement requires using the pipeline form of update, which is not available until MongoDB 4.2.
Once you upgrade, you could use an update like this:
db.collection.updateMany({
"courses": {$elemMatch: {
_id:{$exists:true},
createdAt: {$exists: false}
}}
},
[{$set: {
"courses": {
$map: {
input: "$courses",
in: {
$mergeObjects: [
{createdAt: {
$convert: {
input: "$$this._id",
to: "date",
onError: {"error": "$$this._id"}
}
}},
"$$this"
]
}
}
}
}
}
])

$elemMatch doesn't work on nested documents in MongoDB

Stack Overflow!
I have a very strange problem with using $elemMatch in MongoDB. I added multiple documents to a collection. Some of these documents were added using import feature in MongoDB Compass (Add Data -> Import File -> JSON) and some of them were added using insertMany().
Here is an example structure of a single document:
{
"id": "1234567890",
"date": "YYYY-MM-DD",
"contents": {
"0": {
"content": {
"id": "1111111111",
"name": "Name 1"
}
},
"1": {
"content": {
"id": "2222222222",
"name": "Name 2"
}
},
"2": {
"content": {
"id": "3333333333",
"name": "Name 3"
}
}
}
}
The thing is, when I use the following find query using this filter:
{date: "<some_date_here>", "contents": {
$elemMatch: {
"content.id": <some_id_here>
}
}}
ONLY documents that were imported from MongoDB Compass are showing up. Documents that were added by Mongosh or by NodeJS driver (doesn't matter), do NOT show up.
Am I missing something obvious here? What should I do in order to make all documents in a collection (that matches filter) to show up?
Simple filters that do not include $elemMatch work well and all documents that match the filtering rules show up. Problem seems to be with $elemMatch.
I tried adding the same batch of documents using different methods but only direct importing a JSON file in MongoDB Compass make them appear using a filter mentioned above.
Thank you for your help!
$elemMatch if for matching array , and in this case you don't have array
first you should convert contents object to array and then check the query for example id with filter and use match to find all doc that have specific data and size of new filters array
db.collection.aggregate([
{
"$addFields": {
"newField": {
"$objectToArray": "$contents"
}
}
},
{
"$addFields": {
"newField": {
"$filter": {
"input": "$newField",
"as": "z",
"cond": {
$eq: [
"$$z.v.content.id",
"1111111111"
]
}
}
}
}
},
{
"$addFields": {
"newField": {
$size: "$newField"
}
}
},
{
$match: {$and:[ {newField: {
$gt: 0
}},{date:{$gt:Date}}]}
},
{$project:{
contents:1,
date:1,
id:1,
}}
])
https://mongoplayground.net/p/pue4QPp1dYR
in mongoplayground I don't add filter of date

How can I present fields with similar name on mongo shell?

I have a collection name 'persons'. Inside the collection, I have the following structure. I would like to get to present all the fields that contain the term "friend".
name: 'David',
contacts: {
friend_1: 'Bill'
friend_2: 'George'
friend_3: 'Donald'
friend_4: 'Richard'
managaer: 'James'
mentor: 'Andy'
}
I thought to try maybe db.persons.find({}, {/.*friend.*/: 1}), but obviously it didn't work.
How can I achieve it?
It doesn't seem to be a correct structure. If this is the case, maybe you can store an array in 'friends' field.
name: 'David',
contacts: {
friends: ['Bill', 'George', 'Donald', 'Richard'],
manager: 'James',
mentor: 'Andy'
}
First of all, obviously this is not the best structure, but is what it is, and then you can use this aggregation query:
First set the object as array with $objectToArray to filter by the regex.
Then use $filter with $regexMatch to get only values you want into the array.
And recreate the object again using $arrayToObject.
db.collection.aggregate([
{
"$set": {
"contacts": {
"$objectToArray": "$contacts"
}
}
},
{
"$project": {
"name": 1,
"contacts": {
"$filter": {
"input": "$contacts",
"as": "c",
"cond": {
"$regexMatch": {
"input": "$$c.k",
"regex": ".*friend.*",
"options": "i"
}
}
}
}
}
},
{
"$set": {
"contacts": {
"$arrayToObject": "$contacts"
}
}
}
])
Example here