Mongo lookup through array of ids - mongodb

how to correct lookup collection product which has array of ids of prices and need execute query exactly from prices collection query. Need to show price record with lookup product record
so, I have prices record
{
"_id" : "813f02ff-882e-44f7-b2bc-2f067427daf6",
"unit_amount" : 333,
"currency" : "USD",
"interval" : "year",
"active" : true
}
and product
"_id" : "3c46f277-8953-4f96-baf1-bd871ee3301f",
"name" : "test",
"prices" : [
"813f02ff-882e-44f7-b2bc-2f067427daf6",
"f5c76122-6132-4e4b-a26b-41bbd6325acc",
"3e4be68e-fbed-47f7-b871-92de72cb00df"
]
and my query, I thought it should be like that
db.getCollection('price').aggregate([
{
"$lookup": {
"from": "product",
"let": { "prid": "$_id" },
"pipeline": [
{ "$match": { "$expr": { "$in": ["$$prid", '$prices'] } } }
],
"as": "product_tbl"
}
}
])
faced with
{
"ok" : 0,
"errmsg" : "PlanExecutor error during aggregation :: caused by :: $in requires an array as a second argument, found: missing",
"code" : 40081,
"codeName" : "Location40081"
}
but it's not works. How it shoul be look ?

Seems like some of the documents in your product collection are missing prices
key. You can try this:
db.prices.aggregate([
{
"$lookup": {
"from": "product",
"let": {
"prid": "$_id"
},
"pipeline": [
{
"$addFields": {
"prices": {
"$ifNull": [
"$prices",
[]
]
}
}
},
{
"$match": {
"$expr": {
"$in": [
"$$prid",
"$prices"
]
}
}
}
],
"as": "product_tbl"
}
}
])
Here, we recompute the prices and set it to empty array, if it's missing, before the $match. Playground link.

Related

How to get Child data after filtering dataset of Parents in a single mongo collection?

I have a single mongo collection called "CourseCollection" and it contains both parent and child doc. Any document with the key "Parent" is a child doc and a parent can have multiple child doc.
{
"_id" : "abracadavra",
"Name" : "abracadavra",
"Description" : "",
"Type" : "Spell",
"Parent" : {
"_id" : "Magic",
"Type" : "Course",
"Name" : "Magic"
}
},
{
"_id" : "Magic",
"Name" : "Magic",
"Type" : "Course",
"Access" : [
{
"_id" : "2sssdw5oe",
"Name" : "Abc"
},
{
"_id" : "4fddfye42",
"Name" : "Xyz"
}
]
}
What I'm trying to do is, based on the Access of Parent doc, I'm trying to get all the child doc.
Existing and working solution:
The solution that I have currently is to perform 2 queries.
Query 1. Get all the courses that the user has access to.
db.getCollection("CourseCollection").find({"Type": "Course", "Access._id": {"$in": ["2sssdw5oe"]}})
Query 2. Since I'm using Python, I do a list comprehension to get only the IDs of the course and then perform another query with this list
db.getCollection("CourseCollection").find({"Type": "Spell", "Parent._id": {"$in": course_list_id}})
Is there a way to get the child data after filtering out the parent in a single query. I also tried aggregation but only the results of the previous stage are passed to the next stage.
I guess you're trying to do something like this:
db.CourseCollection.aggregate([
{
"$match": {
"Type": "Spell"
}
},
{
"$lookup": {
"from": "CourseCollection",
"localField": "Parent._id",
"foreignField": "_id",
"as": "Parents"
}
},
{
"$match": {
"Parents": {
"$elemMatch": {
"Type": "Course",
"Access._id": {
"$in": [
"2sssdw5oe"
]
}
}
}
}
}
])
You can achieve the same result doing this too:
db.CourseCollection.aggregate([
{
"$match": {
"Type": "Spell"
}
},
{
"$lookup": {
"from": "CourseCollection",
"localField": "Parent._id",
"foreignField": "_id",
"as": "Parents",
"pipeline": [
{
"$match": {
"Type": "Course",
"Access._id": {
"$in": [
"2sssdw5oe"
]
}
}
}
]
}
},
{
"$match": {
"Parents.0": {
"$exists": true
}
}
}
])

Parent child reversal in the result

I am using MongoDB 3.4.
Have 2 collection as follows.
Collection 1:- type
{
"_id": {
"$numberLong": "1234"
},
"name" : "board1"
"type" : "electronic"
},
{
"_id": {
"$numberLong": "1235"
},
"name" : "board2",
"type" : "electronic"
}
Collection 2:- products
{
"_id": {
"$numberLong": "9876"
},
"types" : [
"1234",
"1235",
"1238"
]
},
{
"_id": {
"$numberLong": "9875"
},
"types" : [
"1234",
"1238"
]
}
Type collection will have multiple types and each product in products collection will have multiple types.
There can be multiple document with different ids for the same type in type collection. And, product collection might have types array with different Ids of same type or different type.
I would like to get all the ids of type electronic and find the products which has id in the types array for each product.
I want result like the below one.
{
"_id": {
"$numberLong": "1234"
},
"name" : "board1",
"products" : [
"9876",
"9875"
]
},
{
"_id": {
"$numberLong": "1235"
},
"name" : "board2"
"products" : [
"9876",
"9875"
]
}
Currently, I am making so many calls, like for each type id, get all products.
Is there any other simple way with single query using $lookup or any other mechanism?
You can try below aggregation in mongodb 3.6 and above
db.types.aggregate([
{ "$match": { "type" : "electronic" }},
{ "$lookup": {
"from": "testCollection2",
"let": { "typeId": "$_id" },
"pipeline": [
{ "$match": { "$expr": { "$in": ["$$typeId", "$types"] }}}
],
"as": "products"
}},
{ "$addFields": {
"products": "$products._id"
}}
])
You can try bbelow aggregation in mongodb 3.4
db.types.aggregate([
{ "$match": { "type" : "electronic" }},
{ "$lookup": {
"from": "testCollection2",
"localField": "_id",
"foreignField": "types",
"as": "products"
}},
{ "$addFields": {
"products": "$products._id"
}}
])
In MongoDB 3.4 you can use $lookup and then $addFields to get _id from products:
db.types.aggregate([
{
"$match": { "type" : "electronic" }
},
{
$lookup: {
from: "products",
localField: "_id",
"foreignField": "types",
"as": "products"
}
},
{
$project: {
field1: 1,
field2: 1,
products: {
$map: {
input: "$products",
as: "p",
in: "$$p._id"
}
}
}
}
])

How to make lookup between two collections when an item in an array exists in the other collection?

In Lookup with a pipeline, I would like to get the linked records from an array in the parent document.
// Orders
[{
"_id" : ObjectId("5b5b91a25c68de2538620689"),
"Name" : "Test",
"Products" : [
ObjectId("5b5b919a5c68de2538620688"),
ObjectId("5b5b925a5c68de2538621a15")
]
}]
// Products
[
{
"_id": ObjectId("5b5b919a5c68de2538620688"),
"ProductName": "P1"
},
{
"_id": ObjectId("5b5b925a5c68de2538621a15"),
"ProductName": "P2"
}
,
{
"_id": ObjectId("5b5b925a5c68de2538621a55"),
"ProductName": "P3"
}
]
How to make a lookup between Orders and Products when Products field is an array!
I tried this query
db.getCollection("Orders").
aggregate(
[
{
$lookup:
{
from: "Products",
let: { localId: "$_id" , prods: "$Products" },
pipeline: [
{
"$match":
{
"_id" : { $in: "$$prods" }
}
},
{
$project:
{
"_id": "$_id",
"name": "$prods" ,
}
}
],
as: "linkedData"
}
},
{
"$skip": 0
},
{
"$limit": 1
},
]
)
This is not working because $in is expecting an array, and even though $$prods is an array, it is not accepting it.
Is my whole approach correct? How to make this magic join ?
You were going in the right direction the only thing you missed here is to use expr with in aggregation operator which matches the same fields of the document
db.getCollection("Orders").aggregate([
{ "$lookup": {
"from": "Products",
"let": { "localId": "$_id" , "prods": "$Products" },
"pipeline": [
{ "$match": { "$expr": { "$in": [ "$_id", "$$prods" ] } } },
{ "$project": { "_id": 1, "name": "$ProductName" } }
],
"as": "linkedData"
}},
{ "$skip": 0 },
{ "$limit": 1 }
])
See the docs here
You just need regular $lookup, the documentation states that:
If your localField is an array, you may want to add an $unwind stage to your pipeline. Otherwise, the equality condition between the localField and foreignField is foreignField: { $in: [ localField.elem1, localField.elem2, ... ] }.
So for below aggregation:
db.Orders.aggregate([
{
$lookup: {
from :"Products",
localField: "Products",
foreignField: "_id",
as: "Products"
}
}
])
you'll get following result for your sample data:
{
"_id" : ObjectId("5b5b91a25c68de2538620689"),
"Name" : "Test",
"Products" : [
{
"_id" : ObjectId("5b5b919a5c68de2538620688"),
"ProductName" : "P1"
},
{
"_id" : ObjectId("5b5b925a5c68de2538621a15"),
"ProductName" : "P2"
}
]
}
have you try unwind before the lookup. use unwind to brak the array annd then make lookup.

MongoDB: How to get add filter main collection by second collection using $lookup [duplicate]

How can I add a filter after an $lookup or is there any other method to do this?
My data collection test is:
{ "_id" : ObjectId("570557d4094a4514fc1291d6"), "id" : 100, "value" : "0", "contain" : [ ] }
{ "_id" : ObjectId("570557d4094a4514fc1291d7"), "id" : 110, "value" : "1", "contain" : [ 100 ] }
{ "_id" : ObjectId("570557d4094a4514fc1291d8"), "id" : 120, "value" : "1", "contain" : [ 100 ] }
{ "_id" : ObjectId("570557d4094a4514fc1291d9"), "id" : 121, "value" : "2", "contain" : [ 100, 120 ] }
I select id 100 and aggregate the childs:
db.test.aggregate([ {
$match : {
id: 100
}
}, {
$lookup : {
from : "test",
localField : "id",
foreignField : "contain",
as : "childs"
}
}]);
I get back:
{
"_id":ObjectId("570557d4094a4514fc1291d6"),
"id":100,
"value":"0",
"contain":[ ],
"childs":[ {
"_id":ObjectId("570557d4094a4514fc1291d7"),
"id":110,
"value":"1",
"contain":[ 100 ]
},
{
"_id":ObjectId("570557d4094a4514fc1291d8"),
"id":120,
"value":"1",
"contain":[ 100 ]
},
{
"_id":ObjectId("570557d4094a4514fc1291d9"),
"id":121,
"value":"2",
"contain":[ 100, 120 ]
}
]
}
But I want only childs that match with "value: 1"
At the end I expect this result:
{
"_id":ObjectId("570557d4094a4514fc1291d6"),
"id":100,
"value":"0",
"contain":[ ],
"childs":[ {
"_id":ObjectId("570557d4094a4514fc1291d7"),
"id":110,
"value":"1",
"contain":[ 100 ]
},
{
"_id":ObjectId("570557d4094a4514fc1291d8"),
"id":120,
"value":"1",
"contain":[ 100 ]
}
]
}
The question here is actually about something different and does not need $lookup at all. But for anyone arriving here purely from the title of "filtering after $lookup" then these are the techniques for you:
MongoDB 3.6 - Sub-pipeline
db.test.aggregate([
{ "$match": { "id": 100 } },
{ "$lookup": {
"from": "test",
"let": { "id": "$id" },
"pipeline": [
{ "$match": {
"value": "1",
"$expr": { "$in": [ "$$id", "$contain" ] }
}}
],
"as": "childs"
}}
])
Earlier - $lookup + $unwind + $match coalescence
db.test.aggregate([
{ "$match": { "id": 100 } },
{ "$lookup": {
"from": "test",
"localField": "id",
"foreignField": "contain",
"as": "childs"
}},
{ "$unwind": "$childs" },
{ "$match": { "childs.value": "1" } },
{ "$group": {
"_id": "$_id",
"id": { "$first": "$id" },
"value": { "$first": "$value" },
"contain": { "$first": "$contain" },
"childs": { "$push": "$childs" }
}}
])
If you question why would you $unwind as opposed to using $filter on the array, then read Aggregate $lookup Total size of documents in matching pipeline exceeds maximum document size for all the detail on why this is generally necessary and far more optimal.
For releases of MongoDB 3.6 and onwards, then the more expressive "sub-pipeline" is generally what you want to "filter" the results of the foreign collection before anything gets returned into the array at all.
Back to the answer though which actually describes why the question asked needs "no join" at all....
Original
Using $lookup like this is not the most "efficient" way to do what you want here. But more on this later.
As a basic concept, just use $filter on the resulting array:
db.test.aggregate([
{ "$match": { "id": 100 } },
{ "$lookup": {
"from": "test",
"localField": "id",
"foreignField": "contain",
"as": "childs"
}},
{ "$project": {
"id": 1,
"value": 1,
"contain": 1,
"childs": {
"$filter": {
"input": "$childs",
"as": "child",
"cond": { "$eq": [ "$$child.value", "1" ] }
}
}
}}
]);
Or use $redact instead:
db.test.aggregate([
{ "$match": { "id": 100 } },
{ "$lookup": {
"from": "test",
"localField": "id",
"foreignField": "contain",
"as": "childs"
}},
{ "$redact": {
"$cond": {
"if": {
"$or": [
{ "$eq": [ "$value", "0" ] },
{ "$eq": [ "$value", "1" ] }
]
},
"then": "$$DESCEND",
"else": "$$PRUNE"
}
}}
]);
Both get the same result:
{
"_id":ObjectId("570557d4094a4514fc1291d6"),
"id":100,
"value":"0",
"contain":[ ],
"childs":[ {
"_id":ObjectId("570557d4094a4514fc1291d7"),
"id":110,
"value":"1",
"contain":[ 100 ]
},
{
"_id":ObjectId("570557d4094a4514fc1291d8"),
"id":120,
"value":"1",
"contain":[ 100 ]
}
]
}
Bottom line is that $lookup itself cannot "yet" query to only select certain data. So all "filtering" needs to happen after the $lookup
But really for this type of "self join" you are better off not using $lookup at all and avoiding the overhead of an additional read and "hash-merge" entirely. Just fetch the related items and $group instead:
db.test.aggregate([
{ "$match": {
"$or": [
{ "id": 100 },
{ "contain.0": 100, "value": "1" }
]
}},
{ "$group": {
"_id": {
"$cond": {
"if": { "$eq": [ "$value", "0" ] },
"then": "$id",
"else": { "$arrayElemAt": [ "$contain", 0 ] }
}
},
"value": { "$first": { "$literal": "0"} },
"childs": {
"$push": {
"$cond": {
"if": { "$ne": [ "$value", "0" ] },
"then": "$$ROOT",
"else": null
}
}
}
}},
{ "$project": {
"value": 1,
"childs": {
"$filter": {
"input": "$childs",
"as": "child",
"cond": { "$ne": [ "$$child", null ] }
}
}
}}
])
Which only comes out a little different because I deliberately removed the extraneous fields. Add them in yourself if you really want to:
{
"_id" : 100,
"value" : "0",
"childs" : [
{
"_id" : ObjectId("570557d4094a4514fc1291d7"),
"id" : 110,
"value" : "1",
"contain" : [ 100 ]
},
{
"_id" : ObjectId("570557d4094a4514fc1291d8"),
"id" : 120,
"value" : "1",
"contain" : [ 100 ]
}
]
}
So the only real issue here is "filtering" any null result from the array, created when the current document was the parent in processing items to $push.
What you also seem to be missing here is that the result you are looking for does not need aggregation or "sub-queries" at all. The structure that you have concluded or possibly found elsewhere is "designed" so that you can get a "node" and all of it's "children" in a single query request.
That means just the "query" is all that is really needed, and the data collection ( which is all that is happening since no content is really being "reduced" ) is just a function of iterating the cursor result:
var result = {};
db.test.find({
"$or": [
{ "id": 100 },
{ "contain.0": 100, "value": "1" }
]
}).sort({ "contain.0": 1 }).forEach(function(doc) {
if ( doc.id == 100 ) {
result = doc;
result.childs = []
} else {
result.childs.push(doc)
}
})
printjson(result);
This does exactly the same thing:
{
"_id" : ObjectId("570557d4094a4514fc1291d6"),
"id" : 100,
"value" : "0",
"contain" : [ ],
"childs" : [
{
"_id" : ObjectId("570557d4094a4514fc1291d7"),
"id" : 110,
"value" : "1",
"contain" : [
100
]
},
{
"_id" : ObjectId("570557d4094a4514fc1291d8"),
"id" : 120,
"value" : "1",
"contain" : [
100
]
}
]
}
And serves as proof that all you really need to do here is issue the "single" query to select both the parent and children. The returned data is just the same, and all you are doing on either server or client is "massaging" into another collected format.
This is one of those cases where you can get "caught up" in thinking of how you did things in a "relational" database, and not realize that since the way the data is stored has "changed", you no longer need to use the same approach.
That is exactly what the point of the documentation example "Model Tree Structures with Child References" in it's structure, where it makes it easy to select parents and children within one query.

Update document inside an array using $where and $and

I've document like the one below, I want to update sold field only when the sold is less than the total field. I know how to retrieve a document within a array of documents and I know how to compare two fields in a document, but I don't know how can I do them all together in one single expression along with the update.
{
"_id" : ObjectId("4f7ee46e08403d063ab0b4f9"),
"name" : "MongoDB",
"notes" : [
{
"_Id" : ObjectId("4f7ee46e08403d063ab0h659")
"title" : "Hello MongoDB",
"content" : "Hello MongoDB",
"total" : 100
},
{
"_Id" : ObjectId("4f7ee46e08403d083ab0h659")
"title" : "ReplicaSet MongoDB",
"content" : "ReplicaSet MongoDB",
"total" : 200,
"sold" : 2
},
{
"_Id" : ObjectId("4f7ee65e08403d083ab0h659")
"title" : "ReplicaSet MongoDB",
"content" : "ReplicaSet MongoDB",
"total" : 200,
}
]
}
My expression to read the document from the array of documents:
db.Collection.find({'notes._id':ObjectId('58791af46c698c00475e7f41')},{'notes.$':true})
I can write an expression like this to compare two fields
db.Collection.find( { $where : "this.total > this.sold" });
The best approach to this would be to first use aggregation as means to determine a list of documents as well as a subset of the array elements that match the given condition i.e. where sold is less than the total field.
Once you get this list you can then process Bulk updates which will at least allow many operations to be sent in a single request with a singular response.
The following example shows how you can go about this:
var ops = [],
cursor = db.collection.aggregate([
{
"$redact": {
"$cond": [
{
"$allElementsTrue": {
"$map": {
"input": "$notes",
"as": "el",
"in": { "$gt": [ "$$el.total", "$$el.sold" ] }
}
}
},
"$$KEEP",
"$$PRUNE"
]
}
},
{
"$project": {
"notes": {
"$filter": {
"input": "$notes",
"as": "el",
"cond": {
"$and": [
{ "$gt": [ "$$el.total", "$$el.sold" ] },
{ "$gt": [ "$$el.sold", null ] }
]
}
}
}
}
}
]);
cursor.forEach(function(doc) {
doc.notes.forEach(function(note){
ops.push({
"updateOne": {
"filter": { "_id": doc._id, "notes._id": note._id },
"update": {
"$set": { "notes.$.sold": 400 } // update the sold field accordingly
}
}
});
if (ops.length === 500 ) {
db.collection.bulkWrite(ops);
ops = [];
}
});
})
if (ops.length > 0)
db.collection.bulkWrite(ops);