MongoDB/Mongoose - Querying an array of objects by date - node.js

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"
}
}

Related

Get Total Count using aggregate + facet using Mongo

I want to group my data based on event date with pagination. However what i am getting is whole record totalcount instead of eventDate count. because of this UI part is not working properly. Here is my collection sample:
{
"_id" : ObjectId("5fc4d0009a25e8cfbe306381"),
"eventDate" : ISODate("2021-11-29T01:00:00.000Z"),
"team1" : {
"tName" : "Chicago Bears",
},
"team2" : {
"tName" : "Green Bay Packers",
}
}
{
"_id" : ObjectId("5fc4d0019a25e8cfbe3063ff"),
"eventDate" : ISODate("2021-11-30T01:00:00.000Z"),
"team1" : {
"tName" : "Nashville SC",
},
"team2" : {
"tName" : "Columbus Crew",
}
}
{
"_id" : ObjectId("5fc4d0019a25e8cfbe3063f4"),
"eventDate" : ISODate("2021-11-30T01:00:00.000Z"),
"team1" : {
"tName" : "yyyy",
},
"team2" : {
"tName" : "xxxx",
}
}
here is my query:
db.getCollection('game').aggregate([
{ $addFields: { "newEventDate": {$dateToString:{ format: "%Y-%m-%d", date: "$eventDate" }}}},
{ "$match": {
"eventDate": { $gte: new Date() }
}
},
{ "$facet": {
"resultData": [
{ "$match": {
"eventDate": { $gte: new Date() }
}
},
{ "$group": {
"_id": "$newEventDate",
"games": {$push: {
team1:"$team1",
team2:"$team2"
}}
}
},
{ $sort: {eventDate: 1} },
{
$limit: 1
}
],
"pageInfo": [
{ "$count": "totalRecords" }
]}
}
]);
After executing this query this is my response:
{
"resultData" : [
{
"_id" : "2021-11-29",
"games" : [
{
"awayTeam" : {
"tName" : "Chicago Bears"
},
"homeTeam" : {
"tName" : "Green Bay Packers"
}
},
]
}
],
"pageInfo" : [
{
"totalRecords" : 3 **[here i want 2 ie total event date]**
}
]
}
$match your condition
move your $group stage outside from $facet, convert your date from string inside group, add you date in group stage because we are going to sort in next stage
$sort by eventDate ascending order
$facet, first get single record using $limit, and second part get total count of the record using $count
db.collection.aggregate([
{ $match: { eventDate: { $gte: new Date() } } },
{
$group: {
_id: {
$dateToString: {
format: "%Y-%m-%d",
date: "$eventDate"
}
},
eventDate: { $first: "$eventDate" },
games: {
$push: {
team1: "$team1",
team2: "$team2"
}
}
}
},
{ $sort: { eventDate: 1 } },
{
$facet: {
resultData: [{ $limit: 1 }],
pageInfo: [{ $count: "totalRecords" }]
}
}
])
Playground

mongodb update query in aggregation from multiple collections without grouping

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);
});
});
});

$out ,$projection in mongodb

i want to save records in a new collection using either $out or $merge.
**/////collection 2- reservationdeatils////**
"_id":ObjectId("5e4a898947363e964a886420"),
"phoneNo" : 98765#####,
"name" : "name1",
"userId":ObjectId("5e1efac668c3c811c83263cc"),
"approversId":ObjectId("5e1efad268c3c811c83263cd")
"bookedForDate":ISODate("2020-02-20T07:23:36.130Z"),
"bookingDetails" : [
{ "_id" : ObjectId("5e44f471d1868d2a54aac12d"),
"seatsBooked" : 15,
"floorId" : "#IKE01",
},
{ "_id" : ObjectId("5e44f471d1868d2a54aac12c"),
"seatsBooked" : 35,
"floorId" : "#HKE04",
}
],
**/////collection 2-priceDetails////**
{
"_id" : ObjectId("5e1efb0168c3c811c83263ce"),
"floorId" : "#IKE01",
"weekday" : "monday",
"pricePoint" : 589,
}
{
"_id" : ObjectId("5e2694db54e532a4eb92b477"),
"floorId" : "#IKE02",
"weekday" : "thursday",
"pricePoint" : 699
}
{
"_id" : ObjectId("5e2694f954e532a4eb92b478"),
"floorId" : "#HKE04",
"weekday" : "monday",
"pricePoint" : 579
}
**/////collection 3- discount////**
{
"_id" : ObjectId("5e427de64617181a4ce38893"),
"userId" : ObjectId("5e3d05ba964d0e06c4bb0f07"),
"approversId" : ObjectId("5e1d82156a67173cb877f67d"),
"floorId" : "#IKE01",
"weekday" : "monday",
"discount" : 20%,
},
{
"_id" : ObjectId("5e4281e7fec2e01a4c60b406"),
"userId" : ObjectId("5e1efac668c3c811c83263cc"),
"approversId" : ObjectId("5e1efad268c3c811c83263cd"),
"floorId" : "#IKE01",
"weekday" : "monday",
"discount" : 24%,
}
Now below is the query i have tried :
db.reservationdeatils.aggregate([
{
'$match': {
'approverId': ObjectId('5e1efad268c3c811c83263cd'),
'userId': ObjectId('5e1efac668c3c811c83263cc'),
'bookedForDate': ISODate("2020-02-11T18:30:00Z"),
}
},
{
'$unwind': {
'path': '$bookingDetails',
},
},
{
$lookup:
{
from: 'priceDetails',
let: { floorId: '$bookingDetails.floorId' },
pipeline: [
{
$match: {
weekday: 'monday',
$expr: {
$eq: ["$floorId", "$$floorId"]
}
}
}
], as: 'priceDetails'
}
},
{ '$unwind': '$priceDetails' },
{
$lookup:
{
from: 'discount',
let: { floorId: '$bookingDetails.floorId' },
pipeline: [
{
$match: {
weekday: 'monday',
$expr: {
$eq: ["$floorId", "$$floorId"]
}
}
}
], as: 'discounts'
}
},
{ '$unwind': '$discounts' },
{
'$group': {
'_id': {
'floorId': '$bookingDetails.floorId',
'date': '$bookedForDate',
'price': '$priceDetails.pricePoint',
'discount': '$discounts.discount'
},
'seatsBooked': {
'$sum': '$bookingDetails.seatsBooked'
},
}
},
{
'$project': {
'amount': {
'$multiply':
[
'$seatsBooked',
{'$subtract':
['$_id.pricePoint',
{ '$multiply':
['$_id.pricePoint',
{ '$divide':
['$_id.discount', 100]
}]
}]
}]
},
},
},
{
$group: {
_id: null,
totalAmount: {
$sum: "$amount"
}
}
},
{
'$project': {
_id:0,
totalAmount:1,
bookedForDate:1,
'floorId':'$priceDetails.floorId'
}
},{'$merge':'invoice'}
]).pretty()
i have been able to achieve the totalAmount but what i want to achieve is that i want to save these fields into "invoice" collection "userId","approversId","floorId","sum","totalSum","bookedForDate","name" BUT 1:whenever i use $out instead of $merge the previous document gets replaces which i dont want, 2: if i use $merge everytime i run the query a new document is created and that too only with _id:ObjectId(5e4a899c47363e964a88642f),totalBill:#### these fields , any suggestion how can i achieve this
You are going in a good direction you just need to look at the $group aggregation.
One more thing I have used the discount as int value, not in percentage.
"discount" : 24
I have updated the query:
db.reservationdeatils.aggregate([
{
$match: {
"userId" : ObjectId("5e1efac668c3c811c83263cc"),
"approversId" : ObjectId("5e1efad268c3c811c83263cd"),
"bookedForDate" : ISODate("2020-02-20T07:23:36.130Z")
}
},
{
$unwind: {
path: "$bookingDetails",
},
},
{
$lookup:
{
from: "priceDetails",
let: { floorId: "$bookingDetails.floorId" },
pipeline: [
{
$match: {
weekday: "monday",
$expr: {
$eq: ["$floorId", "$$floorId"]
}
}
}
],
as: "priceDetails"
}
},
{
$unwind: "$priceDetails"
},
{
$lookup:
{
from: "discount",
let: { floorId: "$bookingDetails.floorId" },
pipeline: [
{
$match: {
weekday: "monday",
$expr: {
$eq: ["$floorId", "$$floorId"]
}
}
}
],
as: "discounts"
}
}
,
{
$unwind: "$discounts"
}
,
{
$group: {
"_id": {
"floorId": "$bookingDetails.floorId",
"date": "$bookedForDate",
"price": "$priceDetails.pricePoint",
"discount": "$discounts.discount"
},
"price":{
$first:"$priceDetails.pricePoint"
},
"discount":{
$first:"$discounts.discount"
},
"seatsBooked": {
$sum: "$bookingDetails.seatsBooked"
},
}
}
,
{
$project: {
"amount": {
$multiply:
[
"$seatsBooked",
{
$subtract:[
"$price",
{
$multiply:[
"$price",
{
$divide:[
"$discount",
100
]
}
]
}
]
}
]
},
},
}
,
{
$group: {
"_id": null,
"totalAmount": {
$sum: "$amount"
}
}
},
{
$project: {
"_id":0,
"totalAmount":1,
"bookedForDate":1,
"floorId":"$priceDetails.floorId"
}
},
{
$out:"invoice"
}
]).pretty()
This will help you.

MongoDB Mongoose querying a deeply nested array of subdocuments by date range

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

How to include empty array when filtering an array

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.

Resources