I have a question that is similar to this other question but not exactly the same because my data structure is more deeply nested, and the accepted answer did not resolve the issue.
Technologies: MongoDB 3.6, Mongoose 5.5, NodeJS 12
I am trying to query a deeply nested array of objects. The query will accept a "Start Date" and an "End Date" from the user. Item Report is an array of subdocuments that contains another array of subdocuments "Work Done By". All WorkDoneBy objects that have a "CompletedDate" in the Start and End date range should be returned along with several other properties.
Desired return properties:
RecordID, RecordType, Status, ItemReport.WorkDoneBy.DateCompleted, ItemReport.WorkDoneBy.CompletedHours, ItemReport.WorkDoneBy.Person
Record schema:
let RecordsSchema = new Schema({
RecordID: {
type: Number,
index: true
},
RecordType: {
type: String,
enum: ['Item', 'OSW']
},
Status: {
type: String
},
// ItemReport array of subdocuments
ItemReport: [ItemReportSchema],
}, {
collection: 'records',
selectPopulatedPaths: false
});
let ItemReportSchema = new Schema({
// ObjectId reference
ReportBy: {
type: Schema.Types.ObjectId,
ref: 'people'
},
ReportDate: {
type: Date,
required: true
},
WorkDoneBy: [{
Person: {
type: Schema.Types.ObjectId,
ref: 'people'
},
CompletedHours: {
type: Number,
required: true
},
DateCompleted: {
type: Date
}
}],
});
Attempt 1:
db.records.aggregate([
{
"$match": {
"ItemReport.WorkDoneBy.DateCompleted": { "$gt": new Date("2017-01-01T12:00:00.000Z"), "$lt": new Date("2018-12-31T12:00:00.000Z") }
}
},
{
"$project": {
"ItemReport.WorkDoneBy": {
"$filter": {
"input": "$ItemReport.WorkDoneBy",
"as": "value",
"cond": {
"$and": [
{ "$ne": [ "$$value.DateCompleted", null ] },
{ "$gt": [ "$$value.DateCompleted", new Date("2017-01-01T12:00:00.000Z") ] },
{ "$lt": [ "$$value.DateCompleted", new Date("2018-12-31T12:00:00.000Z") ] }
]
}
}
}
}
}
])
Attempt 1 returns:
{ "_id" : ObjectId("5dcb6406e63830b7aa54269d"), "ItemReport" : [ { "WorkDoneBy" : [ ] } ] }
{ "_id" : ObjectId("5dcb6406e63830b7aa5426fb"), "ItemReport" : [ { "WorkDoneBy" : [ ] } ] }
{ "_id" : ObjectId("5dcb6406e63830b7aa542708"), "ItemReport" : [ { "WorkDoneBy" : [ ] } ] }
{ "_id" : ObjectId("5dcb6406e63830b7aa542712"), "ItemReport" : [ { "WorkDoneBy" : [ ] } ] }
Desired return (removed _id for brevity):
Note that objects in the WorkDoneBy array should be returned ONLY if they are within the specified date range. For example RecordID 9018 ItemReport.WorkDoneBy actually has dates in 2016 but those are not returned because they are not within the specified date range.
{ "ItemReport" : [ { "WorkDoneBy" : [ { "CompletedHours" : 11, "DateCompleted" : ISODate("2017-09-29T04:00:00Z"), "Person" : ObjectId("5dcb6409e63830b7aa54fd6e") }, { "CompletedHours" : 36, "DateCompleted" : ISODate("2018-05-18T04:00:00Z"), "Person" : ObjectId("5dcb6409e63830b7aa54fd6e") }, { "CompletedHours" : 32, "DateCompleted" : ISODate("2018-05-18T04:00:00Z"), "Person" : ObjectId("5dcb6409e63830b7aa54fd6e") } ] } ], "RecordID" : 9018, "RecordType" : "Item", "Status" : "Done" }
{ "ItemReport" : [ { "WorkDoneBy" : [ { "CompletedHours" : 1.5, "DateCompleted" : ISODate("2017-09-01T04:00:00Z"), "Person" : ObjectId("5dcb6409e63830b7aa54fe5f") } ] } ], "RecordID" : 9019, "RecordType" : "Item", "Status" : "Done" }
{ "ItemReport" : [ { "WorkDoneBy" : [ { "CompletedHours" : 2, "DateCompleted" : ISODate("2017-09-08T04:00:00Z"), "Person" : ObjectId("5dcb6409e63830b7aa54fd6e") }, { "CompletedHours" : 18, "DateCompleted" : ISODate("2017-09-15T04:00:00Z"), "Person" : ObjectId("5dcb6409e63830b7aa54fd6e") }, { "CompletedHours" : 7, "DateCompleted" : ISODate("2017-09-20T04:00:00Z"), "Person" : ObjectId("5dcb6409e63830b7aa54fd6e") } ] } ], "RecordID" : 9017, "RecordType" : "Item", "Status" : "Done" }
The problem here is that WorkDoneBy is an array nested in another array (ItemReport). Therefore single $filter is not enough since you need to iterate twice. You can add $map to iterate over the outer array:
db.records.aggregate([
{
"$project": {
"ItemReport": {
$map: {
input: "$ItemReport",
as: "ir",
in: {
WorkDoneBy: {
$filter: {
input: "$$ir.WorkDoneBy",
as: "value",
cond: {
"$and": [
{ "$ne": [ "$$value.DateCompleted", null ] },
{ "$gt": [ "$$value.DateCompleted", new Date("2017-01-01T12:00:00.000Z") ] },
{ "$lt": [ "$$value.DateCompleted", new Date("2018-12-31T12:00:00.000Z") ] }
]
}
}
}
}
}
}
}
}
])
please check below:
db.collection.aggregate([
{
$project: {
_id: 0,
RecordID: 1,
RecordType: 1,
Status: 1,
ItemReport: {
$let: {
vars: {
wdb: {
$reduce: {
input: "$ItemReport.WorkDoneBy",
initialValue: [],
in: {
$concatArrays: [
"$$this",
"$$value"
]
}
}
}
},
in: {
WorkDoneBy: {
$filter: {
input: "$$wdb",
as: "item",
cond: {
$and: [
{
$gte: [
"$$item.DateCompleted",
ISODate("2010-01-10T04:00:00Z") // start date
]
},
{
$lte: [
"$$item.DateCompleted",
ISODate("2018-01-10T04:00:00Z") // end date
]
},
]
}
}
}
}
}
}
}
}
])
mongodb playground
Related
I have below user details in my bookings collection
{
"_id" : ObjectId("609a382b589346973c84c6fe"),
"Name" : "abc",
"UserId":1
"Status" : "Pending",
"BookingData" : {
"Date" : ISODate("2021-04-30T04:00:00.000Z"),
"info" : [],
"BookingDataMethod" : "avf",
"Message" : null,
"products" : [
{
"_id" : ObjectId("60a4e92775e5de3570578820"),
"ProductName" : "Test1",
"ProductID" : ObjectId("60a4e92475e5de357057880a"),
"IsDeliveryFailed" : "Yes"
},
{
"_id" : ObjectId("60a4e92775e5de357057881f"),
"ProductName" : "Test2",
"ProductID" : ObjectId("60a4e92475e5de357057880d")
}
],
}
}
I have prepared a query for the below conditions and when I run the below query I should get the "UserId":1 documents but I got 0 records
condition 1: products should not be null
condition 2: ProductID should exist in the products array and should not be null
condition 3: IsDeliveryFailed should not be "Yes"
Based on the above user only one product got delivery failed(IsDeliveryFailed": "Yes") so when I run this query it should return "UserId":1 document. if both products "IsDeliveryFailed": "Yes" then
we should not get this user
Query
db.getCollection('bookings').find({
"$and": [
{ "BookingData.products": { $ne: [] } },
{ "BookingData.products": {"$elemMatch":{ "ProductID": { "$exists": true ,$ne: null } }} },
{ "BookingData.products": {"$elemMatch":{ "IsDeliveryFailed": { $ne: 'Yes' } }} }
]
})
Could someone please tell me the issue on the above query or please help me to prepare a query for the above condition?
I think you can do it with aggregations
db.collection.aggregate([
{
$match: {
"BookingData.products": { "$exists": true }
}
},
{
$set: {
"BookingData.products": {
"$filter": {
"input": "$BookingData.products",
"cond": {
$and: [
{ $ne: [ "$$this.ProductID", undefined ] },
{ $ne: [ "$$this._id", null ] },
{ $ne: [ "$$this.IsDeliveryFailed", "Yes" ] }
]
}
}
}
}
},
{
$match: {
$expr: {
$ne: [ "$BookingData.products", [] ]
}
}
}
])
Working Mongo playground
can i update specific fields in a particular collection according to the matched keys from different collections i.e suppose i have 3 collections
**///collection 1: col1///**
_id:ObjectId("#####7b")
name:'name1',
itemsBought:
[
{
"_id":ObjectId("####c1"
"itemName" : "name1",
"itemCode" : "IT001",
"itemQuantity" : 19,
"itemPrediction":23
},
{
"_id":ObjectId("####c2"
"itemName" : "name2",
"itemCode" : "IT002",
"itemQuantity" : 79,
"itemPrediction":69
},
{
"_id":ObjectId("####c3"
"itemName" : "name2",
"itemCode" : "IT003",
"itemQuantity" : 0,
"itemPrediction":0
},
]
**///collection 1: col2///**
{
"itemQuantity" : 21,
"itemCode" : "IT001",
},
{
"itemQuantity" : 2,
"itemCode" : "IT003",
}
**///collection 1: col3///**
{
"itemCode" : "IT001",
"itemPrediction":23
},
{
"itemCode" : "IT002",
"itemPrediction":12
},
{
"itemCode" : "IT003",
"itemPrediction":7
},
i am using $aggregation $lookup to fetch out all the required data, before sending it to the frontend i need to fetch the values of itemQuantity from col2 and itemPrediction from col3 and update that in col1 with the matching itemCode. So i have the query which fetches out all the data from all the collections but i dont know how to use $set to update the values in col1.
Workaround: You may perform aggregation and save the result manually
db.col1.aggregate([
{
$lookup: {
from: "col2",
let: {
root_itemCode: "$itemsBought.itemCode"
},
pipeline: [
{
$match: {
$expr: {
$in: [
"$itemCode",
"$$root_itemCode"
]
}
}
}
],
as: "col2"
}
},
{
$lookup: {
from: "col3",
let: {
root_itemCode: "$itemsBought.itemCode"
},
pipeline: [
{
$match: {
$expr: {
$in: [
"$itemCode",
"$$root_itemCode"
]
}
}
}
],
as: "col3"
}
},
{
$addFields: {
itemsBought: {
$map: {
input: "$itemsBought",
as: "item",
in: {
"_id": "$$item._id",
"itemName": "$$item.itemName",
"itemCode": "$$item.itemCode",
"itemQuantity": {
$let: {
vars: {
input: {
$arrayElemAt: [
{
$filter: {
input: "$col2",
cond: {
$eq: [
"$$item.itemCode",
"$$this.itemCode"
]
}
}
},
0
]
},
default: "$$item.itemQuantity"
},
in: {
$ifNull: [
"$$input.itemQuantity",
"$$default"
]
}
}
},
"itemPrediction": {
$let: {
vars: {
input: {
$arrayElemAt: [
{
$filter: {
input: "$col3",
cond: {
$eq: [
"$$item.itemCode",
"$$this.itemCode"
]
}
}
},
0
]
},
default: "$$item.itemPrediction"
},
in: {
$ifNull: [
"$$input.itemPrediction",
"$$default"
]
}
}
}
}
}
}
}
},
{
$unset: [
"col2",
"col3"
]
}
])
MongoPlayground
Mongoose
Collection1.aggregate([...], function (err, result) {
if(err) console.log("error-agg: " + err);
result.forEach(function(item) {
Collection1.updateOne({_id:item._id}, {$set:item}, function (err) {
if(err) console.log("error-saving: " + err);
});
});
});
When use js code,i can use functional expression one by one;For example:
array.map(***).filter(...)
can i use filter after map like above in mongoose?
My question is like this.I have an dataset like below:
{
"_id" : ObjectId("5e3bd328f3dec754e1b8e17d"),
"userId" : "5e33ee0b4a3895a6d246f3ee",
"userName" : "jackiewillen",
"hasReviewedTimes" : 4,
"notes" : [
{
"time" : ISODate("2020-02-23T10:12:19.190Z"),
"memoryLine" : [
{
"hasReviewed" : false,
"_id" : ObjectId("5e51df83966daeae41e7f5b1"),
"memoryTime" : ISODate("2020-02-23T10:42:19.190Z")
},
{
"hasReviewed" : false,
"_id" : ObjectId("5e51df83966daeae41e7f5b0"),
"memoryTime" : ISODate("2020-02-23T22:12:19.190Z")
}
]
},
{
"time" : ISODate("2020-02-23T10:45:26.615Z"),
"memoryLine" : [
{
"hasReviewed" : false,
"_id" : ObjectId("5e51e746966daeae41e7f5bd"),
"memoryTime" : ISODate("2020-02-23T11:15:26.615Z")
},
{
"hasReviewed" : false,
"_id" : ObjectId("5e51e746966daeae41e7f5bc"),
"memoryTime" : ISODate("2020-02-23T22:45:26.615Z")
}
]
},
}
i use $map to get item which contain memoryTime less than now in memoryLine like below:
db.notes.aggregate([{
$match: {
"$and": [
{ userId: '5e33ee0b4a3895a6d246f3ee'}
]
}
}, {
$project: {
notes: {
$map: {
input: "$notes",
in: {
$mergeObjects: [
"$$this",
{
memoryLine: {
$filter: {
input: "$$this.memoryLine",
as: "mLine",
cond: { $lt: ["$$mLine.memoryTime", new Date()] }
}
}
}
]
},
},
}
}
}
])
my result is like below:
"notes": [
{
"time": "2020-02-23T10:12:19.190Z",
"memoryLine": [
{
"hasReviewed": false,
"_id": "5e51df83966daeae41e7f5b1",
"memoryTime": "2020-02-23T10:42:19.190Z"
}
]
},
{ // =====> this item is not needed because of containing empty memoryLine
"time": "2020-02-23T10:45:26.615Z",
"memoryLine": [] // =======> i dont want empty item
},
]
but i want result like this:
"notes": [
{
"time": "2020-02-23T10:12:19.190Z",
"memoryLine": [
{
"hasReviewed": false,
"_id": "5e51df83966daeae41e7f5b1",
"memoryTime": "2020-02-23T10:42:19.190Z"
}
]
}
]
so i use $filter after $map to filter item which contain empty memoryLine:
db.notes.aggregate([{
$match: {
"$and": [
{ userId: '5e33ee0b4a3895a6d246f3ee'}
]
}
}, {
$project: {
notes: {
$map: {
input: "$notes",
in: {
$mergeObjects: [
"$$this",
{
memoryLine: {
$filter: {
input: "$$this.memoryLine",
as: "mLine",
cond: { $lt: ["$$mLine.memoryTime", new Date()] }
}
}
}
],
$filter: {
input: "$$this",
as: "note",
cond: { $ne: ["$$note.memoryLine", []] }
}
},
},
}
}
}
Then this goes wrong.
You need to run another $filter as a separate pipeline stage (for readability) or as the most outer one for your current $project. I would prefer the first one:
{
$addFields: {
notes: {
$filter: {
input: "$notes",
cond: {
$ne: [ "$$this.memoryLine", [] ]
}
}
}
}
}
Here is my item model.
const itemSchema = new Schema({
name: String,
category: String,
occupied: [Number],
active: { type: Boolean, default: true },
});
I want to filter 'occupied' array. So I use aggregate and unwind 'occupied' field.
So I apply match query. And group by _id.
But if filtered 'occupied' array is empty, the item disappear.
Here is my code
Item.aggregate([
{ $match: {
active: true
}},
{ $unwind:
"$occupied",
},
{ $match: { $and: [
{ occupied: { $gte: 100 }},
{ occupied: { $lt: 200 }}
]}},
{ $group : {
_id: "$_id",
name: { $first: "$name"},
category: { $first: "$category"},
occupied: { $addToSet : "$occupied" }
}}
], (err, items) => {
if (err) throw err;
return res.json({ data: items });
});
Here is example data set
{
"_id" : ObjectId("59c1bced987fa30b7421a3eb"),
"name" : "printer1",
"category" : "printer",
"occupied" : [ 95, 100, 145, 200 ],
"active" : true
},
{
"_id" : ObjectId("59c2dbed992fb91b7421b1ad"),
"name" : "printer2",
"category" : "printer",
"occupied" : [ ],
"active" : true
}
The result above query
[
{
"_id" : ObjectId("59c1bced987fa30b7421a3eb"),
"name" : "printer1",
"category" : "printer",
"occupied" : [ 100, 145 ],
"active" : true
}
]
and the result I want
[
{
"_id" : ObjectId("59c1bced987fa30b7421a3eb"),
"name" : "printer1",
"category" : "printer",
"occupied" : [ 100, 145 ],
"active" : true
},
{
"_id" : ObjectId("59c2dbed992fb91b7421b1ad"),
"name" : "printer2",
"category" : "printer",
"occupied" : [ ],
"active" : true
}
]
how could I do this??
Thanks in advance.
In the simplest form, you keep it simply by not using $unwind in the first place. Your conditions applied imply that you are looking for the "unique set" of matches to specific values.
For this you instead use $filter, and a "set operator" like $setUnion to reduce the input values to a "set" in the first place:
Item.aggregate([
{ "$match": { "active": true } },
{ "$project": {
"name": 1,
"category": 1,
"occupied": {
"$filter": {
"input": { "$setUnion": [ "$occupied", []] },
"as": "o",
"cond": {
"$and": [
{ "$gte": ["$$o", 100 ] },
{ "$lt": ["$$o", 200] }
]
}
}
}
}}
], (err, items) => {
if (err) throw err;
return res.json({ data: items });
});
Both have been around since MongoDB v3, so it's pretty common practice to do things this way.
If for some reason you were still using MongoDB 2.6, then you could apply $map and $setDifference instead:
Item.aggregate([
{ "$match": { "active": true } },
{ "$project": {
"name": 1,
"category": 1,
"occupied": {
"$setDifference": [
{ "$map": {
"input": "$occupied",
"as": "o",
"in": {
"$cond": {
"if": {
"$and": [
{ "$gte": ["$$o", 100 ] },
{ "$lt": ["$$o", 200] }
]
},
"then": "$$o",
"else": false
}
}
}},
[false]
]
}
}}
], (err, items) => {
if (err) throw err;
return res.json({ data: items });
});
It's the same "unique set" result as pulling the array apart, filtering the items and putting it back together with $addToSet. The difference being that its far more efficient, and retains ( or produces ) an empty array without any issues.
I have the following metrics collection:
{
name: "Hello",
values: [
{
value: 2629,
date: "2016-10-28T07:00:00.000Z",
_id: "58453abfef7aaa15ac1fdee8"
},
{
value: 1568,
date: "2016-10-29T07:00:00.000Z",
_id: "58453abfef7aaa15ac1fdee7"
},
{
value: 1547,
date: "2016-10-30T07:00:00.000Z",
_id: "58453abfef7aaa15ac1fdee6"
},
{
value: 1497,
date: "2016-10-31T07:00:00.000Z",
_id: "58453abfef7aaa15ac1fdee5"
},
{
value: 3031,
date: "2016-11-01T07:00:00.000Z",
_id: "58453abfef7aaa15ac1fdee4"
},
{
value: 2559,
date: "2016-11-02T07:00:00.000Z",
_id: "58453abfef7aaa15ac1fdee3"
},
{
value: 2341,
date: "2016-11-03T07:00:00.000Z",
_id: "58453abfef7aaa15ac1fdee2"
},
{
value: 2188,
date: "2016-11-04T07:00:00.000Z",
_id: "58453abfef7aaa15ac1fdee1"
},
{
value: 3280,
date: "2016-11-05T07:00:00.000Z",
_id: "58453abfef7aaa15ac1fdee0"
},
{
value: 4638,
date: "2016-11-06T07:00:00.000Z",
_id: "58453abfef7aaa15ac1fdedf"
}
]
},
.... more of the same
What I would like to get is all the values between a custom date range.
I've tried the following query but I still get the entire values array returned:
{
name: "Hello",
values: {
$elemMatch: {
date: {
$lt: "2016-11-03T07:00:00.000Z",
$gt: "2016-10-28T07:00:00.000Z"
}
}
}
}
Maybe I saved my dates in a wrong format ? Any help would be greatly appreciated.
You can run an aggregation pipeline that uses the $filter operator on the values array. The following mongo shell query demonstrates this:
var start = new Date("2016-10-28T07:00:00.000Z"),
end = new Date("2016-11-03T07:00:00.000Z");
db.metrics.aggregate([
{
"$match": {
"name": "Hello",
"values.date": { "$gt": start, "$lt": end }
}
},
{
"$project": {
"name": 1,
"values": {
"$filter": {
"input": "$values",
"as": "value",
"cond": {
"$and": [
{ "$gt": [ "$$value.date", start ] },
{ "$lt": [ "$$value.date", end ] }
]
}
}
}
}
}
])
Sample Output
/* 1 */
{
"_id" : ObjectId("5845453145fda1298fa50db9"),
"name" : "Hello",
"values" : [
{
"value" : 1568,
"date" : ISODate("2016-10-29T07:00:00.000Z"),
"_id" : ObjectId("58453abfef7aaa15ac1fdee7")
},
{
"value" : 1547,
"date" : ISODate("2016-10-30T07:00:00.000Z"),
"_id" : ObjectId("58453abfef7aaa15ac1fdee6")
},
{
"value" : 1497,
"date" : ISODate("2016-10-31T07:00:00.000Z"),
"_id" : ObjectId("58453abfef7aaa15ac1fdee5")
},
{
"value" : 3031,
"date" : ISODate("2016-11-01T07:00:00.000Z"),
"_id" : ObjectId("58453abfef7aaa15ac1fdee4")
},
{
"value" : 2559,
"date" : ISODate("2016-11-02T07:00:00.000Z"),
"_id" : ObjectId("58453abfef7aaa15ac1fdee3")
}
]
}
For MongoDB 3.0, the following workaround applies:
var start = new Date("2016-10-28T07:00:00.000Z"),
end = new Date("2016-11-03T07:00:00.000Z");
db.metrics.aggregate([
{
"$match": {
"name": "Hello",
"values.date": { "$gt": start, "$lt": end }
}
},
{
"$project": {
"name": 1,
"values": {
"$setDifference": [
{
"$map": {
"input": "$values",
"as": "value",
"in": {
"$cond": [
{
"$and": [
{ "$gt": [ "$$value.date", start ] },
{ "$lt": [ "$$value.date", end ] }
]
},
"$$value",
false
]
}
}
},
[false]
]
}
}
}
])
The Aggregation Framework in MongoDB 2.2+ provides an alternative to Map/Reduce. The $unwind operator can be used to separate your values array into a stream of documents that can be matched:
db.tmp.aggregate(
// Start with a $match pipeline which can take advantage of an index and limit documents processed
{ $match : {
name: "Hello",
"values.date": {
$lt: "2016-11-03T07:00:00.000Z",
$gt: "2016-10-28T07:00:00.000Z" }
}},
{ $unwind : "$values" },
{ $match : {
name: "Hello",
"values.date": {
$lt: "2016-11-03T07:00:00.000Z",
$gt: "2016-10-28T07:00:00.000Z" }
}}
)
Sample output:
{
"_id":ObjectId("5845432720ce37bdc7e9ca1c"),
"name":"Hello",
"values":{
"value":1568,
"date":"2016-10-29T07:00:00.000Z",
"_id":"58453abfef7aaa15ac1fdee7"
}
},{
"_id":ObjectId("5845432720ce37bdc7e9ca1c"),
"name":"Hello",
"values":{
"value":1547,
"date":"2016-10-30T07:00:00.000Z",
"_id":"58453abfef7aaa15ac1fdee6"
}
},{
"_id":ObjectId("5845432720ce37bdc7e9ca1c"),
"name":"Hello",
"values":{
"value":1497,
"date":"2016-10-31T07:00:00.000Z",
"_id":"58453abfef7aaa15ac1fdee5"
}
},{
"_id":ObjectId("5845432720ce37bdc7e9ca1c"),
"name":"Hello",
"values":{
"value":3031,
"date":"2016-11-01T07:00:00.000Z",
"_id":"58453abfef7aaa15ac1fdee4"
}
},{
"_id":ObjectId("5845432720ce37bdc7e9ca1c"),
"name":"Hello",
"values":{
"value":2559,
"date":"2016-11-02T07:00:00.000Z",
"_id":"58453abfef7aaa15ac1fdee3"
}
}