cross collection query in mongoose with $lookup - mongodb

I am building a question-answers database. I have three schemas, User, Question and Replies(answers). My problem begins where I want to query just the questions that the user has not already answered. but when I run this query I get the question with an empty replies array. should I populate it somehow? here's where I got:
Query:
let getQuestions = (req, res)=>{
Question.aggregate([
{
$match: {isGlobal: true}
},
{
$lookup: {
from: 'Reply',
localField: '_id',
foreignField: 'questionID',
as: 'replies'
}
},
// {
// $match: {}
// }
]).then(foundQuestions=> console.log(foundQuestions))
};
User Schema (simplified):
const mongoose = require('mongoose');
const userSchema = mongoose.Schema({
firstName: String,
lastName: String,
email: String,
questionReplies: [
{
type: mongoose.Schema.Types.ObjectId,
ref: 'Reply'
}
],
}, {timestamps: true});
module.exports = mongoose.model('User', userSchema);
Question Schema:
const mongoose = require('mongoose');
const questionSchema = mongoose.Schema({
title: String,
description: String,
isGlobal: Boolean,
options: [
{
number: Number,
title: String,
}
]
}, {timestamps: true});
module.exports = mongoose.model('Question', questionSchema);
Reply Schema:
const mongoose = require('mongoose');
const replySchema = mongoose.Schema({
questionID: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Question',
required: true
},
email: {type: String, required: true},
answerID: {type: String, required: true},
number: Number,
title: String,
}, {timestamps: true});
module.exports = mongoose.model('Reply', replySchema);
my replies collection has this document inside:
"questionID" : ObjectId("5c6f6867cbff9c2a9004eb6d")
and I have a question with this ID:
"_id" : ObjectId("5c6f6867cbff9c2a9004eb6d"),
(any opnions on improving database design are welcome too).

Try below:
let getQuestions = (req, res)=>{
Question.aggregate([
{
$match: {isGlobal: true}
},
{
$lookup: {
from: 'Reply',
localField: '_id',
foreignField: 'questionID',
as: 'replies'
}
},
{
$match: { "replies": { $eq: [] } }
}
]).then(foundQuestions=> console.log(foundQuestions))
};
So you will get question which does not have any replies.

I solved my problem and I post here for future references.
the from field used in $lookup refers to the collection name created by mongodb, not the Model name used in mongoose. So the correct query is:
$lookup: {
from: 'replies',
localField: '_id',
foreignField: 'questionID',
as: 'replies'
}
and then I added
{
$match: { "replies": { $eq: [] } }
}
for finding just the questions with no answers.

Related

Perform share calculations in MongoDB aggregation

I have an Order schema, like so:
const orderSchema = new mongoose.Schema({
order_items: [{
type: mongoose.Schema.Types.ObjectId,
ref: 'OrderItem',
required: true
}],
user: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true
}
total_price: {
type: Number
}
});
And the OrderItems contains purchased products, like so:
const orderItemSchema = new mongoose.Schema({
product_id: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Product',
required: true
},
quantity: {
type: Number,
required: true
}
});
And the Product schema like so:
const productSchema = new mongoose.Schema({
name: {
type: Map,
of: String,
required: true
},
thumbnail: {
type: String,
default: ''
},
unit_price: {
type: Number,
required: true
}
});
I'm trying to get the share of each purchased product from the total price of the order.
I tried the following:
const totalSales = await Order.aggregate([
{
$lookup: {
from: "orderitems",
localField: "order_items",
foreignField: "_id",
as: "order_items"
}
},
{
$lookup: {
from: "products",
localField: "order_items.product_id",
foreignField: "_id",
as: "products",
pipeline: []
}
},
{
$project: {
order_items: 0,
products: { $divide: ['$products.unit_price', '$total_price'] }
}
}
]);
But I got the following error in postman:
Invalid $project :: caused by :: Cannot use expression other than
$meta in exclusion projection
So, how can I get the desired output?
Thanks
Edit:
I removed order_items: 0 from the project, and now I got this error message:
PlanExecutor error during aggregation :: caused by :: $divide only
supports numeric types, not array and int
From the documentation,
If you specify the exclusion of a field other than _id, you cannot
employ any other $project specification forms.
You should remove order_items: 0, because it is anyway not included in the output.

Mongodb mongoose query get count of documents by reference

I have 2 collections in mongodb. articles and tags.
In articles, there can be multiple tags.
Following is the article schema:
const mongoose = require('mongoose');
const articleSchema = new mongoose.Schema({
title: {
type: String,
required: true,
trim: true
},
tags: [{
type: mongoose.Schema.Types.ObjectId,
required: true,
ref: 'Tag'
}]
}, {
timestamps: true
});
const Article = mongoose.model('Article', articleSchema);
module.exports = Article;
Following is the tag schema:
const mongoose = require('mongoose');
const tagSchema = new mongoose.Schema({
title: {
type: String,
required: true,
trim: true,
unique: true
}
}, {
timestamps: true
});
const Tag = mongoose.model('Tag', tagSchema);
module.exports = Tag;
From these collections I wanted to show a simple column chart which shows
how many articles are there against a tag.
I am trying to get data in format like this:
const data = [
{ title: 'Javascript', count: 20 },
{ title: 'ReactJs', count: 12 },
{ title: 'NodeJs', count: 5 }
];
I have tried aggregate, $lookup but not able to find solution.
Also tried this answer
Following I have tried but its not giving desired output.
const result = await Tag.aggregate([
{
$lookup:
{
from: "articles",
localField: "_id",
foreignField: "tags",
as: "articles"
}
}
])
It gives output like this, it returns articles array against tag but I need count of articles only.
[{
"_id": "5f6f39c64250352ec80b0e10",
"title": "ReactJS",
articles: [{ ... }, { ... }]
},{
"_id": "5f6f40325716952d08a6813c",
"title": "Javascript",
articles: [{ ... }, { ... },{ ... }, { ... }]
}]
If anyone knows solution, please let me know. Thank you.
$lookup with articles collection
$project to show required fields and get total size of articles array using $size
const result = await Tag.aggregate([
{
$lookup: {
from: "articles",
localField: "_id",
foreignField: "tags",
as: "articles"
}
},
{
$project: {
_id: 0,
title: 1,
count: { $size: "$articles" }
}
}
])
Playground

How can I use the $match operator twice in a single mongoDB query?

I have two models:
const ClientRequest = new mongoose.Schema({
sourceLanguage: {
type: String,
default: '',
trim: true
},
type: {
type: String,
default: '',
trim: true
},
customer: {
type: Schema.Types.ObjectId, ref: 'Client'
}
}
and
const Client = new mongoose.Schema({
name: {
type: String,
default: '',
trim: true
},
web: {
type: String,
default: '',
trim: true
},
country: {
type: String,
default: '',
trim: true
}
}
And I need to find all requests filtered by sourceLanguage and name.
I'm using this query:
const requests = await ClientRequest.aggregate([
{$match: {
"sourceLanguage.symbol": "EN-GB"}
},
{
$lookup: {
from: "clients",
localField: "customer",
foreignField: "_id",
as: "clients"
}
},
{
$match: {
"clients.name": filters.clientFilter,
}
}
])
But it returns empty array. If I remove one of the $match it works. But how can I use both of the filters at the same time in a single query?
const requests = await ClientRequest.aggregate([
{$match: {
"sourceLanguage": "EN-GB",
"customer": ObjectId("5d933c4b8dd2942a17fca425")
}
},
{
$lookup: {
from: "clients",
localField: "customer",
foreignField: "_id",
as: "clients"
}
},
])
I tried different approaches, but as sometimes it happens, the simpliest way worked out:
const requests = await ClientRequest.aggregate([
{
$lookup: {
from: "clients",
localField: "customer",
foreignField: "_id",
as: "customer" // I used the same name to replace the Id with the unwinded object
}
},
{
$match: {
"customer.name": filters.clientFilter,
"sourceLanguage.symbol": "EN-GB" // or any other filter
}
},
{$unwind: "$customer"} // here I just unwind from array to an object
])

Optimize aggregate query mongoose

I'm trying to optimize a consult on mongoose. First my models go like this:
var TrackSchema = new Schema({
Car: {
type: Schema.ObjectId,
ref: 'Car'
},
Imei: {
type: String,
trim: true
},
Latitude: {
type: Number
},
Longitude: {
type: Number
}
});
var CarSchema = new Schema({
Basis: {
type: Schema.ObjectId,
ref: 'Basis'
},
Imei: {
type: String,
trim: true
}
});
var BasisSchema = new Schema({
Fence: {
type: Array,
}
});
What I want to do is to get the last registers from Tracks with distinct Imei from model Car. But also I need to get the Fence field from Basis that is associate with Car.
I alredy have the query, but I want to improve it because it take some time the get it.
Track.aggregate([
{
$group: {
_id: {
'Imei': '$Imei',
'Car': '$Car',
},
Longitude : { $last: '$Longitude' },
Latitude : { $last: '$Latitude' },
Fecha: { $last: '$CreatedDate'}
},
},
{
$lookup: {
from: 'cars',
localField: '_id.Car',
foreignField: '_id',
as: 'unit'
}
},
{
$unwind: '$unit'
},
{
$lookup: {
from: 'basis',
localField: 'unit.Basis',
foreignField: '_id',
as: 'geofence'
}
}
])
.exec(function(error, results){
});

Mongoose populate and sort by length struggle

I have the following mongoose schema:
const userSchema = new mongoose.Schema({
email: { type: String, unique: true },
fragments: [{type: mongoose.Schema.Types.ObjectId, ref: 'Fragment'}]
}, { timestamps: true, collection: 'user' });
And
const fragmentSchema = new mongoose.Schema({
text: String,
owner: {type: mongoose.Schema.Types.ObjectId, ref: 'User'},
}, { timestamps: true, collection: 'fragment' });
In the data, I have a reference in the Fragment, but not in the User:
User:
{
"_id" : ObjectId("58373e571cbccb010012bfcd"),
"email" : "email#example.com",
// no "fragments": [ObjectId('58075ce37b7f2f01002b718f')] etc.
}
Fragment:
{
"_id" : ObjectId("58075ce37b7f2f01002b718f"),
"text" : "Donc, il faut changer de méthode",
"owner" : ObjectId("58075ce27b7f2f01002b717f")
}
I would like to query users sorted by the count of number of fragments, and I can't achieve this ...
First, I'd like to make this work:
User.find({_id: '58075ce27b7f2f01002b717f'})
.populate('fragments').exec(console.log)
returns
{
_id: 58075ce27b7f2f01002b717f,
email: 'bububg#hotmail.fr',
fragments: []
}
while I should have at least the above fragment included.
And regarding the sorted query, here's where I am now:
User.aggregate([
{ "$project": {
"email": 1,
"fragments": 1,
"nbFragments": { "$size": { "$ifNull": [ "$fragments", [] ] } }
}},
{ "$sort": { "nbFragments": -1 } }
], console.log)
At least it runs, but all the nbFragments fields are set to 0. This might be related to the fact that .populate('fragments') doesn't work but I can't be sure.
Thanks for the help, I did not expect so much trouble using Mongodb...
EDIT: thanks #Veeram, unfortunately your solution is not working:
User.find({}).find({_id: '58075ce27b7f2f01002b717f'}).populate('fragments').exec(console.log)
[ { _id: 58075ce27b7f2f01002b717f,
email: 'email#example.com',
// no fragments entry
} ]
while I updated my schema:
userSchema.virtual('fragments', {
ref: 'Fragment',
localField: '_id',
foreignField: 'owner',
options: { sort: { number: 1 }}, // Added sort just as an example
});
And regarding the aggregate, with:
User.aggregate([{
$lookup: {
from: 'Fragment',
localField: '_id',
foreignField: 'owner',
as: 'fragments'
}
}, { "$project": {
"email": 1,
"fragments": 1,
"nbFragments": {
"$size": { "$ifNull": [ "$fragments", [] ] } }
}}, { "$sort": { "nbFragments": -1 } }
]).exec(console.log)
I get:
{
_id: 58075ce27b7f2f01002b717f,
email: 'email#example.com',
fragments: [] // fragments are always empty while they shouldn't!
}
Tested with following data
User:
{
"_id" : ObjectId("58373e571cbccb010012bfcd"),
"email" : "email#example.com"
}
Fragment:
{
"_id" : ObjectId("58075ce37b7f2f01002b718f"),
"text" : "Donc, il faut changer de méthode",
"owner" : ObjectId("58373e571cbccb010012bfcd")
}
Response
[{"_id":"58373e571cbccb010012bfcd","email":"email#example.com","fragments":[{"_id":"58075ce37b7f2f01002b718f","text":"Donc, il faut changer de méthode","owner":"58373e571cbccb010012bfcd"}],"nbFragments":1}]
You define schema to use owner to populate the fragments also called virtual population. http://mongoosejs.com/docs/populate.html
const userSchema = new mongoose.Schema({
email: { type: String, unique: true }
}, { timestamps: true, collection: 'user' });
var User = mongoose.model("User", userSchema);
const fragmentSchema = new mongoose.Schema({
text: String,
owner: {type: mongoose.Schema.Types.ObjectId, ref: 'User'},
}, { timestamps: true, collection: 'fragment' });
var Fragment = mongoose.model("Fragment", fragmentSchema);
userSchema.virtual('fragments', {
ref: 'Fragment',
localField: '_id',
foreignField: 'owner',
options: { sort: { text: -1 }}, // Added sort just as an example
});
This will now work as expected, but I don't know a way to sort on some dynamic field like count of number of fragments in mongoose. I don't think it is possible
User.find({_id: '58373e571cbccb010012bfcd'})
.populate('fragments').exec(function (err, user) {
console.log(JSON.stringify(user));
});
Okay now for dynamic sorting, you have to use alternative raw mongo query with a $lookup (equivalent of populate).
const userSchema = new mongoose.Schema({
email: { type: String, unique: true }
}, { timestamps: true, collection: 'user' });
var User = mongoose.model("User", userSchema);
const fragmentSchema = new mongoose.Schema({
text: String,
owner: {type: mongoose.Schema.Types.ObjectId, ref: 'User'},
}, { timestamps: true, collection: 'fragment' });
User.aggregate([{
$lookup: {
from: 'fragment',
localField: '_id',
foreignField: 'owner',
as: 'fragments'
}
}, { "$project": {
"email": 1,
"fragments": 1,
"nbFragments": {
"$size": { "$ifNull": [ "$fragments", [] ] } }
}}, { "$sort": { "nbFragments": -1 } }
]).exec(function (err, user) {
console.log(JSON.stringify(user));
})