MongoDB aggregation - replace field in collection from value in another collection - mongodb

so i've got a collection like so
_id: "a6c67aad-e90c-4a13-aae0-74e5ca5c8632"
value : true
and one like this
_id: "a6c67aad-e90c-4a13-aae0-74e5ca5c8632"
otherValue : false
How can i use aggregation pipelines to update the second collection otherValue with the value from the first collection based on _id
I've tried using lookup and then unwind like
{
from: 'col1',
localField: 'otherValue',
foreignField: 'value',
as: 'data'
}
and then unwind
{
path: '$val'
}
But im not quite sure where to go from here, any help would be greatly appreciated.

Try this:
db.collection1.aggregate([
{
$lookup: {
from: "collection2",
let: { c1_id: "$_id", value: "$value" },
pipeline: [
{
$match: {
$expr: { $eq: ["$_id", "$$c1_id"] }
}
},
{
$addFields: { otherValue: "$$value" }
}
],
as: "data"
}
},
{
$unwind: "$data"
}
])
Output:
{
"_id" : "a6c67aad-e90c-4a13-aae0-74e5ca5c8632",
"value" : true,
"data" : {
"_id" : "a6c67aad-e90c-4a13-aae0-74e5ca5c8632",
"otherValue" : true
}
}
Where collection1 is:
{
"_id" : "a6c67aad-e90c-4a13-aae0-74e5ca5c8632",
"value" : true
}
Where collection2 is:
{
"_id" : "a6c67aad-e90c-4a13-aae0-74e5ca5c8632",
"otherValue" : false
}

You might use the $merge aggregation stage.
match the documents from the source collection that you want to use to update the second collect.
lookup the matching document from the second collection
unwind so it is a single document instead of an array (this stage also eliminates documents that don't match one from the second collection)
addFields to store the value from the first document into the looked up document
replaceRoot to the modified looked-up document
merge the modified documents with the original collection, matching on _id
db.collection.aggregate([
{$match: { filter to pick the documents }},
{$lookup: {
from: "otherCollection"
localField: "_id",
foreignField: "_id",
as: "otherDocument"
}},
{$unwind: "$otherDocument"},
{$addFields:{
"otherDocument.otherValue": "$value"
}},
{$replaceRoot: {newRoot: "$otherDocument"}},
{$merge: {
into: "otherCollection",
on: "_id",
whenMatched: "replace",
whenNotMatched: "insert"
}}
])

Related

Array is reordered when using $lookup

I have this aggregation:
db.getCollection("users").aggregate([
{
"$match": {
"_id": "5a708a38e6a4078bd49f01d5"
}
},
{
"$lookup": {
"from": "user-locations",
"localField": "locations",
"as": "locations",
"foreignField": "_id"
}
}
])
It works well, but there is one small thing that I don't understand and I can't fix.
In the query output, the locations array is reordered by ObjectId and I really need to keep the original order of data.
Here is how the locations array from the users collection looks like
'locations' : [
ObjectId("5b55e9820b720a1a7cd19633"),
ObjectId("5a708a38e6a4078bd49ef13f")
],
And here is the result after the aggregation:
'locations' : [
{
'_id' : ObjectId("5a708a38e6a4078bd49ef13f"),
'name': 'Location 2'
},
{
'_id' : ObjectId("5b55e9820b720a1a7cd19633"),
'name': 'Location 1'
}
],
What am I missing here? I really have no idea how to proceed with this issue.
Could you give me a push?
$lookup does not guarantee order of result documents, you can try a approach to manage natural order of document,
$unwind deconstruct locations array and add auto index number will start from 0,
$lookup with locations
$set to select first element from locations
$sort by index field in ascending order
$group by _id and reconstruct locations array
db.users.aggregate([
{ $match: { _id: "5a708a38e6a4078bd49f01d5" } },
{
$unwind: {
path: "$locations",
includeArrayIndex: "index"
}
},
{
$lookup: {
from: "user-locations",
localField: "locations",
foreignField: "_id",
as: "locations"
}
},
{ $set: { locations: { $arrayElemAt: ["$locations", 0] } } },
{ $sort: { index: 1 } },
{
$group: {
_id: "$_id",
locations: { $push: "$locations" }
}
}
])
Playground
From this closed bug report:
When using $lookup, the order of the documents returned is not guaranteed. The documents are returned in "natural order" - as they are encountered in the database. The only way to get a guaranteed consistent order is to add a $sort stage to the query.
Basically the way any Mongo query/pipeline works is that it returns documents in the order they were matched, meaning the "right" order is not guaranteed especially if there's indes usage involved.
What you should do is add a $sort stage as suggested, like so:
db.collection.aggregate([
{
"$match": {
"_id": "5a708a38e6a4078bd49f01d5"
}
},
{
"$lookup": {
"from": "user-locations",
"let": {
"locations": "$locations"
},
"pipeline": [
{
"$match": {
"$expr": {
"$setIsSubset": [
[
"$_id"
],
"$$locations"
]
}
}
},
{
$sort: {
_id: 1 // any other sort field you want.
}
}
],
"as": "locations",
}
}
])
You can also keep the original $lookup syntax you're using and just $unwind, $sort and then $group to restore the structure.

MongoDB $lookup if the local field exists

I have these entities:
// collectionA
{
key: "value",
ref: SOME-OBJECT-ID
}
// collectionB
{
_id: SOME-OBJECT-ID
key1: "value1"
}
I want that if ref exists in the collectionA entity, it will lookup for it on the collectionB and bring its data.
If the ref key is missing or it doesn't missing but the entity in collectionB is missing I get empty result from all of the aggregate query.
This is the aggregate query:
{ $match },
{
$lookup: {
from: "collectionB",
let: {
ref: "$ref"
},
pipeline: [
{
$match: {
$expr: {
$eq: [
"$_id", "$$ref"
]
}
}
},
{
$project: {
key1: 1
}
}
],
as: "someData"
}
}
How can I avoid this or add any conditional $lookup?
One way of doing that is adding another match at the beginning to skip from source
To skip from B, you can omit at the end.
{$match:{ ref:{$exists:true}}}
It will consider only ref existing docs.
play
db.A.aggregate([
{
"$match": {
ref: {
$exists: true
}
}
},
{
"$lookup": {
"from": "B",
"localField": "ref",
"foreignField": "_id",
"as": "output"
}
}
])
But you don't need to do this if you don't have specific use case, as it will not impact much.
I have found it. The document was not selected because I have used the $unwind - and it won't return the document if we are trying to do it on an empty array. So this is the fix:
{
$unwind: {
path: "$ref",
preserveNullAndEmptyArrays: true
}
}
Instead of:
{
$unwind: "$ref"
}
I found the preserveNullAndEmptyArrays from this answer How to get all result if unwind field does not exist in mongodb

The Foreign field of $lookup could be the field of nested document?

$lookup is used to perform a left outer join to an unsharded collection in the same database to filter in documents from the “joined” collection for processing in Mongo.
{
$lookup:
{
from: <collection to join>,
localField: <field from the input documents>,
foreignField: <field from the documents of the "from" collection>,
as: <output array field>
}
}
Could the foreignField be the field of the nested document of from collection?
For example, there are two collections as following.
history collection
[{
id:'001',
history:'today worked',
child_id:'ch001'
},{
id:'002',
history:'working',
child_id:'ch004'
},{
id:'003',
history:'now working'
child_id:'ch009'
}],
childsgroup collection
[{
groupid:'g001', name:'group1'
childs:[{
id:'ch001',
info:{name:'a'}
},{
id:'ch002',
info:{name:'a'}
}]
},{
groupid:'g002', name:'group1'
childs:[{
id:'ch004',
info:{name:'a'}
},{
id:'ch009',
info:{name:'a'}
}]
}]
so, this aggregation code could be executed like this?
db.history.aggregate([
{
$lookup:
{
from: "childsgroup",
localField: "child_id",
foreignField: "childs.$.id",
as: "childinfo"
}
}
])
So I want to get the result like this.
[{
id:'001',
history:'today worked',
child_id:'ch001',
childinfo:{
id:'001',
history:'today worked',
child_id:'ch001'
}
}, .... ]
Isn't this possible?
There's no positional operator for $lookup but you can use custom pipeline in MongoDB 3.6 to define custom join conditions:
db.history.aggregate([
{
$lookup: {
from: "childsgroup",
let: { child_id: "$child_id" },
pipeline: [
{ $match: { $expr: { $in: [ "$$child_id", "$childs.id" ] } } },
{ $unwind: "$childs" },
{ $match: { $expr: { $eq: [ "$childs.id", "$$child_id" ] } } },
{ $replaceRoot: { newRoot: "$childs" } }
],
as: "childInfo"
}
}
])
First $match added to improve performance: we want to find only those documents from childsgroup that contain matching child_id and then we can match subdocuments after $unwind stage.

Perform $lookup to array of object id's in mongodb?

I have got the two collection named
BlogCategory and
SubBlogCategory .The document of the BlogCategory consists the array of objectId's of subcategory id.
The category document is as follows
{
"_id" : ObjectId("5af2c6e8bfab7269e9a42ded"),
"title" : "Javascript",
"subcategory" : [
ObjectId("5af29fcc9a52623b7088ef4e"),
ObjectId("5aebf78681273424e5f55ecc")
]
}
The subcategory document is as follows
{
"_id" : ObjectId("5af29fcc9a52623b7088ef4e"),
"title" : "Reactjs"
}
{
"_id" : ObjectId("5aebf78681273424e5f55ecc"),
"title" : "Vuejs",
}
How can I populate the subcategory documents on the category documents while getting all the documents of the collection category?
I have used the following query but its only populating the single id of the array
db.BlogCategory.aggregate([
{
$unwind: "$subcategory"
},
{
$lookup:
{
from: "SubBlogCategory",
localField: "subcategory",
foreignField: "_id",
as: "sub_doc"
}
}
])
No need to $unwind here... $unwind duplicates each document in the pipeline, once per array element.
db.BlogCategory.aggregate([
{
$lookup:
{
from: *Collection_Name*,
localField: "subcategory",
foreignField: "_id",
as: "sub_doc"
}
}
])
if you are using mongodb version 3.6 then you can use pipeline
db.BlogCategory.aggregate([
{ "$lookup": {
"from": *Collection_Name*,
"let": { "subcategory": "$subcategory" },
"pipeline": [
{ "$match": { "$expr": { "$in": [ "$_id", "$$subcategory" ] } } }
],
"as": "sub_doc"
}}
])

Use array first field in mongo aggregate $lookup query to match a document

I want to use my array field 0th value to find a match in sale document using Mongo aggregate $lookup query. Here is my query:
db.products.aggregate([
{
$match : { _id:ObjectId("57c6957fb190ecc02e8b456b") }
},
{
$lookup : {
from : 'sale',
localField: 'categories.0',
foreignField: 'saleCategoryId',
as : 'pcSales'
}
}]);
Result :
{
"_id" : ObjectId("57c6957fb190ecc02e8b456b"),
"categories" : [
"57c54f0db190ec430d8b4571"
],
"pcSales" : [
{
"_id" : ObjectId("57c7df5f30fb6eacb3810d1b"),
"Title" : "Latest Arrivals",
}
]}
This query will return me a match but when i check it not a match. I don't get why is this happening, And when i removed 0th part from query its return blank array.
Like this:
{
"_id" : ObjectId("57c6957fb190ecc02e8b456b"),
"categories" : [
"57c54f0db190ec430d8b4571"
],
"pcSales" : []
}
saleCategoryId is also a array field which contain array of categoriesKey.
Please help.
Because your localField is an array, you'll need to add an $unwind stage to your pipeline before the lookup or use the $arrayElemAt in a $project pipeline step to get the actual element in the array.
Here are two examples, one which uses the $arrayElemAt operator:
db.products.aggregate([
{ "$match" : { "_id": ObjectId("57c6957fb190ecc02e8b456b") } },
{
"$project": {
"category": { "$arrayElemAt": [ "$categories", 0 ] }
}
},
{
"$lookup": {
from : 'sale',
localField: 'category',
foreignField: 'saleCategoryId',
as : 'pcSales'
}
}
]);
and this which uses $unwind to flatten the categories array first before applying the $lookup pipeline:
db.products.aggregate([
{ "$match" : { "_id": ObjectId("57c6957fb190ecc02e8b456b") } },
{ "$unwind": "$categories" },
{
"$lookup": {
from : 'sale',
localField: 'categories',
foreignField: 'saleCategoryId',
as : 'pcSales'
}
}
]);