MongoDB Realm: Query a Relationship - mongodb

here is my problem:
export const PostSchema = {
name: "Post",
primaryKey: "_id",
properties: {
_id: "string",
comments: "Comment[]"
},
};
export const CommentSchema = {
name: "Comment",
primaryKey: "_id",
properties: {
_id: "string",
post: "Post"
},
};
// Original Query:
const posts = realm.objects("Post");
console.log(posts);
// Original Query query results in:
[
{
_id: "post_1",
comments: [],
},
{
_id: "post_2",
comments: [],
},
...
];
// I'm looking for a query with the following result:
[
{
_id: "post_1",
comments: [
{
_id: "Only the first (newest) comment post_1",
},
],
},
{
_id: "post_2",
comments: [
{
_id: "Only the first (newest) comment post_2",
},
],
},
...
];
//My current solution therefor is:
let data = realm.objects("Post").map((post) => {
return {
...post.toJSON(),
comments: m.linkingObjects("Comment", "post").slice(0, 1),
};
});
I'm not sure if this is the right/optimal solution.
If I understood correctly, then I make an extra request for the first comment for each post?
So if I have 20 posts, that's 20 extra requests? That would be very impractical?
Is there a better way?
I also tried Inverse Relationships.
But with this method I always get all the comments for a post which is a lot of unnecessary data?
THX for any help :-)

db.posts.aggregate([
{
$lookup: {
from: "comments",
localField: "comments",
foreignField: "_id",
as: "comments",
pipeline: [
{ $sort: { _id: 1 } },
{ $limit: 1 }
]
}
}
])
mongoplayground

Related

How do I query to fetch number of Posts made by a User using mongoose?

So I'm trying to fetch the total number of posts done by a specific user.
Models
Post.ts (In post model I have user ref)
user: {
type: Schema.Types.ObjectId,
ref: "User",
required: [true, "Please provide user ID."],
},
I tried
const posts = await Post.find({ user: req.user._id});
const postCount = posts.length;
But I want to aggregate the User so I can get the user data along with the post counts.
what I tried is below but I'm getting 0 number of posts.
const user = await User.aggregate([
{
$lookup: {
from: "post",
let: { userId: "$_id" },
pipeline: [{ $match: { $expr: { $eq: ["$$userId", "$userId"] } } }],
as: "posts_count",
},
},
{ $addFields: { posts_count: { $size: "$posts_count" } } },
]);
I searched but couldn't find any related answer.
You can slightly alter the query as follows. From the post model, I see that the field referenced for user collection is user. In the aggregate, you have used $userId.
const user = await User.aggregate([
{
$lookup: {
from: "post",
let: { userId: "$_id" },
pipeline: [{ $match: { $expr: { $eq: ["$$userId", "$user"] } } }],
as: "posts_count",
},
},
{ $addFields: { posts_count: { $size: "$posts_count" } } },
]);
Let me know if this helps. Thanks

$in operator not working in MongoDB Aggregation

I'm trying to make a discover page for a social media website. The discover page queries the database for all posts that satisfy four things:
User has not already liked post
Post tags do not violate user's filtered tag content
Post text content does not violate user's filtered post content
And finally the part of the aggregation giving me trouble:
Post tagIds contain a given tagId from user (a post using the same tag that the user already follows)
Here's the function:
const asyncFetchTagPosts = async (
query,
//here's a given tag that a user already follows
tagId,
likedPostIds,
Post,
User,
mongoose,
handleFilterTagRegex,
handleFilterPostContentRegex
) => {
var recastTagId = mongoose.Types.ObjectId(tagId)
var user = await User.findOne({ blogName: query })
var filteredTagRegex = handleFilterTagRegex(user)
var filteredPostContentRegex = handleFilterPostContentRegex(user)
var posts = await Post.aggregate([
{
$lookup: {
from: 'posts',
let: {
likedPostIds: likedPostIds,
tagId: recastTagId,
filteredTagRegex: filteredTagRegex,
filteredPostContentRegex: filteredPostContentRegex
},
pipeline: [
{
$match: {
$expr: {
$and: [
{ $not: { $in: ["$_id", "$$likedPostIds"] } },
{ $not: [
{
$regexMatch: {
input: "$tagTitles",
regex: "$$filteredTagRegex"
}
}
]
},
{ $not: [
{
$regexMatch: {
input: "$allText",
regex: "$$filteredPostContentRegex"
}
}
]
},
{ $or: [
//here's the bad expression, $tagIds won't resolve to an array
{ $in: [ "$$tagId", "$tagIds" ] },
]
}
]
}
}
}
],
as: 'posts'
}
},
{ $unwind: '$posts' },
{ $replaceRoot: { "newRoot": "$posts" } },
{ $sort: { "notesHeatLastTwoDays": -1 } },
{ $limit: 5 }
])
return posts
}
Here's the Post model:
import mongoose from 'mongoose';
const Schema = mongoose.Schema;
const options = { discriminatorKey: 'kind' }
const PostSchema = new Schema({
user: {
type: Schema.Types.ObjectId,
ref: 'User'
},
allText: {
type: String
},
descriptions: [
{
kind: String,
content: String,
displayIdx: Number
}
],
descriptionImages: [
{
type: Schema.Types.ObjectId,
ref: 'Image'
}
],
tagIds: [
{
type: Schema.Types.ObjectId,
ref: 'Tag'
}
],
tagTitles: {
type: String
},
mentions: [
{
type: Schema.Types.ObjectId,
ref: 'Mention'
}
],
notesCount: {
type: Number,
default: 0
},
notesHeatLastTwoDays: {
type: Number,
default: 0
},
createdAt: {
type: Date,
default: Date.now
},
updatedAt: {
type: Date,
default: Date.now
},
kind: {
type: String,
default: 'Post'
}
}, options)
const Post = mongoose.model('Post', PostSchema, 'posts')
export default Post;
I keep getting this error:
Error: $in requires an array as a second argument, found: missing
When I comment out the last part of the query the aggregation works. It returns data in this shape:
{
_id: 60c18ee43730198901cfae9b,
descriptionImages: [],
//here's the array I'm trying to get to resolve in the aggregation
tagIds: [],
mentions: [],
notesCount: 1,
notesHeatLastTwoDays: 0,
kind: 'VideoPost',
descriptions: [],
createdAt: 2021-06-10T04:02:44.744Z,
updatedAt: 2021-06-11T08:51:38.166Z,
user: 608f213bb4a094bd91e02936,
videoLink: 60c3241a6c9ed4d1fc908270,
allText: '',
__v: 1,
tagTitles: ''
},
I thought using the $ operator in the aggregation gave me access to each document, does it just not work if you try to use the variable as the first expression?
you need to handle missing "$tagIds" by setting it to empty array []
{
$ifNull: [
"$tagIds",
[]
]
}
https://docs.mongodb.com/manual/reference/operator/aggregation/ifNull/
so you pipeline stage would be
{ $or: [
//here's the bad expression, $tagIds won't resolve to an array
{ $in: [ "$$tagId", { $ifNull: [ "$tagIds", [] ] } ] },
]
}

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

Mongoose querying a reference collection data using aggrigate/populate

I am a newbie in MongoDB and Express JS
I have a Product model looks like
const productSchema = mongoose.Schema({
name: String,
title: String,
brand_id: { type: mongoose.Schema.Types.ObjectId, ref: 'brand' },
varient:[{
country_id: { type: mongoose.Schema.Types.ObjectId,ref: 'countries'},
max_retail_price : Number,
profit_margin : Number
}],
and Order model
const orderTransactionSchema = mongoose.Schema({
shop_id: { type: mongoose.Schema.Types.ObjectId, ref: 'shop' },
brand_id:{ type: mongoose.Schema.Types.ObjectId, ref: 'brand' },
product_id: { type: mongoose.Schema.Types.ObjectId, ref: 'product' },
product_varient_id:{ type: mongoose.Schema.Types.ObjectId, ref: 'product.varient._id' },
transaction_id:{ type: mongoose.Schema.Types.ObjectId, ref: 'transaction' }
})
module.exports = mongoose.model('order', orderTransactionSchema );
In my Product collection, each product can have multiple variants.
But in order collection, a user can order only one product variant under a product.
I am trying to display orders with particular product details and variant details, But the problem is when I try to display it using either populate/Aggregate I am getting all the variants in the response array. actually, I want only one product and variant details as in the order collection.
This is what I tried
order.aggregate([{ $match :{} },
{
$lookup: {
from: "products",
localField: "product_id",
foreignField: "_id",
as: "product_data"
}
},
]).exec(function(err,result){
console.log(result);
});
and I am getting the output as
{ _id: 5c8a010b8feeb875abc1b066,
shop_id: 5c7d194ca10ea45c0c03a0ee,
brand_id: 5c41a8c34272c61a176b7639,
product_varient_id: 5c41a9f3f8e1e71aa75b4f32,
transaction_id: 5c6670d5b6c63d0762c6cc77,
product_id: 5c41aac4d45a731af564c433,
product_data:
[ { _id: 5c41aac4d45a731af564c433,
brand_id: 5c41a8c34272c61a176b7639,
image: 'test.jpg',
varient: //getting all the varients here
[ { _id: 5c4ee531bc20b27948b3aa98,
sell_rate_local: 66,
country_id: 5c01149d3c20440a6b2e4928 },
{ _id: 5c4ee53bbc20b27948b3aa99,
sell_rate_local: 66,
country_id: 5c00e1697dd7a23f08bdae68 } ],
__v: 0 } ] } ]
In the Order table, there is porduct_id and product_varient_id is there
I want to populate only the product with product_varient_id.
I also tried something with Populate
order.find().
populate([
{ path: 'shop_id', model: 'shop',select : 'name accounts' }, //it works
{ path: 'transaction_id', model: 'transaction' }, //it wrks
{ path: 'product_varient_id', model: 'product', select:'product.varient.name'},
]).then(result =>
{
// console.log(result);
}).catch(err =>{
// console.log(err);
});
These are the sample product and order document
Order Document Sample :
{
"_id":"5c77a025d65a892f6acf1803",
"shop_id":"5c7d194ca10ea45c0c03a0ee",
"brand_id":"5c41a8b44272c61a176b7638",
"product_varient_id":"5c41a9f3f8e1e71aa75b4f32",
"buy_rate":10,
"buy_rate_after_discount":20,
"product_mrp":30,
"sell_rate":40,
"customer_mobile":123456789,
"status":true,
"transaction_id":"5c6670c9b6c63d0762c6cc76",
"product_id":"5c41a95ff8e1e71aa75b4f30",
"createdAt":"2019-02-28T08:47:33.097Z",
"updatedAt":"2019-02-28T08:47:33.097Z",
"__v":0
}
Product document Sample :
{
"_id":"5c41aac4d45a731af564c433",
"recharge_type":[
"5c00d9cf7dd7a23f08bdae5e"
],
"name":"25 OC - Product 1",
"title":"First installation recharge",
"description":"0.1 credit for first time installation",
"offer_message":"Hi.. You got 0.1 credits..!!",
"brand_id":"5c41a8c34272c61a176b7639",
"buy_rate":20,
"profit_margin":80,
"image":"test.jpg",
"varient":[
{
"_id":"5c4ee531bc20b27948b3aa98",
"display_name":"testlia",
"profit_margin":66,
"max_retail_price":66,
"sell_rate":66,
"sell_rate_local":66,
"country_id":"5c01149d3c20440a6b2e4928"
},
{
"_id":"5c4ee53bbc20b27948b3aa99",
"display_name":"testrinu",
"profit_margin":66,
"max_retail_price":66,
"sell_rate":66,
"sell_rate_local":66,
"country_id":"5c00e1697dd7a23f08bdae68"
}
],
"createdAt":"2019-01-18T10:30:28.991Z",
"updatedAt":"2019-01-28T11:19:23.662Z",
"__v":0
}
MongoDB 3.6 or above have new lookup syntax
db.orders.aggregate([{
$lookup: {
from: "products",
let: {
"productId": "$product_id",
"productVarientId": "$product_varient_id"
},
pipeline: [
{ $match: {
$expr: { $eq: [ "$_id", "$$productId" ]}
}
},
{ $addFields: {
varient: {
$filter: {
input: "$varient",
as: "varient",
cond: { $eq: [ "$$productVarientId", "$$varient._id" ] }
}
}
}
}
],
as: "product_data"
}
}])
You can check datasample Here

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