How to implement a SUM of items - node.js

I have a MongoDB db which has two tables, there is the ItemGroup and there is the Item. And ItemGroup has a name and a set [] of Items, and Items hold the actual price.
I use MondoDB with NodeJS and Mongoose, my models are defined like this:
var itemGroupSchema = new Schema({
title : {type: String, required: true},
items : [{type: Schema.Types.ObjectId, ref: 'Item'}],
totalp : Number,
date : {type: Date, default: Date.now}
});
var itemSchema = new Schema({
_group : {type: Schema.ObjectId, ref: 'ItemGroup'},
name : {type: String, required: true},
price : Number,
date : {type: Date, default: Date.now}
});
I am open to changing my models if there is a better structure. The basic action I want t achieve is present the total price of all items in a group.
These are the things I could think of:
Have a function in my itemGroup model that sums all the items on save.
Have a getter in the itemGroup that sums all the items on get, totalp could be removed.
Sum all the items just in NodeJS in a loop, and then just update the itemGroup
Maybe there is a better way?

You have a couple of reasonable options, mostly determined by how you're using other parts of the application.
Option 1:
Find the item IDs from your group, then use those IDs in a {$in:itemIds} aggregate query. This requires double-reads but keeps the writing to a minimum. This is useful if you're regularly updating your items. Not as useful if your primary action is to display an itemGroup
Option 2: Update the itemGroup to include a bit more cached or computed information. This could include the total price, recalculated whenever you update a corresponding item. It could also include the price for each item:
{title : 'Red items', items: [{itemId: 'I1', price: 2}, {itemId: 'I2', price : 3}] ... }
The advantage of option 2: allows brief display of data and easy recalculation of totals. Downside: every write to the itemSchema becomes a double-write to both collections. There are no transactions, so there will be points (however brief) where they are not perfectly in sync.
Option 3: Place the entire itemSchema inside the itemGroupSchema. This is fast for retrieval and fast for saving as they all come from one document. You can even index on sub-documents. It doesn't work as well if you have unbounded growth in the number of items in a group, or very very large items. Probably what I'd recommend.

Related

Proper way of updating average rating for a review system using Mongoose

I'm currently learning some backend stuff using an Udemy course and I have an example website that lets you add campgrounds (campground name, picture, description, etc.) and review them. I'm using the Express framework for Node.js, and Mongoose to access the database.
My campground schema looks like:
const campgroundSchema = new mongoose.Schema({
name: String,
image: String,
description: String,
price: String,
comments: [
{
type: mongoose.Schema.Types.ObjectId,
ref: "Comment"
}
],
rating: {type: Number, default: 0}
});
And my comment/review schema looks like:
const commentSchema = new mongoose.Schema({
text: String,
rating: {
type: Number,
min: 1,
max: 5,
validate: {validator: Number.isInteger}
},
campground: {type: mongoose.Schema.Types.ObjectId, ref: "Campground"}
});
Campgrounds and Comments also have references to a User but I've left that out for simplicity.
I'm looking to know the best practice for updating and displaying the campground average rating.
The method used by the tutorial I'm following is to recalculate the average rating each time a comment is added, changed, or deleted. Here's how it would work for a new comment:
Campground.findById(campgroundId).populate("comments").exec(function(err, campground) {
Comment.create(newComment, function(err, comment) {
campground.comments.push(comment);
campground.rating = calculateRating(campground.comments);
campground.save();
});
});
"calculateRating" iterates through the comment array, gets the total sum, and returns the sum divided by the number of comments.
My gut instinct tells me that there should be a way to make the "rating" field of Campground perform the functionality of the "calculateRating" function, so that I don't have to update the rating every time a comment is added, changed, or removed. I've been poking around documentation for a while now, but since I'm pretty new to Mongoose and databases in general, I'm a bit lost on how to proceed.
In summary: I want to add functionality to my Campground model so that when I access its rating, it automatically accesses each comment referenced in the comments array, sums up their ratings, and returns the average.
My apologies if any of my terminology is incorrect. Any tips on how I would go about achieving this would be very much appreciated!
Love,
Cal
I think what you are trying to do is get a virtual property of the document that gets the average rating but it does not get persisted to the mongo database.
according to mongoosejs :- Virtuals are document properties that you can get and set but that do not get persisted to MongoDB. They are set on the schema.
You can do this:
CampgroundSchema.virtual('averageRating').get(function() {
let ratings = [];
this.comments.forEach((comment) => ratings.push(comment.rating));
return (ratings.reduce((a,b)=>a+b)/ratings.length).toFixed(2);
});
After that on your view engine after finding campgrounds or a campground, all you need to call is ; campground.averageRating;
Read more here : https://mongoosejs.com/docs/guide.html#virtuals
also note that you can not make any type of query on virtual properties.

Best way to structure my mongoose schema: embedded array , populate, subdocument?

Here is my current Schema
Brand:
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var BrandSchema = new mongoose.Schema({
name: { type: String, lowercase: true , unique: true, required: true },
photo: { type: String , trim: true},
email: { type: String , lowercase: true},
year: { type: Number},
timestamp: { type : Date, default: Date.now },
description: { type: String},
location: { },
social: {
website: {type: String},
facebook: {type: String },
twitter: {type: String },
instagram: {type: String }
}
});
Style:
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var StyleSchema = new mongoose.Schema({
name: { type: String, lowercase: true , required: true},
});
Product
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var ProductSchema = new mongoose.Schema({
name: { type: String, lowercase: true , required: true},
brandId : {type: mongoose.Schema.ObjectId, ref: 'Brand'},
styleId: {type: mongoose.Schema.ObjectId, ref: 'Style'},
year: { type: Number },
avgRating: {type: Number}
});
Post:
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var PostSchema = new mongoose.Schema({
rating: { type: Number},
upVote: {type: Number},
brandId : {type: mongoose.Schema.ObjectId, ref: 'Brand'},
comment: {type: String},
productId: {type: mongoose.Schema.ObjectId, ref: 'Style'},
styleId: {type: mongoose.Schema.ObjectId, ref: 'Style'},
photo: {type: String}
});
I'm currently making use of the mongoose populate feature:
exports.productsByBrand = function(req, res){
Product.find({product: req.params.id}).populate('style').exec(function(err, products){
res.send({products:products});
});
};
This works, however, being a noob --- i've started reading about performance issues with the mongoose populate, since it's really just adding an additional query.
For my post , especially, it seems that could be taxing. The intent for the post is to be a live twitter / instagram-like feed. It seems that could be a lot of queries, which could greatly slow my app down.
also, I want to be able to search prodcuts / post / brand by fields at some point.
Should i consider nesting / embedding this data (products nested / embedded in brands)?
What's the most efficient schema design or would my setup be alright -- given what i've specified I want to use it for?
User story:
There will be an Admin User.
The admin will be able to add the Brand with the specific fields in the Brand Schema.
Brands will have associated Products, each Product will have a Style / category.
Search:
Users will be able to search Brands by name and location (i'm looking into doing this with angular filtering / tags).
Users will be able to search Products by fields (name, style, etc).
Users will be able to search Post by Brand Product and Style.
Post:
Users will be able to Post into a feed. When making a Post, they will choose a Brand and a Product to associate the Post with. The Post will display the Brand name, Product name, and Style -- along with newly entered Post fields (photo, comment, and rating).
Other users can click on the Brand name to link to the Brand show page. They can click on the Product name to link to a Product show page.
Product show page:
Will show Product fields from the above Schema -- including associated Style name from Style schema. It will also display Post pertaining to the specific Product.
Brand show page:
Will simply show Brand fields and associated products.
My main worry is the Post, which will have to populate / query for the Brand , Product, and Style within a feed.
Again, I'm contemplating if I should embed the Products within the Brand -- then would I be able to associate the Brand Product and Style with the Post for later queries? Or, possibly $lookup or other aggregate features.
Mongodb itself does not support joins. So, mongoose populate is an attempt at external reference resolution. The thing with mongodb is that you need to design your data so that:
most of you queries need not to refer multiple collections.
after getting data from query, you need not to transform it too much.
Consider the entities involved, and their relations:
Brand is brand. Doesn't depend on anything else.
Every Product belongs to a Brand.
Every Product is associated with a Style.
Every Post is associated with a Product.
Indirectly, every Post is associated to a Brand and Style, via product.
Now about the use cases:
Refer: If you are looking up one entity by id, then fetching 1-2 related entities is not really a big overhead.
List: It is when you have to return a large set of objects and each object needs an additional query to get associated objects. This is a performance issue. This is usually reduced by processing "pages" of result set at a time, say 20 records per request. Lets suppose you query 20 products (using skip and limit). For 20 products you extract two id arrays, one of referred styles, and other of referred brands. You do 2 additional queries using $in:[ids], get brands and styles object and place them in result set. That's 3 queries per page. Users can request next page as they scroll down, and so on.
Search: You want to search for products, but also want to specify brand name and style name. Sadly, product model only holds ids for style and brand. Same issue with searching Posts with brand and product. Popular solution is to maintain a separate "search index", a sort of table, that stores data exactly the way it will be searched for, with all searchable fields (like brand name, style name) at one place. Maintaining such search collections in mongodb manually can be a pain. This is where ElasticSearch comes in. Since you are already using mongoose, you can simply add mongoosastic to your models. ElasticSearch's search capabilities are far greater than a DB Storage engine will offer you.
Extra Speed: There is still some room for speeding things up: Caching. Attach mongoose-redis-cache and have frequent repeated queries served, in-memory from Redis, reducing load on mongodb.
Twitter like Feeds: Now if all Posts are public then listing them up for users in chronological order is a trivial query. However things change when you introduce "social networking" features. Then you need to list "activity feeds" of friends and followers. There's some wisdom about social inboxes and Fan-out lists in mongodb blog.
Moral of the story is that not all use cases have only "db schema query" solutions. Scalability is one of such cases. That's why other tools exist.

query on many to many relation mongodb database struct

I have two collections in MongoDB: one saves post data of blog, the other saves comment data of blog with below schemas. How can I use nodejs and mongoose to query all posts with comment belong to it and respond to single page application?. Thanks!
var PostSchema = mongoose.Schema({
created: {
type: Date,
default: Date.now
},
content: {
type: String,
default: '',
trim: true
},
user: {
type: Schema.ObjectId,
ref: 'user'
}
});
var CommentSchema = mongoose.Schema({
created: {
type: Date,
default: Date.now
},
content: {
type: String,
default: '',
trim: true
},
ofpost: {
type: Schema.ObjectId,
ref: 'post' //which post this comment belong to
},
user: {
type: Schema.ObjectId,
ref: 'user'
}
});
var Post = mongoose.model('Post', PostSchema);
var Comment = mongoose.model('Comment', CommentSchema);
//example:the Comment1 and Comment2 belong to Post1
var Post1 = new Post({ content: 'good day', user: 'John' });
var Comment1 = new Comment({content: 'yeah', ofpost: Post1._id, user:'Tom'})
var Comment2 = new Comment({content: 'agree', ofpost: Post1._id, user:'Tina'})
As mongodb is NoSQL type of database and has no JOIN's or any sort of relationship between documents, you have to take care of such.
There are generally two ways to do so:
Caching
Consider storing comments data within blog document. You can have embedded documents without any problem. In reality it leads to some extra caches, like comments count, array of user id's of comments and other stuff that will make your queries indexed and more easy ways to search through collection.
Multiple Queries
If you still need separate collections, then you need to 'simulate' joins. Most efficient ways is to make temporary indexing arrays and multiple queries to different collections. Usually it should be just 2 queries for one Join (many to many), and small iteration to add second query documents to first array of documents.
Here is the flow that is suitable and performs well still, on example:
Two collections, first is posts, and second is comments which has id of post.
Make query to posts.
Iterate through each post and add its id into postIds array, as well make postMap object where key will be id of post and value will be specific post. - this is so called indexing posts.
Make query to comments collection with $in argument with postIds array of post id's. This collection should have indexing on post id field in order to make this query very efficient. As well this query can include sorting by date (additional compound indexing will speedup it).
Iterate through each comment and using postMap add it to comments array of post.
So we have only 2 queries, and one iteration through all comments to embed data into posts O(n). Without second step, adding to posts will be potentially O(p*c) where p - number of posts and c - number of comments. Which is obviously much slower as well on big queries can be potentially slow.
Summary
Second approach is more manageable approach from data point of view, as well is easier on writes, while is more complicated on reads.
Still will require some caching, like number of comments for blog posts.

One to Many mapping in mongoose, How to receive and process?

The problem I have is a One to many mapping with mongoose(Mongodb). The one is the Order(buyer data) and the many is the Items(price,quantity,etc).
1) How I should create the Schema for the the Order and items, like should I put the items in an array in the order?
2) Would all the data be in one post function?
I herd you can use ObjectId to link the many to one but I am not sure how to.
Since an Order sounds like it will have a relative small number of items, the simplest thing would probably be just a list of item Ids:
var OrderSchema = new mongoose.Schema({
items: [{type: mongoose.Schema.Types.ObjectId, ref: 'Item'}]
});
var ItemSchema = new mongoose.Schema({
price: Number,
quantity: Number
});
Most UIs would not build an entire order in a single POST function, so it's probably best to allow creating an order and then separately adding items to it via order.items.push(itemId).

How can I check if the user already rated the item in NodeJS & Mongoose Application?

I have following schemas in Mongoose:
UserSchema = new Schema({
ratings = [{type : Schema.ObjectId, ref : 'Rating'}] })
ItemSchema = new Schema({
ratings = [{type : Schema.ObjectId, ref : 'Rating'}] })
Rating = new Schema({
user = [{type : Schema.ObjectId, ref : 'User'}],
venue = [{type : Schema.ObjectId, ref : 'Venue'}]
})
Are they right? I should query ratings by users, ratings for items. Also I want to check if the user has already rated an item.
Here are two of the following options you can go with.
You can maintain a separate collection Rating quite similar to what you would have done in SQL.
User: voter (reference to User object),
Item: item_voted (reference to item object),
Rating: what so ever user rated
Time: time_rated,
other fields as per your requirements...
Now maintain index over User and Item to boost up queries to check if user already rated for an item or not.
OR you could maintain an array in User collection for items rated by that user, and index over that array. Here it is what you can have your data model for User like.
items_rated: [item1, item2, item3]
other fields of User as per your requirements...
This second approach has a limitation that it fails if your BSON records exceeds 16MB limit, but in practical usage it very very less probable that you actually would hit that limit. Though nothing can be said. If your Users turn out to be maniac like some top stackoverflow users you will hit that 16MB wall :P
The way you can check if item has been rated or not (if you opt for second choice is)
if (db.user.count({item_rated: item_k, _id:'user-id-1'}) == 0) { ... }

Resources