I have built a simple MERN app where users can rate phone numbers. Users just fill in the phone number, choose rating (1 - 5 star rating), their city & short text. The app has search function with filter & sorting options. It all works good enough ATM but I think it might break when multiple concurrent users use the website because I update the phone number model (mobileSchema) after a rating (messageSchema) has been submitted - using Mongoose middlewares (post hooks).
For example, I need to calculate number of ratings (messagesCount) for phone number. I use Message.countDocuments({ mobile: mobile._id }) for that. However, I also need to update other properties of phone number (mobileSchema - lastMessageDate, globalRating, averageRating) so that operation takes some time. I believe the number of ratings might not be right when 2 users submit rating at the same time - it will increment the number of ratings (messagesCount) by 1 instead of 2.
Is there a better approach? Can a post hook be fired after the previous post hook already finished?
Sample code:
const mobileSchema = new Schema({
number: { type: String, required: true },
plan: { type: String, required: true },
date: { type: Date, default: Date.now, required: true, index: 1 },
messagesCount: { type: Number, default: 0, index: 1 },
lastMessageDate: { type: Date, index: 1 },
// normal mean
globalRating: { type: Number, default: 0, index: 1 },
// weighted mean
averageRating: { type: Number, default: 0, index: 1 }
});
const messageSchema = new Schema({
comment: { type: String, required: true },
city: { type: Schema.Types.ObjectId, ref: 'City', required: true, index: 1 },
rating: { type: Number, required: true, index: 1 },
date: { type: Date, default: Date.now, required: true, index: 1 },
mobile: { type: Schema.Types.ObjectId, ref: 'Mobile', required: true },
user: { type: Schema.Types.ObjectId, ref: 'User', required: true }
});
messageSchema.post('save', function (message, next) {
const messageModel = this.constructor;
return updateMobile(messageModel, message, next, 1);
});
const updateMobile = (messageModel, message, next, addMessage) => {
const { _id } = message.mobile;
const cityId = message.city._id;
const lastMessageDate = message.date;
let mobile;
hooks.get(Mobile, { _id })
.then(mobileRes => {
mobile = mobileRes;
return Message.countDocuments({ mobile: mobile._id })
})
.then(messagesCount => {
if (messagesCount <= 0) {
const deleteMobile = Mobile.findOneAndDelete({ _id: mobile._id })
const deleteSeen = SeenMobile.findOneAndDelete({ mobile: mobile._id, user: message.user._id })
const cityMobile = updateCityMobile(messageModel, mobile, cityId)
Promise.all([deleteMobile, deleteSeen, cityMobile])
.then(() => {
return next();
})
.catch((err) => {
console.log(err);
return next();
})
}
else {
if (addMessage === -1) lastMessageDate = mobile.lastMessageDate;
const ratings = hooks.updateGlobalRating(mobile, messageModel)
.then(() => hooks.updateAverageRating(mobile, messageModel))
.then(() => {
return new Promise((resolve, reject) => {
mobile.set({
messagesCount,
lastMessageDate
});
mobile.save((err, mobile) => {
if (err) return reject(err);
resolve();
});
})
})
const cityMobile = updateCityMobile(messageModel, mobile, cityId)
Promise.all([ratings, cityMobile])
.then(([ratings, cityMobile]) => {
return next();
})
.catch(err => console.log(err))
}
})
.catch(err => {
console.log(err);
})
}
I think you are always going to run into async issues with your approach. I don't believe you can "synchronize" the hooks; seems to go against everything that is true about MongoDB. However, at a high level, you might have more success grabbing the totals/summaries at run-time, rather than trying to keep them always in sync. For instance, if you need the total number of messages for a given mobile device, why not:
Messages.find({mobile: mobile._id})
and then count the results? That will save you storing the summaries and keeping them updated. However, I also think your current approach could work, but you probably need to scrap the "countDocuments". Something a bit more async friendly, like:
Mobile.aggregation([
{ $match: { _id: mobile._id } },
{ $add: [ "$mobile.messagesCount", 1 ] }
]);
Ultimately I think your design would be strengthened if you stored Messages as an array inside of Mobile, so you can just push the message on it. But to answer the question directly, the aggregation should keep everything tidy.
I found this answer: Locking a document in MongoDB
I will calculate all the values I need (messagesCount, globalRating etc.) in post hook and then I will check if the mobile document has the same __v (version) value during final findOneAndUpdate operation (because this operation locks the document and can increment __v). If it has different __v then I will call the post hook again to ensure it will calculate the right values.
First we need to fix some database structure here
Mobile schema
const mobileSchema = new Schema({
number: { type: String, required: true },
plan: { type: String, required: true },
date: { type: Date, default: Date.now, required: true, index: 1 },
//messagesCount: { type: Number, default: 0, index: 1 },
//lastMessageDate: { type: Date, index: 1 },
// normal mean
//globalRating: { type: Number, default: 0, index: 1 },
// weighted mean
//averageRating: { type: Number, default: 0, index: 1 }
});
Message schema
const messageSchema = new Schema({
comment: { type: String, required: true },
city: { type: Schema.Types.ObjectId, ref: 'City', required: true, index: 1 },
//rating: { type: Number, required: true, index: 1 },
date: { type: Date, default: Date.now, required: true, index: 1 },
mobile: { type: Schema.Types.ObjectId, ref: 'Mobile', required: true },
user: { type: Schema.Types.ObjectId, ref: 'User', required: true }
});
Rating system (take all rating or make them a set)
(numerator & denominator after 100 ratings it is difficult to read every single one) can also check for the mobile
const ratingSchema = new Schema({
mobile: { type: String, required: true },
commmentId:{type:String, required: true, index: 1}
rate: { type: Number required: true, },
//rating: { type: Number, required: true, index: 1 },
timestamp: { type: Date, default: Date.now, required: true, index: 1 }
denominator:{ type: Number},
numerator:{type:Number}
});
Thanks
Related
I am trying to display all the data with the "populate" statement. But I only get one "populate" but when I put many it doesn't work.
what I want to do is to bring me the data from the "User", "Customer" model as well.
This is my code
My Model:
import { model, Schema } from 'mongoose';
const RoomSchema = new Schema({
Users: [
{
agentId: {
type: Schema.Types.ObjectId,
ref: "User",
required: false,
},
customerId: {
type: Schema.Types.ObjectId,
ref: "Customer",
required: false,
},
typeId: Number, // 1 - agent, 2 - client
},
],
Messages: [
{
agentId: {
type: Schema.Types.ObjectId,
ref: "User",
required: false,
},
customerId: {
type: Schema.Types.ObjectId,
ref: "Customer",
required: false,
},
message: {
type: String,
required: true,
},
date: Date,
sender: Number,
},
],
FinishAt: Date,
FinishBy: String,
typeFinishBy: Number, // 1 - agent, 2 - client
}, {
timestamps: true,
versionKey: false
});
export default model('Room', RoomSchema);
this is the sentence I am using
import Room from "../models/Room.js";
async function getOnlyRoom(id) {
const foundRoom = await Room.findById(id)
.populate('Users.agentId')
.populate('Users.customerId')
.populate('Messages.agentId')
.populate('Messages.customerId')
.execPopulate();
return foundRoom
}
Image of Json Postman
only works with one
foundRoom.populate('Users.customerId')
Works with only populate
this is the error
image of error
Thank you very much for your help
I have found the solution to my problem
I was researching the solution on the mongoose website and did it with a single populate.
const populateRoom = await foundRoom
.populate([
{ path: 'Users.agentId', select: 'name' },
{ path: 'Users.customerId', select: 'name' },
{ path: 'Messages.agentId', select: 'name' },
{ path: 'Messages.customerId', select: 'name' }
])
return populateRoom;
Im struggling with this. I have in my db Profile{name: string, table:0}, and im using a for loop to asign the number of the table to the different users, but this is not working. The for loop works fine if i checked in the console, but in the postman request, is not working.
When i filter by table number from the database, there are more than 5 results per table. And in somecases, there are irregular, like, table 1: 8 users, table 2: 5 users, so on...
const ProfileSchema = new Schema(
{
name: {
type: String,
required: true,
},
password: {
type: String,
required: true,
},
moderator: {
type: Boolean,
default: false,
},
email: {
type: String,
required: true,
},
country: {
type: String,
required: true,
},
institution: {
type: String,
/* required: true */
},
score: {
type: [Number],
default: [],
},
active: {
type: Boolean,
required: true,
default: false,
},
img: String,
meetLink: String,
table: {
type: Number,
default: 0,
},
group: Number,
},
routes/index.js
router.put('/asignTable', async (req, res) => {
let users = await Profile.find()
shuffle(users)
asignTable(users)
res.send(users)
routes/utils.js
async function asignTable(users) {
var contador = 0
let numTable = 1
filter = {}
for (const user of users) {
const filter = { name: user.name }
const update = { table: numTable }
if (contador == 5) {
contador = 0
numTable++
console.log(`desde el if ${contador}`)
} else if (contador < 5) {
await Profile.findOneAndUpdate(filter, update).then(contador++)
console.log(`desde el elseif contador:${contador} numtable:${numTable}`)
}
}
}
})
I need to Populate courses of StudentSchema with the courses (Object_id) from CoursesSchema that belong to the major same as students major
let StudentSchema = new Schema({
_id: new Schema.Types.ObjectId,
emplId: {
type: Number,
required: true
},
major:{
type: String,
required: true
},
courses:[{
type: mongoose.Schema.Types.ObjectId,
ref: 'courses',
grade:{
type: String,
required: false,
}
}],
});
const courseSchema = new mongoose.Schema({
code: {
type: String,
required: true
},
title: {
type: String,
required: true
},
//array of majors that a courses is required for e.g: ['CS', 'CIS']
major: {
type: Array,
required: false,
},
//
CIS:{
type: Boolean,
required: false,
},
CNT:{
type: Boolean,
required: false,
},
CS:{
type: Boolean,
required: false,
},
GIS:{
type: Boolean,
required: false,
},
})
What do I do?
StudentCoursesRouter.get('/studentcourses', (req, res) => {
Courses.find({CS: true}, (err, courses) => {
if ( err ) {
console.log('Error occured while getting records');
res.json(err);
} else {
courseMap = {}
courses.forEach(function(course) {
courseMap[course._id] = course._id;
});
//res.send(courses);
Students.find({empleId: 12345678}).courses.push(courses);
}
res.json(Students);
})
This is what i am doing but it is not populating courses of student and gives an empty array for courses.
API Request Response Screenshot
You mention populate but you are not using populate?
e.g. Students.find({empleId: 12345678}).populate('course')
If you want it lean u also need to install mongoose-lean-virtuals
e.g. Students.find({empleId: 12345678}).populate('course').lean({ virtuals: true })
In MongoDB's documentation it is suggested to put as much data as possible in a single document. It is also suggested NOT to use ObjectId ref based sub-documents unless the data of those sub-documents must be referenced from more than one document.
In my case I have a one-to-many relationship like this:
Log schema:
const model = (mongoose) => {
const LogSchema = new mongoose.Schema({
result: { type: String, required: true },
operation: { type: Date, required: true },
x: { type: Number, required: true },
y: { type: Number, required: true },
z: { type: Number, required: true }
});
const model = mongoose.model("Log", LogSchema);
return model;
};
Machine schema:
const model = (mongoose) => {
const MachineSchema = new mongoose.Schema({
model: { type: String, required: true },
description: { type: String, required: true },
logs: [ mongoose.model("Log").schema ]
});
const model = mongoose.model("Machine", MachineSchema);
return model;
};
module.exports = model;
Each Machine will have many Production_Log documents (more than one million). Using embedded documents I hitted the 16mb per document limit very quickly during my tests and I couldn't add any more Production_Log documents to the Machine documents.
My doubts
Is this a case where one should be using sub-documents as ObjectId references instead of embedded documents?
Is there any other solution I could evaluate?
I will be accessing Production_Log documents to generate stats for each Machine using the aggregation framework. Should I have any extra consideration on the schema design?
Thank you very much in advance for your advice!
Database normalization is not applicable to MongoDB
MongoDB scales better if you store full information in the single document (Data redundancy). Database normalization obligate split data in different collections, but once growth your data, it will cause bottlenecks issues.
Use only LOG Schema:
const model = (mongoose) => {
const LogSchema = new mongoose.Schema({
model: { type: String, required: true },
description: { type: String, required: true },
result: { type: String, required: true },
operation: { type: Date, required: true },
x: { type: Number, required: true },
y: { type: Number, required: true },
z: { type: Number, required: true }
});
const model = mongoose.model("Log", LogSchema);
return model;
};
Read / Write operation scales fine in this way.
With Aggregation you can process data and compute desired result.
Please see if this approach suits your need
The Log collection would be having more data generated whereas the Machine collection never exceed 16MB. Instead of embedding Log collection into Machine collection try the vice versa.
Your modified schema would be like this
Machine schema:
const model = (mongoose) => {
const MachineSchema = new mongoose.Schema({
model: { type: String, required: true },
description: { type: String, required: true }
});
const model = mongoose.model("Machine", MachineSchema);
return model;
};
module.exports = model;
Log schema:
const model = (mongoose) => {
const LogSchema = new mongoose.Schema({
result: { type: String, required: true },
operation: { type: Date, required: true },
x: { type: Number, required: true },
y: { type: Number, required: true },
z: { type: Number, required: true },
machine: [ mongoose.model("Machine").schema ]
});
const model = mongoose.model("Log", LogSchema);
return model;
};
If still we are overshooting the size of Document(16MB) then in the Log Schema we can create a new document for every Day/Hour/Week depending on the logs we are generating.
I have a simple help desk app I've been building, where user can make request for site changes. One of the features is being able to see all request made by a specific person, which is working. However on that page I wanted to have something akin to "User's Request" where user is the person's page you are on. However I can't seem to get it to work without some weird issues. If I use:
{{#each request}}
{{user.firstName}}'s Request
{{/each}}
It works but I end up with the header being written as many times as the user has request. However, when I tried:
{{request.user.firstName}}
It returns nothing.
My route is populating user data, so I think I should be able to reference it directly. Here's the route:
// list Request by User
router.get('/user/:userId', (req, res) => {
Request.find({user: req.params.userId})
.populate('user')
.populate('request')
.then(request => {
res.render('request/user', {
request: request,
});
});
});
Here's the schema:
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
// Create Schema
const RequestSchema = new Schema({
title: {
type: String,
required: true,
},
body: {
type: String,
required: true,
},
status: {
type: String,
default: 'new',
},
priority: {
type: String,
default: 'low',
},
project: {
type: String,
default: 'miscellaneous',
},
category: {
type: String,
default: 'change',
category: ['change', 'bug', 'enhancement', 'investigation', 'minor_task', 'major_task', 'question'],
},
organization: {
type: String,
default: 'any',
},
assignedUser: {
type: String,
default: 'venkat',
},
allowComments: {
type: Boolean,
default: true,
},
user: {
type: Schema.Types.ObjectId,
ref: 'users',
},
lastUser: {
type: Schema.Types.ObjectId,
ref: 'users',
},
date: {
type: Date,
default: Date.now,
},
lastUpdate: {
type: Date,
default: Date.now,
},
comments: [{
commentBody: {
type: String,
required: true,
},
commentDate: {
type: Date,
default: Date.now,
},
commentUser: {
type: Schema.Types.ObjectId,
ref: 'users',
},
}],
});
// Create collection and add Schema
mongoose.model('request', RequestSchema);
The rest of the code is at: https://github.com/Abourass/avm_req_desk
If anyone is wondering how, the answer was to add the array identifier to the dot path notation:
<h4>{{request.0.user.firstName}}'s Request</h4>