how to lookup deeply nested mongodb field - node.js

I want to lookup for dynamic field of object
Schema
const settingsRef = new mongoose.Schema({
onsiteCounter: { ... },
onsiteOffCounter: { ... },
});
const DemoSchema = new mongoose.Schema({
settings: {
type: settingsRef,
default: {},
}
});
mongoose.model(Demo, DemoSchema);
Schema on which I'm aggregating
const OtherSchema = new mongoose.Schema({
siteLocation: {
type: String,
required: true,
}
})
mongoose.model(Other, OtherSchema);
What should be exact code here : "$eq": ["$settings", "$$serviceType"]
query
Other.aggregate([{
$lookup: {
"from": Demo.collection.collectionName,
"let": {
"serviceType": "$siteLocation"
},
"pipeline": [
{
"$match": {
"$expr": {
"$eq": ["$settings", "$$serviceType"]
}
}
},
{
"$project": {
isAutoServiceChargeActive: 1,
serviceChargeType: 1,
serviceChargeAmount: 1,
serviceChargeCutOff: 1,
absoluteMinOrder: 1,
preSetTipType: 1,
preSetTipAmount: 1,
isAutoAcceptActive: 1
}
}
],
"as": "serviceTypeDetails"
}
}])
I'm getting serviceTypeDetails as empty array []. Thank you so much for help in advance!

Related

How query for elements in array of subdocuments

This is mongoose schema of route model
const routeSchema = new mongoose.Schema({
route: {
type: [{
stationCode: {
type: String,
required: true,
uppercase: true,
validate: {
validator: async function(val) {
const doc = await Station.findOne({
code: val,
});
if (!doc) return false;
return true;
},
message: `A Station with code {VALUE} not found`,
},
},
distanceFromOrigin: {
type: Number,
required: [
true,
'A station must have distance from origin, 0 for origin',
],
},
}, ],
validate: {
validator: function(val) {
return val.length >= 2;
},
message: 'A Route must have at least two stops',
},
},
}, {
toJSON: {
virtuals: true
},
toObject: {
virtuals: true
},
});
This schema has a field route as array of documents,
each document has a stationCode,
I want to query for all the documents which has "KMME" and "ASN" stationCode in the specified order.
Below is example of a document created with this schema
{
"_id": {
"$oid": "636957ce994af955df472ebc"
},
"route": [{
"stationCode": "DHN",
"distanceFromOrigin": 0,
"_id": {
"$oid": "636957ce994af955df472ebd"
}
},
{
"stationCode": "KMME",
"distanceFromOrigin": 38,
"_id": {
"$oid": "636957ce994af955df472ebe"
}
},
{
"stationCode": "ASN",
"distanceFromOrigin": 54,
"_id": {
"$oid": "636957ce994af955df472ebf"
}
}
],
"__v": 0
}
Please suggest a query for this problem or another schema definition for this problem
One simple option is:
db.collection.aggregate([
{$match: {$expr: {$setIsSubset: [["ASN", "KMME"], "$route.stationCode"]}}},
{$set: {
wanted: {$first:{
$filter: {
input: "$route",
cond: {$in: ["$$this.stationCode", ["ASN", "KMME"]]}
}
}}
}},
{$match: {"wanted.stationCode": "KMME"}},
{$unset: "wanted"}
])
See how it works on the playground example

Combine geoQuery with rating aggregation

I want to get all objects in a radius and also for each single of those objects their average rating and total ratings. I've got both queries working but I'm looking to combine these 2 into one.
LocationSchema
const LocationObject = new Schema({
name: String
location: {
type: {
type: String,
enum: ['Point'],
default: 'Point',
required: true
},
coordinates: {
type: [Number],
required: true
}
}
})
ratingSchema
const Rating = new Schema({
locationObject: { type: Schema.Types.ObjectId, ref: 'LocationObject' },
average: Number,
})
locationQuery
const objects = await LocationObject.find({
location: {
$geoWithin: {
$centerSphere: [[lon, lat, radius]
}
}
})
RatingAggregation for single LocationObject
const result = await Rating.aggregate([
{
"$match": {
"locationObject": objectID
}
},
{
"$facet": {
"numbers": [
{
"$group": {
"_id": null,
"totalRating": {
"$sum": "$average"
},
"totalItemCount": {
"$sum": 1.0
}
}
}
],
}
},
{
"$unwind": "$numbers"
},
{
"$project": {
"_id": null,
"avgRating": {"$divide": ["$numbers.totalRating", "$numbers.totalItemCount"]},
"totalRatings": "$numbers.totalItemCount"
}
}
])
The final result should return an array with locationObjects which each has an average and totalRatings added.
mongo playground: https://mongoplayground.net/p/JGuJtB5bZV4
Expected result
[
{
name: String,
location: {
coordinates: [Number, Number],
},
avgRating: Number,
totalRatings: Number
},
{
name: String,
location: {
coordinates: [Number, Number],
}
}
]
As per your latest playground, you could achieve using this
db.locationObject.aggregate([
{
"$match": {
"location": {
"$geoWithin": {
"$centerSphere": [
[
6.064953,
52.531348
],
0.0012
]
}
}
}
},
{
"$lookup": { //You need to bring both the collection data together
"from": "Rating",
"localField": "_id",
"foreignField": "locationObject",
"as": "locRatings"
}
},
{
$unwind: "$locRatings"
},
{
"$group": { //you can simplify the other pipelines
"_id": "$_id",
"field": {
"$avg": "$locRatings.average"
},
"totalItemCount": {
"$sum": 1.0
}
}
}
])
To preserve the document fields, you need to use accumulators as in this playground
{
"$group": {
"_id": "$_id",
"field": {
"$avg": "$locRatings.average"
},
"totalItemCount": {
"$sum": 1.0
},
"locations": {
"$addToSet": "$location"
}
}
}
you can keep empty/null arrays in unwind stage as below
playground
{
$unwind: {
"path": "$locRatings",
"preserveNullAndEmptyArrays": true
}
},
You can add a project stage to ignore null values if needed.

Mongodb mongoose agregation and getting length of array [duplicate]

I am VERY close to getting what I want out of this query... but I only want SOME of the fields returned and right now it is returning all of them
NOTE: This is a refinement : I am now asking how to return only certain fields, while my similar question asks how to return the data between a start and end date
In addition, can somebody please please provide an answer using the MongoDB Playground with MY data sets so I can try it out... I can't quite figure out how to "name" the data sets so they work in the playground !
Register Schema
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const RegisterSchema = new Schema({
userId: {type: Schema.Types.ObjectId, required: true},
accessToken: {type:String, required: true, default: null},
})
module.exports = Register = mongoose.model( 'register', RegisterSchema)
Here is some register data
[
{
"_id": "5eac9e815fc57b07f5d0d29f",
"userId": "5ea108babb65b800172b11be",
"accessToken": "111"
},
{
"_id": "5ecaeba3c7b910d3276df839",
"userId": "5e6c2dddad72870c84f8476b",
"accessToken": "222"
}
]
The next document contains data that is related to the Register schema via the accessToken
Notifications
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const NotificationSchema = new Schema({
accessToken: {type:String, required: true},
summaryId: {type:Number, required: true},
dateCreated: {type: Date, default: Date.now},
// I don't want these returned in the final results
dontWantThis1: {type:Number, required: true},
dontWantThis2: {type:Number, required: true},
})
module.exports = Notification = mongoose.model( 'notification', NotificationSchema)
Here is some notification data
[{
"_id": "5ebf0390c719e60004f42e74",
"accessToken": "111",
"summaryId": 1111,
"dontWantThis1": 61,
"dontWantThis2": 62,
"dateCreated": "2020-04-17T00:00:00.000+00:00" },
{
"_id": "6ebf0390c719e60004f42e76",
"accessToken": "222",
"summaryId": 2221,
"dontWantThis1": 71,
"dontWantThis2": 72,
"dateCreated": "2020-04-18T00:00:00.000+00:00" },
{
"_id": "6ebf0390c719e60004f42e78",
"accessToken": "111",
"summaryId": 1112,
"dontWantThis1": 611,
"dontWantThis2": 622,
"dateCreated": "2020-05-25T00:00:00.000+00:00" },
{
"_id": "6ebf0390c719e60004f42e80",
"accessToken": "222",
"summaryId": 2222,
"dontWantThis1": 711,
"dontWantThis2": 722,
"dateCreated": "2020-05-26T00:00:00.000+00:00" }
]
Works, returns data between the two dates, but
This code returns everything, including the 'dontWantThis1' and 'dontWantThis2'
NOTE
I do not want the fields prefaced with 'dontWantThis' - but that is only to show which ones I don't want... I don't literally want to exclude fields prefaced with 'dontWantThis' ..... they could be named 'foo' or 'apple' or 'dog' they are just named that way to indicate that I don't want them
// make sure the input dates are REALLY date objects
// I only want to see notifications for the month of May (in this example)
var dateStart = new Date('2020-05-01T00:00:00.000+00:00');
var dateEnd = new Date('2020-05-30T00:00:00.000+00:00');
var match = {$match: { userId: mongoose.Types.ObjectId(userId) } };
var lookup ={
$lookup:
{
from: "my_Notifications",
localField: "accessToken",
foreignField: "accessToken",
as: "notifications"
}
};
var dateCondition = { $and: [
{ $gte: [ "$$item.dateCreated", dateStart ] },
{ $lte: [ "$$item.dateCreated", dateEnd ] }
]}
var project = {
$project: {
notifications: {
$filter: {
input: "$notifications",
as: "item",
cond: dateCondition
} } }
};
var agg = [
match,
lookup,
project
];
Register.aggregate(agg)
.then( ..... )
Try 1
I thought I could do something like this, but it still returns ALL of the notification fields
var project = {
$project: {
"_id": 1,
"userId": 1,
"accessToken":1,
"count":{$size:"$notifications"},
"notifications._id":1,
"notifications.summaryId": 1,
"notifications.dateCreated":1,
notifications : {
$filter: {
input: "$notifications",
as: "item",
cond: dateCondition
},
}}
};
SOLUTION
I created another projection and added that to the pipeline:
var project2 = {
$project: {
"_id": 1,
"userId": 1,
"accessToken":1,
"count":{$size:"$notifications"},
"notifications._id":1,
"notifications.summaryId": 1,
"notifications.dateCreated":1,
"notifications.dateProcessed":1,
}
};
var agg = [
match,
lookup,
project,
project2,
];
Thanks!!
https://stackoverflow.com/users/6635464/ngshravil-py was spot on.
I created another projection:
var project2 = {
$project: {
"_id": 1,
"userId": 1,
"accessToken":1,
"count":{$size:"$notifications"},
"notifications._id":1,
"notifications.summaryId": 1,
"notifications.dateCreated":1,
"notifications.dateProcessed":1,
}
};
Then added it to my aggregation pipeline:
var agg = [
match,
lookup,
project,
project2,
];
Worked ! -- thank you https://stackoverflow.com/users/6635464/ngshravil-py
You need to convert notifications.dateCreated to ISODate, as your date is in string, by using $dateFromString and $map operator. i suggest you to do this, because I don't think that you can do date comparison with string formats. Also, make sure that dateStart and dateEnd should also be in ISODate format.
And you need two $project operator in order to achieve this. Also, I don't see any field with userAccessToken, I assume, it's accessToken. Check the below query.
db.Register.aggregate([
{
$lookup: {
from: "my_Notifications",
localField: "accessToken",
foreignField: "accessToken",
as: "notifications"
}
},
{
$project: {
"_id": 1,
"userId": 1,
"accessToken": 1,
notifications: {
$map: {
input: "$notifications",
as: "n",
in: {
"_id": "$$n._id",
"summaryId": "$$n.summaryId",
"dateCreated": {
$dateFromString: {
dateString: "$$n.dateCreated"
}
}
}
}
}
}
},
{
$project: {
"userId": 1,
"accessToken": 1,
"notifications": {
$filter: {
input: "$notifications",
as: "item",
cond: {
$and: [
{
$gte: [
"$$item.dateCreated",
ISODate("2020-05-24T00:00:00Z")
]
},
{
$lte: [
"$$item.dateCreated",
ISODate("2020-05-26T00:00:00Z")
]
}
]
}
}
}
}
},
{
$set: {
"count": {
$size: "$notifications"
}
}
}
])
MongoPlayGroundLink

Querying nested objects using find not working in mongoose (MongoDB)

I'm trying to get an object which has isDraft value true, but I'm also getting objects which have isDraft value false. I need only objects having isDraft value true. I have tried all possible ways but am not able to find a solution for this. Can anyone help me with this?
Below are the schema, query and response.
Schema
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const Contract = new Schema({
name: {
type: String,
unqiue: true,
required: true
},
version: [
{
no: {
type: Number,
required: true
},
sections: [
{
sectionName: {
type: String,
required: true
},
clause: [{
description: {
type: String,
required: true
},
}]
}
],
approvedBy: [
{
user: {
type: Schema.Types.ObjectId,
ref: 'user'
},
}
],
acceptedBy: [
{
name: {
type: String,
},
eamil: {
type: String,
},
}
],
isDraft: {
type: Boolean,
required: true
},
date: {
type: Date,
default: Date.now
}
}
],
createdBy: {
type: Schema.Types.ObjectId,
ref: 'user',
required: true
},
});
module.exports = mongoose.model('contract', Contract);
Query
query = {
$and: [
{ createdBy: clientAdminDetails._id },
{ "version.isDraft": true }
],
};
await Contract
.find(query)
.skip(req.body.noOfItems * (req.body.pageNumber - 1))
.limit(req.body.noOfItems)
.exec((err, contract) => {
if (err) {
return res.json(err);
}
Contract.countDocuments(query).exec((count_error, count) => {
if (err) {
return res.json(count_error);
}
return res.json({
total: count,
page: req.body.pageNumber,
pageSize: contract.length,
contracts: contract
});
});
});
Response
{
"total": 1,
"page": 1,
"pageSize": 1,
"contracts": [
{
"_id": "61449469775..",
"name": "Octavia Blankenship",
"version": [
{
"_id": "614496593cc..",
"sections": [
{
"_id": "61449469775..",
"sectionName": "Est dolore dolorem n Updated `1323",
"clause": [
{
"_id": "614494697..",
"description": "Numquam nostrud et a"
}
]
}
],
"isDraft": false,
"no": 1,
"approvedBy": [],
"acceptedBy": [],
"date": "2021-09-17T13:21:29.509Z"
},
{
"_id": "614496122904ee4e046fbee8",
"sections": [
{
"_id": "6144955a8c0061025499606f",
"sectionName": "Praesentium suscipit",
"clause": [
{
"_id": "6144955a8c00610254996070",
"description": "Velit aperiam ut vel"
}
]
}
],
"isDraft": true,
"no": 2,
"approvedBy": [],
"acceptedBy": [],
"date": "2021-09-17T13:20:18.128Z"
}
],
"createdBy": "614367e980b29e6c...",
"__v": 0
}
]
}
This is why using your query you are telling mongo "Give me a document where createdBy is desired id and version.isdraft is true" So, as the DOCUMENT contains both values, is returned, even existing false into the array.
To solve this you have many ways.
First one is using $elemMatch into projection (docs here). But using this way only the first element is returned, so I think you prefer other ways.
So you can use an aggregation query using $filter like this:
First $match by values you want (as in your query).
Then override version array filtering by values where isDraft = true.
db.collection.aggregate([
{
"$match": {
"createdBy": "",
"version.isDraft": true
}
},
{
"$set": {
"version": {
"$filter": {
"input": "$version",
"as": "v",
"cond": {
"$eq": [
"$$v.isDraft",
true
]
}
}
}
}
}
])
Example here

Mongoose: Calculate number of distinct field values in array returned as result of $lookup aggregation step

I'm struggling with a problem of how to get the number of distinct field values in array returned as result of $lookup aggregation step in MongoDB using Mongoose. By the number of distinct field values I mean the number of rows with unique value on certain field.
Parent document has this structure:
{ _id: 678, name: "abc" }
Child document has this structure:
{ _id: 1009, fieldA: 123, x: { id: 678, name: "abc" } }
$lookup step is defined as follow:
{
from "children",
localField: "_id"
foreignField: "x.id"
as: "xyz"
}
Let's assume that I get this array as a result of $lookup aggregation step for a parent with _id equal to: 678
xyz: [
{ _id: 1009, fieldA: 123, x: { id: 678, name: "abc" } },
{ _id: 1010, fieldA: 3435, x: { id: 678, name: "abc" } },
{ _id: 1011, fieldA: 123, x: { id: 678, name: "abc" } }
]
I want to know how many distinct fieldA values are in this array. In this example it would be 2.
Of coure the step should be in aggregation flow, ater $lookup step and before (inside?) $project step. As a side note I must to add that I also need total number of elements in array xyz as another value ($size operator in $project step).
So given what you are saying, then you would basically have some data like this:
parents
{
"_id": 1,
"xyz": ["abc", "abd", "abe", "abf"]
}
children
{ "_id": "abc", "fieldA": 123 },
{ "_id": "abd", "fieldA": 34 },
{ "_id": "abe", "fieldA": 123 },
{ "_id": "abf", "fieldA": 54 }
N.B. If you actually defined the parent reference within the child instead of an array of child references in the parent, then there is a listing example at the bottom. The same principles generally apply in either case however.
Where your current $lookup that produces a result like that in the question would be something like this:
{ "$lookup": {
"from": "children",
"localField": "xyz",
"foreignField": "_id"
"as": "xyz"
}}
Best Approach
Now you could do other operations on the array returned in order to actually return the total count and distinct counts, but there is a better way with any modern MongoDB release which you should be using. Namely there is a more expressive form of $lookup which allows a pipeline to be specified to act on the resulting children:
Parent.aggregate([
{ "$lookup": {
"from": "children",
"let": { "ids": "$xyz" },
"pipeline": [
{ "$match": {
"$expr": { "$in": [ "$_id", "$$ids" ] }
}},
{ "$group": {
"_id": "$fieldA",
"total": { "$sum": 1 }
}},
{ "$group": {
"_id": null,
"distinct": { "$sum": 1 },
"total": { "$sum": "$total" }
}}
],
"as": "xyz"
}},
{ "$addFields": {
"xyz": "$$REMOVE",
"distinctCount": { "$sum": "$xyz.distinct" },
"totalCount": { "$sum": "$xyz.total" }
}}
])
The whole point there being that you don't actually need all the array results to be returned from the $lookup, so instead of working with the returned array of all matching children you just reduce that content from within the pipeline expression of the $lookup.
In order to get a total count and a distinct count for the inner content, after the initial $match conditions which specify the "join" and what matches to return, you would then $group on the "distinct" value as the key and maintain a "count" of the elements found in total. The second $group uses a null value for the key since the only thing you want now is the count of the distinct keys already returned, and of course return the $sum of the existing total of counted elements.
The result being of course:
{
"_id": 1,
"distinctCount": 3,
"totalCount": 4
}
And since we are using $addFields this would be in addition to all other fields present in the parent document with the exception of xyz which we explicitly removed via the $$REMOVE operator.
You might also note the usage of $sum in that last stage. The actual result of our $lookup pipeline is of course a single document, but it is as always within an array, since that is what the output of $lookup always is. In this case it's just a very simple way ( being the shortest syntax ) to just extract those values from the array as individual fields in the parent document instead.
Alternate
The alternate approach is of course to just work with the returned array, and all this really needs is essentially any of the appropriate "set operators" and the $size operator:
Parent.aggregate([
{ "$lookup": {
"from": "children",
"localField": "xyz",
"foreignField": "_id",
"as": "xyz"
}},
{ "$addFields": {
"xyz": "$$REMOVE",
"distinctCount": { "$size": { "$setUnion": [ [], "$xyz.fieldA" ] }},
"totalCount": { "$size": "$xyz" }
}}
])
Here we use $setUnion basically providing arguments of an empty array [] and the array of fieldA values. Since this would return a "set" that is the combination of both arguments, the one thing that defines a "set" is that the values can appear only once and are thus *distinct. This is a quick way of obtaining only the distinct values and then of course each "array" ( or "set" ) is simply measured by $size for their respective counts.
So it "looks simple" but the problem is that it's not really efficient, and mostly because we spent operational time returning those array values from the $lookup and then we basically discarded the result. This is generally why the former approach is preferred since it will actually reduce the result before it is ever returned as an array. So "less work" overall.
If on the other hand you actually want to keep the array returned from the $lookup result then the latter case would be of course more desirable
Example listing
const { Schema } = mongoose = require('mongoose');
const uri = 'mongodb://localhost:27017/test';
const options = { useNewUrlParser: true, useUnifiedTopology: true };
mongoose.set('debug', true);
mongoose.set('useFindAndModify', false);
mongoose.set('useCreateIndex', true);
const parentSchema = new Schema({
_id: Number,
xyz: [{ type: String, ref: 'Child' }]
},{ _id: false });
parentSchema.index({ "xyz": 1 });
const childSchema = new Schema({
_id: String,
fieldA: Number
},{ _id: false });
const Parent = mongoose.model('Parent', parentSchema);
const Child = mongoose.model('Child', childSchema);
const log = data => console.log(JSON.stringify(data, undefined, 2));
(async function() {
try {
const conn = await mongoose.connect(uri, options);
// Clean data for demonstration
await Promise.all(
Object.values(conn.models).map(m => m.deleteMany())
);
// Insert some data
await Parent.create({ "_id": 1, "xyz": ["abc", "abd", "abe", "abf"] });
await Child.insertMany([
{ "_id": "abc", "fieldA": 123 },
{ "_id": "abd", "fieldA": 34 },
{ "_id": "abe", "fieldA": 123 },
{ "_id": "abf", "fieldA": 54 }
]);
let result1 = await Parent.aggregate([
{ "$lookup": {
"from": Child.collection.name,
"let": { "ids": "$xyz" },
"pipeline": [
{ "$match": {
"$expr": { "$in": [ "$_id", "$$ids" ] }
}},
{ "$group": {
"_id": "$fieldA",
"total": { "$sum": 1 }
}},
{ "$group": {
"_id": null,
"distinct": { "$sum": 1 },
"total": { "$sum": "$total" }
}}
],
"as": "xyz"
}},
{ "$addFields": {
"xyz": "$$REMOVE",
"distinctCount": { "$sum": "$xyz.distinct" },
"totalCount": { "$sum": "$xyz.total" }
}}
]);
log({ result1 });
let result2 = await Parent.aggregate([
{ "$lookup": {
"from": Child.collection.name,
"localField": "xyz",
"foreignField": "_id",
"as": "xyz"
}},
{ "$addFields": {
"xyz": "$$REMOVE",
"distinctCount": { "$size": { "$setUnion": [ [], "$xyz.fieldA" ] } },
"totalCount": { "$size": "$xyz" }
}}
]);
log({ result2 })
} catch(e) {
console.error(e);
} finally {
mongoose.disconnect();
}
})()
And the output:
Mongoose: parents.createIndex({ xyz: 1 }, { background: true })
Mongoose: parents.deleteMany({}, {})
Mongoose: children.deleteMany({}, {})
Mongoose: parents.insertOne({ xyz: [ 'abc', 'abd', 'abe', 'abf' ], _id: 1, __v: 0 }, { session: null })
Mongoose: children.insertMany([ { _id: 'abc', fieldA: 123, __v: 0 }, { _id: 'abd', fieldA: 34, __v: 0 }, { _id: 'abe', fieldA: 123, __v: 0 }, { _id: 'abf', fieldA: 54, __v: 0 }], {})
Mongoose: parents.aggregate([ { '$lookup': { from: 'children', let: { ids: '$xyz' }, pipeline: [ { '$match': { '$expr': { '$in': [ '$_id', '$$ids' ] } } }, { '$group': { _id: '$fieldA', total: { '$sum': 1 } } }, { '$group': { _id: null, distinct: { '$sum': 1 }, total: { '$sum': '$total' } } } ], as: 'xyz' } }, { '$addFields': { xyz: '$$REMOVE', distinctCount: { '$sum': '$xyz.distinct' }, totalCount: { '$sum': '$xyz.total' } } }], {})
{
"result1": [
{
"_id": 1,
"__v": 0,
"distinctCount": 3,
"totalCount": 4
}
]
}
Mongoose: parents.aggregate([ { '$lookup': { from: 'children', localField: 'xyz', foreignField: '_id', as: 'xyz' } }, { '$addFields': { xyz: '$$REMOVE', distinctCount: { '$size': { '$setUnion': [ [], '$xyz.fieldA' ] } }, totalCount: { '$size': '$xyz' } } }], {})
{
"result2": [
{
"_id": 1,
"__v": 0,
"distinctCount": 3,
"totalCount": 4
}
]
}
Example without child array in parent
Shows defining a schema without an array of values in the parent and instead defining the parent reference within all children:
const { Schema } = mongoose = require('mongoose');
const uri = 'mongodb://localhost:27017/test';
const options = { useNewUrlParser: true, useUnifiedTopology: true };
mongoose.set('debug', true);
mongoose.set('useFindAndModify', false);
mongoose.set('useCreateIndex', true);
const parentSchema = new Schema({
_id: Number,
},{ _id: false });
parentSchema.virtual("xyz", {
ref: 'Child',
localField: '_id',
foreignField: 'parent',
justOne: false
});
const childSchema = new Schema({
_id: String,
parent: Number,
fieldA: Number
},{ _id: false });
childSchema.index({ "parent": 1 });
const Parent = mongoose.model('Parent', parentSchema);
const Child = mongoose.model('Child', childSchema);
const log = data => console.log(JSON.stringify(data, undefined, 2));
(async function() {
try {
const conn = await mongoose.connect(uri, options);
// Clean data for demonstration
await Promise.all(
Object.values(conn.models).map(m => m.deleteMany())
);
// Insert some data
await Parent.create({ "_id": 1 });
await Child.insertMany([
{ "_id": "abc", "fieldA": 123 },
{ "_id": "abd", "fieldA": 34 },
{ "_id": "abe", "fieldA": 123 },
{ "_id": "abf", "fieldA": 54 }
].map(e => ({ ...e, "parent": 1 })));
let result1 = await Parent.aggregate([
{ "$lookup": {
"from": Child.collection.name,
"let": { "parent": "$_id" },
"pipeline": [
{ "$match": {
"$expr": { "$eq": [ "$parent", "$$parent" ] }
}},
{ "$group": {
"_id": "$fieldA",
"total": { "$sum": 1 }
}},
{ "$group": {
"_id": null,
"distinct": { "$sum": 1 },
"total": { "$sum": "$total" }
}}
],
"as": "xyz"
}},
{ "$addFields": {
"xyz": "$$REMOVE",
"distinctCount": { "$sum": "$xyz.distinct" },
"totalCount": { "$sum": "$xyz.total" }
}}
]);
log({ result1 });
let result2 = await Parent.aggregate([
{ "$lookup": {
"from": Child.collection.name,
"localField": "_id",
"foreignField": "parent",
"as": "xyz"
}},
{ "$addFields": {
"xyz": "$$REMOVE",
"distinctCount": { "$size": { "$setUnion": [ [], "$xyz.fieldA" ] } },
"totalCount": { "$size": "$xyz" }
}}
]);
log({ result2 })
} catch(e) {
console.error(e);
} finally {
mongoose.disconnect();
}
})()
And the output:
Mongoose: children.createIndex({ parent: 1 }, { background: true })
Mongoose: parents.deleteMany({}, {})
Mongoose: children.deleteMany({}, {})
Mongoose: parents.insertOne({ _id: 1, __v: 0 }, { session: null })
Mongoose: children.insertMany([ { _id: 'abc', fieldA: 123, parent: 1, __v: 0 }, { _id: 'abd', fieldA: 34, parent: 1, __v: 0 }, { _id: 'abe', fieldA: 123, parent: 1, __v: 0 }, { _id: 'abf', fieldA: 54, parent: 1, __v: 0 }], {})
Mongoose: parents.aggregate([ { '$lookup': { from: 'children', let: { parent: '$_id' }, pipeline: [ { '$match': { '$expr': { '$eq': [ '$parent', '$$parent' ] } } }, { '$group': { _id: '$fieldA', total: { '$sum': 1 } } }, { '$group': { _id: null, distinct: { '$sum': 1 }, total: { '$sum': '$total' } } } ], as: 'xyz' } }, { '$addFields': { xyz: '$$REMOVE', distinctCount: { '$sum': '$xyz.distinct' }, totalCount: { '$sum': '$xyz.total' } } }], {})
{
"result1": [
{
"_id": 1,
"__v": 0,
"distinctCount": 3,
"totalCount": 4
}
]
}
Mongoose: parents.aggregate([ { '$lookup': { from: 'children', localField: '_id', foreignField: 'parent', as: 'xyz' } }, { '$addFields': { xyz: '$$REMOVE', distinctCount: { '$size': { '$setUnion': [ [], '$xyz.fieldA' ] } }, totalCount: { '$size': '$xyz' } } }], {})
{
"result2": [
{
"_id": 1,
"__v": 0,
"distinctCount": 3,
"totalCount": 4
}
]
}
I ended up following first approach suggested by #Neil Lunn. Due to a fact that my schemas of parent and child are different from those assumed by #Neil Lunn I post my own answer whitch solves my particular problem:
Parent.aggregate([
{
$lookup: {
from: "children",
let: { id: "$_id" },
pipeline: [
{ $match: { $expr: { $eq: ["$x.id", "$$id"] } } },
{
$group: {
_id: "$fieldA",
count: { $sum: 1 }
}
},
{
$group: {
_id: null,
fieldA: { $sum: 1 },
count: { $sum: "$count" }
}
}
],
as: "children"
}
},
{
$project: {
total: { $sum: "$children.count" },
distinct: { $sum: "$children.fieldA" }
}
}
]);

Resources