MongoDB aggregate merging fields - mongodb

I have a mongo Database I'll like to "join" two of them and then merge some other fields:
Let's see the schemas:
Students Schema (and data):
{
"_id": ObjectId("5fbd564981b1313de790b580"),
"name": "John Doe",
"age": "21",
"image": "https://XXXX/481.png",
"subjects": [
{
"_id": ObjectId("5fbd4e6881b1313de790b56b"),
"passed": true,
},
{
"_id": ObjectId("5fcb63fa8814d96876c687bf"),
}
],
"__v": NumberInt("1"),
}
and Subject schema:
{
"_id": ObjectId("5fbd4e6881b1313de790b56b"),
"course": 3,
"teacher": "John Smith",
"name": "Math",
},
{
"_id": ObjectId("5fcb63fa8814d96876c687bf"),
"name": "IT",
"course": 8,
"teacher": "John Peter",
}
What I'll like to make a query with the subjects (all info) of a student, also if the student have additional fields in subject like passed add it to the subject subdocument.
Here is my query till now:
db.students.aggregate([
{
$match:
{
_id : ObjectId('5fbd564981b1313de790b580')
}
},
{
$lookup :
{
from : "subjects",
localField : "subjects._id",
foreignField : "_id",
as : "FoundSubject"
}
}
]);
which correctly make the "join" but the merge is still missing, I got as result:
{
"_id": ObjectId("5fbd564981b1313de790b580"),
"name": "John Doe",
"age": "21",
"image": "https://XXXX/481.png",
"subjects": [
{
"_id": ObjectId("5fbd4e6881b1313de790b56b"),
"passed": true,
},
{
"_id": ObjectId("5fcb63fa8814d96876c687bf"),
}
],
"__v": NumberInt("1"),
"FoundSubject": [
{
"_id": ObjectId("5fbd4e6881b1313de790b56b"),
"course": 3,
"teacher": "John Smith",
"name": "Math"
},
{
"_id": ObjectId("5fcb63fa8814d96876c687bf"),
"name": "IT",
"course": 8,
"teacher": "John Peter"
}
]
}
but I'll like to have:
{
"_id": ObjectId("5fbd564981b1313de790b580"),
"name": "John Doe",
"age": "21",
"image": "https://XXXX/481.png",
"subjects": [
{
"_id": ObjectId("5fbd4e6881b1313de790b56b"),
"course": 3,
"teacher": "John Smith",
"name": "Math",
"passed": true,
},
{
"_id": ObjectId("5fcb63fa8814d96876c687bf"),
"name": "IT",
"course": 8,
"teacher": "John Peter"
}
],
"__v": NumberInt("1"),
}
with merged data and field "passed" added. How can accomplish that?
I'm new to MongoDB coming from MySQL.
Thanks

You need to merge both objects, add below stage after $lookup,
MongoDB Version From 3.4
$map to iterate loop of students array
$reduce to iterate loop of FoundSubject array, check condition if condition match then return required fields otherwise return initial value
$project to remove FoundSubject from result
{
$addFields: {
subjects: {
$map: {
input: "$subjects",
as: "s",
in: {
$reduce: {
input: "$FoundSubject",
initialValue: {},
in: {
$cond: [
{ $eq: ["$$s._id", "$$this._id"] },
{
_id: "$$this._id",
course: "$$this.course",
name: "$$this.name",
teacher: "$$this.teacher",
passed: "$$s.passed"
},
"$$value"
]
}
}
}
}
}
}
},
{ $project: { FoundSubject: 0 } }
Playground
MongoDB Version From 4.4
$map to iterate loop of students array,
$filter to get matching document from FoundSubject array and $first to get first object from array returned by filter
$mergeObjects to merge current objects with found result object from filter
remove FoundSubject using $$REMOVE
// skipping your stages
{
$addFields: {
FoundSubject: "$$REMOVE",
subjects: {
$map: {
input: "$subjects",
as: "s",
in: {
$mergeObjects: [
"$$s",
{
$first: {
$filter: {
input: "$FoundSubject",
cond: { $eq: ["$$s._id", "$$this._id"] }
}
}
}
]
}
}
}
}
}
Playground

Related

MongoDB - For each group select the records with the max value

In MongoDB, I'm trying to filter a collection down to only those documents that contain the most recent date by their respective group.
In traditional SQL I'd do something like:
Select *
From table a
Join (Select my_group, max(date) as max_date
From table group by my_group) b
ON a.my_group = b.my_group AND
a.date = b.max_date
With the following sample collection:
[
{
"_id": "123",
"item1": "group 1",
"item2": "abc",
"item3": "abc",
"date": "2022-01-01"
},
{
"_id": "234",
"item1": "group 1",
"item2": "abc",
"item3": "abc",
"date": "2022-01-02"
},
{
"_id": "345",
"item1": "group 1",
"item2": "abc",
"item3": "abc",
"date": "2022-01-02"
},
{
"_id": "789",
"item1": "group 2",
"item2": "abc",
"item3": "abc",
"date": "2022-01-01"
},
{
"_id": "678",
"item1": "group 2",
"item2": "abc",
"item3": "abc",
"date": "2022-01-02"
},
{
"_id": "456",
"item1": "group 2",
"item2": "abc",
"item3": "abc",
"date": "2022-01-02"
}
]
The expected output is:
[
{
"_id": "234",
"date": "2022-01-02",
"item1": "group 1",
"item2": "abc",
"item3": "abc"
},
{
"_id": "345",
"date": "2022-01-02",
"item1": "group 1",
"item2": "abc",
"item3": "abc"
},
{
"_id": "678",
"date": "2022-01-02",
"item1": "group 2",
"item2": "abc",
"item3": "abc"
},
{
"_id": "456",
"date": "2022-01-02",
"item1": "group 2",
"item2": "abc",
"item3": "abc"
}
]
My current best attempt is:
db.collection.aggregate([
{
$group: {
"_id": "$item1",
"max_date": {
$max: "$date"
},
"records": {
$push: "$$ROOT"
}
}
},
{
"$project": {
items: {
"$filter": {
"input": "$records",
"as": "records",
"cond": {
$eq: [
"$$records.date",
"$max_date"
]
}
}
}
}
},
{
$replaceRoot: {
newRoot: {
results: "$items"
}
}
}
])
Unfortunately, this returns the results partitioned by group. I've tried a few alternatives suggested by other posts & get a similar problem, eg:
How to group and select document corresponding to max within each group in MongoDB?
MongoDB get rows where max value grouped
Get all rows, groupped and with max value
Here's a playground example with the query & sample data.
You're close to the answer.
For the last 2 stages:
$unwind - Deconstruct the items array field to multiple documents.
$replaceWith - Replace the output document with items document.
db.collection.aggregate([
{
$group: {
"_id": "$item1",
"max_date": {
$max: "$date"
},
"records": {
$push: "$$ROOT"
}
}
},
{
"$project": {
items: {
"$filter": {
"input": "$records",
"as": "records",
"cond": {
$eq: [
"$$records.date",
"$max_date"
]
}
}
}
}
},
{
$unwind: "$items"
},
{
$replaceWith: "$items"
}
])
Sample Mongo Playground
Bonus
Although the query above is better, also would like to share the MongoDB query that is similar to SQL implementation.
$group - Group by item1 and get the max value of date.
$lookup - Self join the collection with item1 and date. And returns items array field.
$match - Filter the document with items not an empty array.
$unwind - Deconstruct the items array into multiple documents.
$replaceWith - Replace the output document with items document.
db.collection.aggregate([
{
$group: {
"_id": "$item1",
"max_date": {
$max: "$date"
}
}
},
{
$lookup: {
from: "collection",
let: {
item1: "$_id",
max_date: "$max_date"
},
pipeline: [
{
$match: {
$expr: {
$and: [
{
$eq: [
"$item1",
"$$item1"
]
},
{
$eq: [
"$date",
"$$max_date"
]
}
]
}
}
}
],
as: "items"
}
},
{
$match: {
items: {
$ne: []
}
}
},
{
$unwind: "$items"
},
{
$replaceWith: "$items"
}
])
Sample Mongo Playground (Bonus)

How to project nested array elements with custom field name in MongoDB

I have a users collection in MongoDB with this sample row:
[
{
"_id": 1,
"name": "John Doe",
"comments": [
{
"_id": 100,
"content": "Comment 100",
"post": "1000"
},
{
"_id": 101,
"content": "Comment 101",
"post": "1000"
}
]
}
]
I want to project users and comments data after converting _id fields into id. So I used the following query:
db.users.aggregate([
{
$project: {
_id: 0,
id: '$_id',
name:1
comments: {
_id: 0,
id: '$_id',
content: 1,
},
}
}
])
Now the users _id field is successfully converted. But the comments id field is equal to _id of users collection instead of comments.
[
{
"id": 1,
"name": "john Doe",
"comments": [
{
"id": 1,
"content": "comment 100"
},
{
"id": 1,
"content": "comment 101"
},
]
}
]
How can I achieve the right result.
you should use $map in project like this
https://mongoplayground.net/p/H4pueuejYdf
db.collection.aggregate([
{
"$project": {
_id: 0,
id: "$_id",
name: 1,
comments: {
"$map": {
"input": "$comments",
"as": "c",
"in": {
id: "$$c._id",
content: "$$c.content",
post: "$$c.post"
}
}
}
}
}
])

MongoDB: Filter an array by _id using aggregate

I need some help to filter the field Internals array to only show the item with the same _id of this field profile_id using aggregate, but I realy cant get this.
I try use filter, lookup and search in many posts.
I want using aggregate, to get the documents with filtered array of users based on the objectID of the profile.
Every profile id has only one internal id
Each user has only one INTERNAL.
Mongo PLayGround Query
PRODUCTS TABLE
[
{
"_id": {"$oid": "5dd5a66e8ddbd07fa251b88e"},
"name": "Product One",
"internals": [
{
"default": false,
"_id": {"$oid": "5e94da0f4d499253049ec532"},
"name": "Type A"
},
{
"_id": {"$oid": "5e94da0f4d499253049ec534"},
"name": "Type B"
},
{
"_id": {"$oid": "5e94da0f4d499253049ec536"},
"name": "Type C"
},
{
"_id": {"$oid": "5e94da0f4d499253049ec538"},
"name": "Type D"
},
{
"_id": {"$oid": "5f05d9a2bfc8fa1361d8beea"},
"name": "Type E"
},
{
"_id": {"$oid": "5f05d9a2bfc8fa1361d8beec"},
"name": "Type F"
}
]
},
{
"_id": {"$oid": "5e8b40bc91dd0e12e0219578"},
"name": "Product Two",
"internals": [
{
"_id": {"$oid": "5e8b40bc91dd0e12e0219579"},
"name": "Default Type"
}
],
}
]
USER TABLE
[
{
"_id": {"$oid": "603d4c44505a51001c9b936f"},
"email": "mister.paper#papers.org",
"name": "Mister Paper",
"profile": {
"products": [
{
"_id": {"$oid": "603d4d101c24a9001cf79db1"},
"product": {"$oid": "5dd5a66e8ddbd07fa251b88e"},
"profile": {"$oid": "5e94da0f4d499253049ec534"}
},
{
"_id": {"$oid": "603fe3ebca063ea357ca0666"},
"product": {"$oid": "5e8b40bc91dd0e12e0219578"},
"profile": {"$oid": "5e8b40bc91dd0e12e0219579"}
},
{
"_id": {"$oid": "603fe3ebca063ea357ca0667"},
"product": {"$oid": "5dd5a66e8ddbd07fa251b88e"},
"profile": {"$oid": "5e94da0f4d499253049ec538"}
}
],
},
"username": "mister.paper#papers.org"
}
]
That is my query at this moment after try many others :
QUERY
db.getCollection('users').aggregate([
{ $match: { email: { $in: [ /mister.paper/i ] }} }, { $sort: { created_at: -1 } },
{$unwind:"$profile"},
{$unwind:"$profile.products"},
{$unwind:"$profile.products.profile"},
{'$lookup': { from: 'products', localField: 'profile.products.product', foreignField: '_id', as: 'products' } },
{$unwind:"$products"},
//{$unwind:"$products"},
//{$unwind:"$products.internals"},
{$addFields: { "products.profile_id": '$profile.products.profile'}},
{'$lookup': { from: 'products.internals', localField: 'products.profile_id', foreignField: '_id', as: 'internalsArray' } },
{
'$project': {
name: 1,
products: {
_id: "$products._id",
name: "$products.name",
profile_id : 1,
},
productsId: 1,
"internals": { _id: "$products.internals._id" , name: "$products.internals.name" },
internalsArray : 1,
}
}
], {});
I used $filter to filter out products internal array by matching internal._id and profile.products.profile. You can extend this query to further shape your data in desired format.
Here's the core pipeline that performs filtration. I used it after your {$unwind: "$products"} stage.
{
"$addFields": {
"products.internals": {
$filter: {
input: "$products.internals",
as: "internal",
cond: {
$eq: [
"$$internal._id",
"$profile.products.profile"
]
},
}
}
}
}
Mongo Playground:
https://mongoplayground.net/p/zPxE-H2mYc7
A suggestion after a brief look on your sample data is your first unwind {$unwind:"$profile"} and {$unwind:"$profile.products.profile"} doesn't make sense since profile is an object in first unwind and ObjectId in second unwind so you can remove them.

Select data from two MongoDB tables and update the results

We have the following problem
Given are the tables and fields
Offer
OfferId
State
Article
OfferId
ArticleId
NetPrice
GrossPrice
VatRate
Example-data:
Offer-Collection
{
"_id": "1",
"State": "INITIAL",
"_class": "com.example.dto.OfferData"
}
{
"_id": "2",
"State": "COMPLETED",
"_class": "com.example.dto.OfferData"
}
Article-Collection
{
"_id": {
"$oid": "a"
},
"Description": "asdf",
"NetPrice": "100",
"GrossPrice": "116",
"VatRate": "16",
"OfferId": "1",
"_class": "com.example.dto.Article"
}
{
"_id": {
"$oid": "b"
},
"Description": "my description",
"NetPrice": "100",
"GrossPrice": "119",
"VatRate": "19",
"OfferId": "1",
"_class": "com.example.dto.Article"
}
{
"_id": {
"$oid": "c"
},
"Description": "my description",
"NetPrice": "100",
"GrossPrice": "116",
"VatRate": "16",
"OfferId": "2",
"_class": "com.example.dto.Article"
}
Now we have to update all articles belonging to an offer with the state "initial" in the following way: if the VatRate is equal to 16 than it must be updated to 19 AND the GrossPrice must be recalculated from the existing NetPrice.
The result should be: the article with _id = "a" and VatRate = 16 for OfferId = 1 (State = INITIAL) should have VatRate = 19 and GrossPrice = 119. The fields should be updated and persisted in the original MongoDB collection.
Can we do this only with Mongo-shell? Our Version is 3.6.
Our tries:
We have played around with .aggregate, $lookup, $match and $project but without much luck. It's the first time we are using the Mongo-shell.
db.getCollection("Offers").aggregate([{
$lookup:{
from:"Articles",
localField:"OfferId",
foreignField:"OfferId",
as:"selected-articles"
}
},
{
$match: { "state": { "$eq": "INITIAL" } }
},
{
$project: { "articles": 1 }
}
]).forEach(...?)
$match your State condition
$lookup with Articles collection
$map to iterate loop of selected-articles array, check condition using $cond if VatRate is "16" then updated to 19 and recalculate GrossPrice as per NetPrice using $multiply before it convert NetPrice to integer because its in string type, back to merge objects with current objects using $mergeObjects
db.getCollection("Offers").aggregate([
{ $match: { State: { $eq: "INITIAL" } } },
{
$lookup: {
from: "Articles",
localField: "_id",
foreignField: "OfferId",
as: "selected-articles"
}
},
{
$addFields: {
"selected-articles": {
$map: {
input: "$selected-articles",
in: {
$mergeObjects: [
"$$this",
{
$cond: [
{ $eq: ["$$this.VatRate", "16"] },
{
VatRate: 19,
GrossPrice: {
$multiply: [{ $toInt: "$$this.NetPrice" }, 19]
}
},
{}
]
}
]
}
}
}
}
}
])
Playground

How to match each array field to other field in Mongodb

I have an array as below:
const test = [{
"_id": 1,
"name": "apple",
"car": "ford"
},{
"_id": 2,
"name": "melon",
"car": "ferrari"
},{
"_id": 3,
"name": "perl",
"car": "Renaut"
}]
And there is are documents of Mongodb as below:
[{
"name": "perl", "company": "A"
},{
"name": "melon", "company": "B"
},{
"name": "apple", "company": "C"
},{
"name": "apple", "company": "D"
},{
"name": "perl", "company": "E"
},{
"name": "apple", "company": "F"
}]
And I want to get this result using mongodb aggregate:
[{
"name": "perl", "company": "A", testInform: { "_id": 3, "name": "perl", "car": "Renaut"}
},{
"name": "melon", "company": "B", testInform: { "_id": 2, "name": "melon", "car": "ferrari"}
},{
"name": "apple", "company": "C", testInform: { "_id": 1, "name": "apple", "car": "ford"}
},{
"name": "apple", "company": "D", testInform: { "_id": 1, "name": "apple", "car": "ford"}
},{
"name": "perl", "company": "E", testInform: { "_id": 3, "name": "perl", "car": "Renaut"}
},{
"name": "apple", "company": "F", testInform: { "_id": 1, "name": "apple", "car": "ford"}
}]
I think to use aggregate with $match and $facet, etc., but I don't know exactly how to do this. Could you recommend a solution for this?
Thank you so much for reading this.
$lookup with pipeline keyword
db.demo2.aggregate(
{
$lookup:
{
from: "demo1",
let: { recordName: "$name"},
pipeline: [
{ $match:
{ $expr:
{ $and:
[
{ $eq: [ "$$recordName", "$name" ] },
]
}
}
},
],
as: "testInform"
}
}
)
If the test array data is stored in a collection then acheiving O/P is pretty straightforward $lookup with $project aggregation
$arrayElemAt Why? because the lookup would fetch the joined documents in an array as testInform
db.maindocs.aggregate([
{
$lookup: {
from: "testdocs",
localField: "name",
foreignField: "name",
as: "testInform"
}
},
{
$project: {
_id: 0,
name: 1,
company: 1,
testInform: { $arrayElemAt: ["$testInform", 0] }
}
}
]);
Update based on comments:
The idea is to iterate the cursor from the documents stored in mongodb Array.prototype.find() the object from test which matches the name field, add it to result.
const test = [
{
_id: 1,
name: "apple",
car: "ford"
},
{
_id: 2,
name: "melon",
car: "ferrari"
},
{
_id: 3,
name: "perl",
car: "Renaut"
}
];
const cursor = db.collection("maindocs").find();
const result = [];
while (await cursor.hasNext()) {
const doc = await cursor.next();
const found = test.find(e => e.name === doc.name);
if (found) {
doc["testInform"] = found;
}
result.push(doc);
}
console.info("RESULT::", result);
The aggregation has one stage: Iterate over the test array and get the array element as an object which matches the name field in both the document and the array (using the $reduce operator).
const test = [ { ... }, ... ]
db.test_coll.aggregate( [
{
$addFields: {
testInform: {
$reduce: {
input: test,
initialValue: { },
in: {
$cond: [ { $eq: [ "$$this.name", "$name" ] },
{ $mergeObjects: [ "$$this", "$$value" ] },
"$$value"
]
}
}
}
}
}
] )