Structuring My Models with Mongoose/MongoDB - node.js

I have begun diving into the server side of things lately, and am working on an app where I need to think about how I plan my models.
My users are teachers, and in the dashboard will have the ability to create a list of students. My schema's will contain more directives to prevent duplicates being created, but I have simplified them here. Here's what I have attempted so far:
// Teacher Model
const Teacher = new Schema({
fname: String,
lname: String,
email: String,
})
// Student Model
const Student = new Schema({
fname: String,
lname: String,
full: String,
uuid: String
grades: {
classwork: Array,
quizzes: Array,
tests: Array
}
})
Here's where my inexperience with backend work comes into play. This setup doesn't really make sense to me. Say when I go and save a student, it will create a new student under the student collection in the database. This is not ideal, as the student should be stored in a way that is strictly accessible to the teacher who created it.
I was thinking about creating a new key in my Teachers Schema called "students"(which would be an array) that would push a student into it each time one was created.
It's definitely important that I plan this properly, as the teacher is going to have much more ability in the future, like creating assignments, grading students etc. I'd like to design this with best practices in mind, to ensure the teachers data is safe from other users.

I don't agree with #Lazyexpert. MongoDB is a non-relational database and
you can store until 16Mb of data per document. It is really enough for what you need
The maximum BSON document size is 16 megabytes. The maximum document size helps ensure that a single document cannot use excessive amount of RAM or, during transmission, excessive amount of bandwidth. To store documents larger than the maximum size, MongoDB provides the GridFS API.
i.e: https://docs.mongodb.com/manual/reference/limits/
So I suggest you just add the datas of each student directly in your teacher.
You can find some tips here : https://www.safaribooksonline.com/library/view/50-tips-and/9781449306779/ch01.html
So your model would looks something like that :
const Teacher = new Schema({
fname: String,
lname: String,
email: String,
students : [
{
fname: String,
lname: String,
full: String,
uuid: String
grades: {
classwork: Array,
quizzes: Array,
tests: Array
},
},
],
})
And if you absolutely want a collection Student as well, then use a "post" middleware on the "save" action on your student schema. Something like this :
StudentSchema.post('save', function(doc) {
Teacher.findOneAndUpdate({_id: <your teacher id>}, <your student object>, callback);
});
i.e: mongoosejs.com/docs/api.html#schema_Schema-post
Good luck :)

Using nested array in mongo model is not so smooth.
I can suggest to think about size of this array.
If there is a chance that your array can grow - don't use it.
My suggestion for your database design is simple.
Add teacherId to the student model.
This way, when you need to fetch students list according to the certain teacher - you can easily query by teacherId.
So your student schema modified would look this way:
const Student = new Schema({
teacherId: {
type: mongoose.Schema.Types.ObjectId,
index: true,
required: true
},
fname: String,
lname: String,
full: String,
uuid: String
grades: {
classwork: Array,
quizzes: Array,
tests: Array
}
});

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.

Mongoose best practices for document creation with unknown number of items in a field

I've looked around left and right, I wrote some demo code, wrote some tests to implement a school management system.
What I want to know from people more used to mongoose development is how would be the best practice to create this schema in a way that made it possible to add as many address, and contact fees as I want from this single document.
I made my own solution, but I don't know if it is the most elegant and feasible way, I want an opinion from seasoned people.
Should I create separate models for address, email and phone numbers?
I created this schema. It still has some pseudo-code, but for giving the general idea is fine.
var student = {
name: String,
surname: String,
currentClass: {
type: mongoose.Schema.Types.ObjectId,
ref: "Classes"
},
birthday: {
year: Number,
month: Number,
day: Number
},
address: [{
name: String,
zip: Number,
address: String,
city: String,
state: String,
complement: String
}]
accountable: {
name: String,
surname: String,
email: [{
type: String,
required: true,
lowercase: true
}],
phone: [String, String]
}
My sollution was, by using html, creating a new "email" or "address" fields as the user requested by clickinking in the propper button. This generated a new input field with a name that followed a pattern like:
email1, email2, email3, email4
And so, when the user sent the data, if we were creating a new student I would first create the array with the data and send it to mongoose.
In case of updating, I would get the already created emails and add it to the new array with the newEmails.concat(old_emails)
To design the database, there are many situations for it:
1 to 1, 1 to many, many to many.
One to one: you should to put the strong object inside the other, for example:a person can have only one passport, then we should put passport object inside the person object.
One to Many, 3 cases for one to many.
One to Few:few is less than 100 objects,Then you should add the few as list in the one object, for example:
A person can have multiple addresses as in your example above.
One to Many:many is Thousands, then you should put the primary keys of the many in a list inside the the one object.
One to Too many: then do not do the previous solution, but instead add the primary of the one in every objects of the many.
And finally, Many to Many: you should put them as list in both sides.
Check the below references:
https://www.mongodb.com/blog/post/6-rules-of-thumb-for-mongodb-schema-design-part-1
https://www.mongodb.com/blog/post/6-rules-of-thumb-for-mongodb-schema-design-part-2
https://www.mongodb.com/blog/post/6-rules-of-thumb-for-mongodb-schema-design-part-3
Morover, for this part phone: [String, String], you should make it phone: [String]

Mongoose Schema: Using nested arrays to store data that will be queried for

I am using mongodb and mongoose to store data for a group of students.
They attend a camp session once or twice a year in grades K & 1, and I mainly want to keep track of the skills that the students achieved mastery in, and the dates that they achieved it, as well as some personal, session-specific data.
Here is my schema:
var location = ["DJ", "MLK"];
var days = ["MoWe", "TuTh"];
var grades = ["K", "First"];
var skills = ["FK", "BC", "BK", "IK", "PS", "SLF", "RBC", "RBK", "RIK", "FS", "BS"];
var achievements = ["Red", "Yellow", "Green"];
var studentSchema = new Schema({
name: String,
sessionInfo: [{
sessionName: String,
location: {type: String, enum: locations},
instructor: String,
class: {school: String, teacher: String},
grade: {type: String, enum : grades},
age: {type: Number},
days: {type: String, enum : days}}],
skills: [{skill: {type: String, enum: skills}, date: Date}],
absences: [{date: Date, reason: String}],
achievements: [{achievement: {type: String, enum: achievements}}],
notes: [{instructorName: String, note: String}]
});
Because the students will at most attend 3 or 4 sessions, I thought it made sense to store the session info in it's own array than to try to store each student-session in it's own separate document.
My question is whether or not this will significantly impact the response time for querying the database if I am going to be regularly querying for students based on skills attained, grade, school, and location.
Note that none of the arrays within the session info will be growing without bound.
As mongodb stores indexes in RAM, if the size of your collection is large then performance can be lower in case of creating separate collection for sessionInfo where every session will have its own already indexed ObjectId (Assuming total number of sessions for all the students will be very large).
It is generally faster to query indexed ObjectId than querying for a primary key in an Array of objects. But as you said sessionInfo array won't be having more than 3 to 4 records, it is better to keep sessionInfo an array. Also you can create index on a particular property of session object.

Mongoose: Difference between referencing "Schema.ObjectId" instead of directly using the schema name?

Suppose I have the following MessageSchema model:
var MessageSchema = new Schema({
userId: String,
username: String,
message: String,
date: Date
});
mongoose.model('Message', MessageSchema)
Can someone tell me the difference between the following two implementations of the Meetings model? Thanks.
var Meetings = new Schema({
_id: ObjectId,
name: String,
messages: [MessageSchema],
...
});
var Meetings2 = new Schema({
_id: ObjectId,
name: String,
messages: [{type: Schema.ObjectId, ref: 'Message'}],
...
});
The main difference is that Meeting model is embedding the MessageSchema (denormalization) whilst the Meeting2 model references it (normalization). The difference in choice boils down to your model design and that depends mostly on how you query and update your data. In general, you would want to use an embedded schema design approach if the subdocuments are small and the data does not change frequently. Also if the Message data grows by a small amount, consider denormalizing your schema. The embedded approach allows you to do optimized reads thus can be faster since you will only execute a single query as all the data resides in the same document.
On the other hand, consider referencing if your Message documents are very large so they are kept in a separate collection that you can then reference. Another factor that determines the referencing approach is if your document grows by a large amount. Another important consideration is how often the data changes (volatility) versus how it's read. If it's updated regularly, then referencing is a good approach. This way enhances fast writes.
You can use a hybrid of embedding and referencing i.e. create an array of subdocuments with the frequently accessed data but with a reference to the actual document for more information.
The general rule of thumb is that if your application's query pattern is well-known and data tends to be accessed only in one way, an embedded approach works well. If your application queries data in many ways or you unable to anticipate the data query patterns, a more normalized document referencing model will be appropriate for such case.
Meetings messages field contains an array of Message object, while Meetings2 messages field contains an array of Message Id's.
var Meetings2 = new Schema({
...
messages: [{type: Schema.ObjectId, ref: 'Message'}],
...
});
can be written as
var Meetings2 = new Schema({
...
messages: [Schema.ObjectId],
...
});
The ref is just a helper function in mongoose, making it easier to populate the messages.
So in summary. In Meetings you embed the messages in an array, while in Meetings2 you reference the messages.

Mongoose: populate() / DBref or data duplication?

I have two collections:
Users
Uploads
Each upload has a User associated with it and I need to know their details when an Upload is viewed. Is it best practice to duplicate this data inside the the Uploads record, or use populate() to pull in these details from the Users collection referenced by _id?
OPTION 1
var UploadSchema = new Schema({
_id: { type: Schema.ObjectId },
_user: { type: Schema.ObjectId, ref: 'users'},
title: { type: String },
});
OPTION 2
var UploadSchema = new Schema({
_id: { type: Schema.ObjectId },
user: {
name: { type: String },
email: { type: String },
avatar: { type: String },
//...etc
},
title: { type: String },
});
With 'Option 2' if any of the data in the Users collection changes I will have to update this across all associated Upload records. With 'Option 1' on the other hand I can just chill out and let populate() ensure the latest User data is always shown.
Is the overhead of using populate() significant? What is the best practice in this common scenario?
If You need to query on your Users, keep users alone. If You need to query on your uploads, keep uploads alone.
Another question you should ask yourself is: Every time i need this data, do I need the embedded objects (and vice-versa)? How many time this data will be updated? How many times this data will be read?
Think about a friendship request:
Each time you need the request you need the user which made the request, then embed the request inside the user document.
You will be able to create an index on the embedded object too, and your search will be mono query / fast / consistent.
Just a link to my previous reply on a similar question:
Mongo DB relations between objects
I think this post will be right for you http://www.mongodb.org/display/DOCS/Schema+Design
Use Cases
Customer / Order / Order Line-Item
Orders should be a collection. customers a collection. line-items should be an array of line-items embedded in the order object.
Blogging system.
Posts should be a collection. post author might be a separate collection, or simply a field within posts if only an email address. comments should be embedded objects within a post for performance.
Schema Design Basics
Kyle Banker, 10gen
http://www.10gen.com/presentation/mongosf2011/schemabasics
Indexing & Query Optimization
Alvin Richards, Senior Director of Enterprise Engineering
http://www.10gen.com/presentation/mongosf-2011/mongodb-indexing-query-optimization
**These 2 videos are the bests on mongoddb ever seen imho*
Populate() is just a query. So the overhead is whatever the query is, which is a find() on your model.
Also, best practice for MongoDB is to embed what you can. It will result in a faster query. It sounds like you'd be duplicating a ton of data though, which puts relations(linking) at a good spot.
"Linking" is just putting an ObjectId in a field from another model.
Here is the Mongo Best Practices http://www.mongodb.org/display/DOCS/Schema+Design#SchemaDesign-SummaryofBestPractices
Linking/DBRefs http://www.mongodb.org/display/DOCS/Database+References#DatabaseReferences-SimpleDirect%2FManualLinking

Resources