I am referring mongodb official page for projection where I came across following example where elements of array in subdocument is filtered:
https://docs.mongodb.com/manual/reference/operator/aggregation/filter/#exp._S_filter
db.sales.aggregate([
{
$project: {
items: {
$filter: {
input: "$items",
as: "item",
cond: { $gte: [ "$$item.price", 100 ] }
}
}
}
}
])
I am trying to implement this in Java but I am not doing it correctly and elements in subdocument array are not filtered.
Input Collection:
{
_id: 0,
items: [
{ item_id: 43, quantity: 2, price: 10 },
{ item_id: 2, quantity: 1, price: 240 }
]
}
{
_id: 1,
items: [
{ item_id: 23, quantity: 3, price: 110 },
{ item_id: 103, quantity: 4, price: 5 },
{ item_id: 38, quantity: 1, price: 300 }
]
}
{
_id: 2,
items: [
{ item_id: 4, quantity: 1, price: 23 }
]
}
Expected Output Collection:
{
"_id" : 0,
"items" : [
{ "item_id" : 2, "quantity" : 1, "price" : 240 }
]
}
{
"_id" : 1,
"items" : [
{ "item_id" : 23, "quantity" : 3, "price" : 110 },
{ "item_id" : 38, "quantity" : 1, "price" : 300 }
]
}
{ "_id" : 2, "items" : [ ] }
In Java(mongo Driver 3.9.1), this is what I am doing:
Bson priceFilter = Filters.gte("items.price", 100);
mongoCollection.aggregate(
Aggregates.project(Projections.fields(priceFilter))
);
How do I project with aggregate function for the subdocument arrays where I need to filter out elements from subdocument array based on some condition?
In MongoDB Java Driver 3.9.1, collection.aggregate() takes a java.util.List as parameter. So you need to replace your Java code with the below.
mongoCollection.aggregate(
Arrays.asList(
Aggregates.project(Projections.computed("items",
new Document().append("$filter",
new Document().append("input", "$items").append("as", "item").append("cond",
new Document().append("$gte", Arrays.asList("$$item.price",100))))))
)
);
Related
I have 2 collections collection1 and collection2. I want to lookup two tables and group and sum the quantity. I tried grouping in collection 2 and lookup with collection1 but didnt get the ouput document correctly. Please help to find this problem.
//collection 1
{
_id: 1,
name: Product1,
units: 20
},
{
_id: 2,
name: Product2,
units: 10
},
{
_id: 3,
name: Product3,
units: 50
},
{
_id: 4,
name: Product4,
units: 4
}
//collection2
{
_id2: 1,
inventory: 1, //foreign key
quantity: 20,
},
{
_id2: 2,
inventory: 1 //foreign key
quantity: 10
},
{
_id2: 3,
inventory: 1 //foreign key
quantity: 50
},
{
_id2: 4,
inventory: 2 //foreign key
quantity: 4
},
{
_id2: 5,
inventory: 2 //foreign key
quantity: 45
},
{
_id2: 6,
inventory: 3 //foreign key
quantity: 49
},
How to write a query in order to get output with collection1 data like this
{
_id1: 1,
name: Product1,
units: 20,
inventoryList: [quantity: 80]
},
{
_id1: 2,
name: Product2,
units: 10,
inventoryList: [quantity: 49]
},
{
_id1: 3,
name: Product3,
units: 50,
inventoryList: [quantity: 49]
},
{
_id1: 4,
name: Product4,
units: 4,
inventoryList: [quantity: 0]
}
SOLUTION #1:
db.collection1.aggregate([
{
$lookup: {
from: "collection2",
localField: "_id",
foreignField: "inventory",
as: "inventoryList"
}
},
{
$addFields: {
inventoryList: {
$reduce: {
input: "$inventoryList",
initialValue: 0,
in: {
$sum: ["$$value", "$$this.quantity"]
}
}
}
}
}
]);
SOLUTION #2: As suggested by #turivishal in the below comments.
db.collection1.aggregate([
{
$lookup: {
from: "collection2",
localField: "_id",
foreignField: "inventory",
as: "inventoryList"
}
},
{
$addFields: {
inventoryList: { $sum: "$inventoryList.quantity" }
}
}
]);
Output:
/* 1 */
{
"_id" : 1,
"name" : "Product1",
"units" : 20,
"inventoryList" : 80
},
/* 2 */
{
"_id" : 2,
"name" : "Product2",
"units" : 10,
"inventoryList" : 49
},
/* 3 */
{
"_id" : 3,
"name" : "Product3",
"units" : 50,
"inventoryList" : 49
},
/* 4 */
{
"_id" : 4,
"name" : "Product4",
"units" : 4,
"inventoryList" : 0
}
SOLUTION 3: If you want it exactly as in your expected output:
db.collection1.aggregate([
{
$lookup: {
from: "collection2",
localField: "_id",
foreignField: "inventory",
as: "inventoryList"
}
},
{
$addFields: {
inventoryList: [{ quantity: { $sum: "$inventoryList.quantity" } }]
}
}
]);
Output:
/* 1 */
{
"_id" : 1,
"name" : "Product1",
"units" : 20,
"inventoryList" : [
{
"quantity" : 80
}
]
},
/* 2 */
{
"_id" : 2,
"name" : "Product2",
"units" : 10,
"inventoryList" : [
{
"quantity" : 49
}
]
},
/* 3 */
{
"_id" : 3,
"name" : "Product3",
"units" : 50,
"inventoryList" : [
{
"quantity" : 49
}
]
},
/* 4 */
{
"_id" : 4,
"name" : "Product4",
"units" : 4,
"inventoryList" : [
{
"quantity" : 0
}
]
}
You can try lookup with pipeline,
let to pass inventory id and match expression condition
$group by null and sum quantity
db.col1.aggregate([
{
$lookup: {
from: "col2",
let: { inventory: "$_id" },
pipeline: [
{
$match: {
$expr: { $eq: ["$$inventory", "$inventory"] }
}
},
{
$group: {
_id: null,
quantity: { $sum: "$quantity" }
}
}
],
as: "inventoryList"
}
}
])
Playground
This question already has an answer here:
Move an element from one array to another within same document MongoDB
(1 answer)
Closed 3 years ago.
I have data that looks like this:
{
"_id": ObjectId("4d525ab2924f0000000022ad"),
"arrayField": [
{ id: 1, other: 23 },
{ id: 2, other: 21 },
{ id: 0, other: 235 },
{ id: 3, other: 765 }
],
"someOtherArrayField": []
}
Given a nested object's ID (0), I'd like to $pull the element from one array (arrayField) and $push it to another array (someOtherArrayField) within the same document. The result should look like this:
{
"_id": ObjectId("id"),
"arrayField": [
{ id: 1, other: 23 },
{ id: 2, other: 21 },
{ id: 3, other: 765 }
],
"someOtherArrayField": [
{ id: 0, other: 235 }
]
}
I realize that I can accomplish this with a find followed by an update, i.e.
db.foo.findOne({"_id": param._id})
.then((doc)=>{
db.foo.update(
{
"_id": param._id
},
{
"$pull": {"arrayField": {id: 0}},
"$push": {"someOtherArrayField": {doc.array[2]} }
}
)
})
But I'm looking for an atomic operation like, in pseudocode, this:
db.foo.update({"_id": param._id}, {"$move": [{"arrayField": {id: 0}}, {"someOtherArrayField": 1}]}
Is there an atomic way to do this, perhaps using MongoDB 4.2's ability to specify a pipeline to an update command? How would that look?
I found this post that generously provided the data I used, but the provided solution isn't an atomic operation. Has an atomic solution become possible with MongoDB 4.2?
Here's an example:
> db.baz.find()
> db.baz.insert({
... "_id": ObjectId("4d525ab2924f0000000022ad"),
... "arrayField": [
... { id: 1, other: 23 },
... { id: 2, other: 21 },
... { id: 0, other: 235 },
... { id: 3, other: 765 }
... ],
... "someOtherArrayField": []
... })
WriteResult({ "nInserted" : 1 })
function extractIdZero(arrayFieldName) {
return {$arrayElemAt: [
{$filter: {input: arrayFieldName, cond: {$eq: ["$$this.id", 0]}}},
0
]};
}
extractIdZero("$arrayField")
{
"$arrayElemAt" : [
{
"$filter" : {
"input" : "$arrayField",
"cond" : {
"$eq" : [
"$$this.id",
0
]
}
}
},
0
]
}
db.baz.updateOne(
{_id: ObjectId("4d525ab2924f0000000022ad")},
[{$set: {
arrayField: {$filter: {
input: "$arrayField",
cond: {$ne: ["$$this.id", 0]}
}},
someOtherArrayField: {$concatArrays: [
"$someOtherArrayField",
[extractIdZero("$arrayField")]
]}
}}
])
{ "acknowledged" : true, "matchedCount" : 1, "modifiedCount" : 1 }
> db.baz.findOne()
{
"_id" : ObjectId("4d525ab2924f0000000022ad"),
"arrayField" : [
{
"id" : 1,
"other" : 23
},
{
"id" : 2,
"other" : 21
},
{
"id" : 3,
"other" : 765
}
],
"someOtherArrayField" : [
{
"id" : 0,
"other" : 235
}
]
}
from the mongodb official documentation :
The following examples query against the inventory collection with the following documents:
{ _id: 1, item: { name: "ab", code: "123" }, qty: 15, tags: [ "A", "B", "C" ] }
{ _id: 2, item: { name: "cd", code: "123" }, qty: 20, tags: [ "B" ] }
{ _id: 3, item: { name: "ij", code: "456" }, qty: 25, tags: [ "A", "B" ] }
{ _id: 4, item: { name: "xy", code: "456" }, qty: 30, tags: [ "B", "A" ] }
{ _id: 5, item: { name: "mn", code: "000" }, qty: 20, tags: [ [ "A", "B" ], "C" ] }
The following example queries the inventory collection to select all documents where the tags array equals exactly the specified array or the tags array contains an element that equals the array [ "A", "B" ].
db.inventory.find( { tags: { $eq: [ "A", "B" ] } } )
The query is equivalent to:
db.inventory.find( { tags: [ "A", "B" ] } )
Both queries match the following documents:
{ _id: 3, item: { name: "ij", code: "456" }, qty: 25, tags: [ "A", "B" ] }
{ _id: 5, item: { name: "mn", code: "000" }, qty: 20, tags: [ [ "A", "B" ], "C" ] }
Now i wish to know how i can query in order to get the document(s) having its tags field exactly equal to [ "A", "B" ] and not containing it alone or among other elements ? i want the result for the example above will be only the first document returned :
{ _id: 3, item: { name: "ij", code: "456" }, qty: 25, tags: [ "A", "B" ] }
If you want to only extract the documents that accurately match the array that you provide, you can add a $size operand in your query:
db.inventory.find({
$and: [
{ tags: "A" },
{ tags: "B" },
{ tags: { $size: 2 }}
]
});
The above query will only match documents that have the tags field equal to the specified array, with its elements in that exact order.
The solution provided by chridam in the comments is a more elegant solution:
db.inventory.find({ "tags": { "$all": [ "A", "B" ], "$size": 2 } })
UPDATE:
I inserted the documents you provided in a local MongoDB instance to test my and chridam's queries, and they both return the same result set from the documents that you provided:
{ "_id" : ObjectId("580146168ff3eea72fd1edc7"), "item" : { "name" : "ij", "code" : "456" }, "qty" : 25, "tags" : [ "A", "B" ] }
{ "_id" : ObjectId("580146168ff3eea72fd1edc8"), "item" : { "name" : "xy", "code" : "456" }, "qty" : 30, "tags" : [ "B", "A" ] }
As you can see, it matches the elements of the array and the size, but it does not account for the order in which they appear in the array.
Therefore, I explored different approaches in order to provide a working solution for the outcome you specified, which is to match both the exact contents of the array, as well as their order.
I managed to write the following query using the $where operator, which complies with your request:
db.items.find({ $where: function() {
var arr = ["A", "B"],
tags = this.tags;
if(tags.length !== arr.length) {
return false;
}
for(var i = 0; i < tags.length; i++) {
if(tags[i] !== arr[i]) {
return false;
}
}
return true;
}});
/*
* RESULT SET
*/
{ "_id" : ObjectId("580146168ff3eea72fd1edc7"), "item" : { "name" : "ij", "code" : "456" }, "qty" : 25, "tags" : [ "A", "B" ] }
I've searched high and low but not been able to find what i'm looking for so apologies if this has already been asked.
Consider the following documents
{
_id: 1,
items: [
{
category: "A"
},
{
category: "A"
},
{
category: "B"
},
{
category: "C"
}]
},
{
_id: 2,
items: [
{
category: "A"
},
{
category: "B"
}]
},
{
_id: 3,
items: [
{
category: "A"
},
{
category: "A"
},
{
category: "A"
}]
}
I'd like to be able to find those documents which have more than 1 category "A" item in the items array. So this should find documents 1 and 3.
Is this possible?
Using aggregation
> db.spam.aggregate([
{$unwind: "$items"},
{$match: {"items.category" :"A"}},
{$group: {
_id: "$_id",
item: {$push: "$items.category"}, count: {$sum: 1}}
},
{$match: {count: {$gt: 1}}}
])
Output
{ "_id" : 3, "item" : [ "A", "A", "A" ], "count" : 3 }
{ "_id" : 1, "item" : [ "A", "A" ], "count" : 2 }
I have a list of employees, each who belong to a department and a company.
An employee also has a salary history. The last value is their current salary.
Example:
{
name: "Programmer 1"
employee_id: 1,
dept_id: 1,
company_id: 1,
salary: [50000,50100,50200]
},
{
name: "Programmer 2"
employee_id: 2,
dept_id: 1,
company_id: 1,
salary: [50000,50200,50300]
},
{
name: "Manager"
employee_id: 3,
dept_id: 2,
company_id: 1,
salary: [60000,60500,61000]
},
{
name: "Contractor (different company)"
employee_id: 4,
dept_id: 1,
company_id: 2,
salary: [60000,60500,75000]
}
I want to find the current average salary for employees, grouped by dept_id and company_id.
Something like:
db.employees.aggregate(
{ $project : { employee_id: 1, dept_id: 1, company_id: 1, salaries: 1}},
{ $unwind : "$salaries" },
{
"$group" : {
"_id" : {
"dept_id" : "$dept_id",
"company_id" : "$company_id",
},
current_salary_avg : { $avg : "$salaries.last()" }
}
}
);
In this case it would be
Company 1, Group 1: 50250
Company 1, Group 2: 61000
Company 2, Group 1: 75000
I've seen examples doing something similar with $unwind, but I'm struggling with getting the last value of salary. Is $slice the correct operator in this case, and if so how do I use it with project?
In this case you need to set up your pipeline as follows :
unwind the salary list to get all the salaries for each employee
group by employee, dept and company and get the last salary
group by dept and company and get the average salary
The code for this aggregation pipeline is :
use test;
db.employees.aggregate( [
{$unwind : "$salary"},
{
"$group" : {
"_id" : {
"dept_id" : "$dept_id",
"company_id" : "$company_id",
"employee_id" : "$employee_id",
},
"salary" : {$last: "$salary"}
}
},
{
"$group" : {
"_id" : {
"company_id" : "$_id.company_id",
"dept_id" : "$_id.dept_id",
},
"current_salary_avg" : {$avg: "$salary"}
}
},
{$sort :
{
"_id.company_id" : 1,
"_id.dept_id" : 1,
}
},
]);
Assuming that you have imported the data with:
mongoimport --drop -d test -c employees <<EOF
{ name: "Programmer 1", employee_id: 1, dept_id: 1, company_id: 1, salary: [50000,50100,50200]}
{ name: "Programmer 2", employee_id: 2, dept_id: 1, company_id: 1, salary: [50000,50200,50300]}
{ name: "Manager", employee_id: 3, dept_id: 2, company_id: 1, salary: [60000,60500,61000]}
{ name: "Contractor (different company)", employee_id: 4, dept_id: 1, company_id: 2, salary: [60000,60500,75000]}
EOF
Now you can use $slice in aggregation. To return elements from either the start or end of the array: { $slice: [ <array>, <n> ] }
To return elements from the specified position in the array: { $slice: [ <array>, <position>, <n> ] }.
And a couple of examples from the mongo page:
{ $slice: [ [ 1, 2, 3 ], 1, 1 ] } // [ 2 ]
{ $slice: [ [ 1, 2, 3 ], -2 ] } // [ 2, 3 ]
{ $slice: [ [ 1, 2, 3 ], 15, 2 ] } // [ ]
{ $slice: [ [ 1, 2, 3 ], -15, 2 ] } // [ 1, 2 ]