MongoDB index on ref is not taking effect - javascript

I have the following model
const PostSchema = new mongoose.Schema(
{
_id: mongoose.Schema.Types.ObjectId,
workSpace: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Workspace'
}
}
);
After getting complains of slow requests, I went over to my MongoDB Atlas dashboard in production and created an index on the Post documents like this
{
'workSpace':1
}
Yet, when I queried for posts like this
let skip=userDefined ?? 0;
let limit=1000;
let postsInWorkSpace = await Post.find({workSpace})
.sort({ created_at: 'desc' })
.skip(limit * page)
.limit(limit);
The above query still takes up to 50seconds before returning results.
So, is there something else I need to do to make the above query faster, because right now it seems the index is not working?
Thank you

after your schema definition you can try to add this line
PostSchema .index({ workSpace: 1 });

Related

How to find key with type objectId , ref to <some model>

I have created a Notes model having schema as shown below
const notesschema = new Schema({
user :{
type : Schema.Types.ObjectId,
ref : 'User',
required : true
},
problem : {
type : Schema.Types.ObjectId,
ref : 'Problems',
required : true
},
content : {
type : 'string'
}
},{
timestamps : true
})
To show the User his notes for a particular task/problem I am trying to fetch notes and show to him and possibly update if he do some changes and save, The problem is with this schema I dont know how to write <model.findById >API to find notes from my notes model having particular user and specific task/problem.Which I would know the Id of.
With this particular schema , and my current knowledge i would have to write So much code. So if there is any easier way to do this task is welcomed, I was also thinking to change my schema and just placing my user id in my schema instead of whole user and finding notes from my database
edit : as suggested by all the answers we can simply find using user.id which I thought initially would not word as that was just the path, but which stores actually user.id
You create the notes' collection the same way you're doing it,
const notesSchema = new mongoose.Schema({
user: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User', // # the name of the user model
required: true
},
problem: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Problem', // # the name of the user model
required: true
},
content: String
})
Then you'll create the model of the notesSchema as follows:
const NoteModel = mongoose.model('Note', notesSchema, 'notes')
export them so you can use them in your controllers:
module.exports = {
NoteModel,
notesSchema
}
or if you're using +es6 modules (think of, if you're using TypeScript):
export default {
NoteModel,
notesSchema
}
This will result in creating the following table (collection) in the database:
Let's think of the following challenges:
To get all the notes:
NoteModel.find({})
To get all the users:
UserModel.find({}) // you should have something like this in your code of course
To get all the problems:
ProblemModel.find({}) // you should have something like this in your code of course
To get all the notes of a user:
NotesModel.find({ user: USER_ID })
To search for notes by problems:
NotesModel.find({ problem: PROBLEM_ID })
Now, the above is how you do it in mongoose, now let's create a RESTFUL controller for all of that: (assuming you're using express)
const expressAsyncHandler = require('express-async-handler') // download this from npm if you want
app.route('/notes').get(expressAsyncHandler(async (req, res, next) => {
const data = await NotesModel.find(req.query)
res.status(200).json({
status: 'success',
data,
})
}))
The req.query is what's going to include the search filters, the search filters will be sent by the client (the front-end) as follows:
http://YOUR_HOST:YOUR_PORT/notes?user=TheUserId
http://YOUR_HOST:YOUR_PORT/notes?problem=TheProblemId
http://YOUR_HOST:YOUR_PORT/notes?content=SomeNotes
const notesschemaOfUser = await notesschema.findOne({user: user_id});

Mongoose: Count array elements

I have the following Schema with a array of ObjectIds:
const userSchema = new Schema({
...
article: [{
type: mongoose.Schema.Types.ObjectId,
}],
...
},
I will count the array elements in the example above the result should be 10.
I have tried the following but this doesn't worked for me. The req.query.id is the _id from the user and will filter the specific user with the matching article array.
const userData = User.aggregate(
[
{
$match: {_id: id}
},
{
$project: {article: {$size: '$article'}}
},
]
)
console.log(res.json(userData));
The console.log(article.length) give me currently 0. How can I do this? Is the aggregate function the right choice or is a other way better to count elements of a array?
Not sure why to use aggregate when array of ids is already with user object.
Define articles field as reference:
const {Schema} = mongoose.Schema;
const {Types} = Schema;
const userSchema = new Schema({
...
article: {
type: [Types.ObjectId],
ref: 'Article',
index: true,
},
...
});
// add virtual if You want
userSchema.virtual('articleCount').get(function () {
return this.article.length;
});
and get them using populate:
const user = await User.findById(req.query.id).populate('articles');
console.log(user.article.length);
or simply have array of ids:
const user = await User.findById(req.query.id);
console.log(user.article.length);
make use of virtual field:
const user = await User.findById(req.query.id);
console.log(user.articleCount);
P.S. I use aggregate when I need to do complex post filter logic which in fact is aggregation. Think about it like You have resultset, but You want process resultset on db side to have more specific information which would be ineffective if You would do queries to db inside loop. Like if I need to get users which added specific article by specific day and partition them by hour.

How can I make a private message schema for mongoose?

I am trying to make a messages schema + routes for my backend. I want that two users can write a message to each other and the message has to be stored for both of them.
I made the the user-model-schema and the user-routes, they are working but I'm stuck with the messaging.
mongoDB should contain the message
how can I manage sending messages?
Here is what I tried so far
messages-route:
var express = require("express");
var User = require("../models/users.js");
var router = express.Router();
const message = require("../models/messages");
router.post("/:recipient", (request, response) => {
User.find({
username: [request.body.sender, request.params.recipient],
// }, {
// message: request.body.message
// }, {
// upsert: false,
// new: true,
})
.then((users) => {
users.forEach((user) => {
user.updateMany({
message: request.body.message
})
})
response.status(200).json(users);
})
.catch((error) => {
response.status(500).json(error);
});
});
module.exports = router;
and my messages-schema:
var mongoose = require("mongoose");
var UserMessage = new mongoose.Schema({
user: { "type": mongoose.Schema.ObjectId, "ref": "User" },
username: String,
view: {
inbox: Boolean,
outbox: Boolean,
archive: Boolean
},
content: {type: String},
read: {
marked: { "type": Boolean, default: false },
date: Date
}
});
var schemaMessage = new mongoose.Schema.ObjectId({
from: String,
to: [UserMessage],
message: String,
created: Date
});
module.exports = mongoose.model("Messages", UserMessage);
I'm very unsure with the schema, I put in some suggestions I found here on stackoverflow.
Thanks!
I'm now making a messages Schema, and i thought of something like this:
const ChatSchema = new mongoose.Schema({
participants: [{type: mongoose.Schema.objectId, ref: "users"}],
last_message: Number,
_id: {type: mongoose.Schema.objectId}
//...
})
Here you could use the same schema to make chat groups, and it will work having a separate document for each message with a field with the chatroom _id, so you could query them
And you might be thinking, why not put all the messages in an array on ChatSchema?
Well, because we don't want the users to recieve all their messages each time they enter a chat.
I know, you might be thinking about something like this to prevent that over-load of data to the client
MessageSchema.find(({ ... })
.sort({ updatedAt: -1 })
.limit(20)
But here you're still recieving all the messages from the database to the server, and users could have even 1000 messages per chat, so an idea that came to my mind was making MessageSchema like this:
const MessageSchema = new mongoose.Schema({
message: String,
chatRoom_id: {type: mongoose.Schema.objectId},
sentBy: {type: mongoose.Schema.objectId, ref: "users"},
seenBy: [{ user: {type: mongoose.Schema.objectId, ref: "users"}, seen: Boolean }],
numberOfMessage: Number
//...
})
And the magic is with numberOfMessage, because you could query the MessageSchema with comparison operators like this:
const currentMessage = 151 //this is the numberOfMessage number of the last message the user saw, and when the user scrolls up the chat you could send the numberOfMessage of the last loaded message.
//To know from wich numberOfMessage start, just use the ``last_message`` field from the chatroom's ``ChatSchema``
const quantityNewMessages = 10 //this is the quantity of new messages you want the user to recieve
MessageSchema.find({
chatRoom_id,
numberOfMessage:
{ $lt: currentMessage , $gte: currentMessage + quantityNewMessages } //we use less than operator because the user will always recieve the last message first, so the previous messages are going to have a numberOfMessage less than this first message
})
And it will return us an array of 10 messages, wich you can concat to the messages array you already have in your frontend.
take note:
With this MessageSchema, all the users will share the same document for each message, you could make a separate document of a message for each user in a chatroom, so each user can delete a message without affecting the others.
I don't recommend you saving a username field, because the user could change it's username and if he does that, you would have to update ALL message documents with that username to the new username, so just leave the ref and populate it
You shouldn't put the document object in the to field, make it a separate document and only save it's ref. But I don't recommend doing it that way either
Don't use users.forEach because it modifies the array, use users.map because it returns a new array and doesn't mutates the original array
I see you have a bug, you are asking the data with the returned variable of the forEach( user => user.update(...) ), it should be map( user => UserSchema.update({ _id: user._id }) )
But still, looping an array and making a call to the DB each time is very expensive and will lag the server, so use something like the ChatSchema I showed you, because you could get the chatroom information, and with the _id of that chatroom, query the newest messages
I've not tested this code, i'm about to do it. If you run into any problem feel free to comment

Get collection by user id in MongoDB

The biggest problem is I'm totally new to MongoDB. I know how to do it in SQL but am unable to shift my thinking into NoSQL. I have this model:
var accountSchema = new mongoose.Schema({
isPremium: Boolean,
website: []
});
I'm using mongoose so it creates Id, username, and password automatically. My registered user looks like this:
{
_id: ObjectId('5a79c89b59b6042a5d89584b'),
websites: ['a.com', 'b.com', 'c.com'],
username: 'a#a.a',
isPremium: false,
hash:
'a long hash',
salt: 'a long salt',
__v: 0,
};
I want want to write out the websites array. I want to grab only the websites under a certain user. (Don't want others to just see all websites).
How would I do it? Would I pass the userId after a click or make it in a session? And would the 'query' look like?
You can do it like this.
First define your model as a separate module
var mongoose = require('mongoose');
const accountSchema = mongoose.Schema(
{
isPremium: Boolean,
website: []
});
module.exports = mongoose.model('account',accountSchema);
Then you can use this model everywhere in your code
const account = require('yourModulePath');
account.findOne({YouSearchParameters}).
then((account) => {
// let websites = account.website
// Do you logic
})
You also can filter result with .select
account.findOne({YouSearchParameters}).select({ "website": 1, "_id": 0}).then((account)
So you will just get your array of websites

Mongoose find/update subdocument

I have the following schemas for the document Folder:
var permissionSchema = new Schema({
role: { type: String },
create_folders: { type: Boolean },
create_contents: { type: Boolean }
});
var folderSchema = new Schema({
name: { type: string },
permissions: [ permissionSchema ]
});
So, for each Page I can have many permissions. In my CMS there's a panel where I list all the folders and their permissions. The admin can edit a single permission and save it.
I could easily save the whole Folder document with its permissions array, where only one permission was modified. But I don't want to save all the document (the real schema has much more fields) so I did this:
savePermission: function (folderId, permission, callback) {
Folder.findOne({ _id: folderId }, function (err, data) {
var perm = _.findWhere(data.permissions, { _id: permission._id });
_.extend(perm, permission);
data.markModified("permissions");
data.save(callback);
});
}
but the problem is that perm is always undefined! I tried to "statically" fetch the permission in this way:
var perm = data.permissions[0];
and it works great, so the problem is that Underscore library is not able to query the permissions array. So I guess that there's a better (and workgin) way to get the subdocument of a fetched document.
Any idea?
P.S.: I solved checking each item in the data.permission array using a "for" loop and checking data.permissions[i]._id == permission._id but I'd like a smarter solution, I know there's one!
So as you note, the default in mongoose is that when you "embed" data in an array like this you get an _id value for each array entry as part of it's own sub-document properties. You can actually use this value in order to determine the index of the item which you intend to update. The MongoDB way of doing this is the positional $ operator variable, which holds the "matched" position in the array:
Folder.findOneAndUpdate(
{ "_id": folderId, "permissions._id": permission._id },
{
"$set": {
"permissions.$": permission
}
},
function(err,doc) {
}
);
That .findOneAndUpdate() method will return the modified document or otherwise you can just use .update() as a method if you don't need the document returned. The main parts are "matching" the element of the array to update and "identifying" that match with the positional $ as mentioned earlier.
Then of course you are using the $set operator so that only the elements you specify are actually sent "over the wire" to the server. You can take this further with "dot notation" and just specify the elements you actually want to update. As in:
Folder.findOneAndUpdate(
{ "_id": folderId, "permissions._id": permission._id },
{
"$set": {
"permissions.$.role": permission.role
}
},
function(err,doc) {
}
);
So this is the flexibility that MongoDB provides, where you can be very "targeted" in how you actually update a document.
What this does do however is "bypass" any logic you might have built into your "mongoose" schema, such as "validation" or other "pre-save hooks". That is because the "optimal" way is a MongoDB "feature" and how it is designed. Mongoose itself tries to be a "convenience" wrapper over this logic. But if you are prepared to take some control yourself, then the updates can be made in the most optimal way.
So where possible to do so, keep your data "embedded" and don't use referenced models. It allows the atomic update of both "parent" and "child" items in simple updates where you don't need to worry about concurrency. Probably is one of the reasons you should have selected MongoDB in the first place.
In order to validate subdocuments when updating in Mongoose, you have to 'load' it as a Schema object, and then Mongoose will automatically trigger validation and hooks.
const userSchema = new mongoose.Schema({
// ...
addresses: [addressSchema],
});
If you have an array of subdocuments, you can fetch the desired one with the id() method provided by Mongoose. Then you can update its fields individually, or if you want to update multiple fields at once then use the set() method.
User.findById(userId)
.then((user) => {
const address = user.addresses.id(addressId); // returns a matching subdocument
address.set(req.body); // updates the address while keeping its schema
// address.zipCode = req.body.zipCode; // individual fields can be set directly
return user.save(); // saves document with subdocuments and triggers validation
})
.then((user) => {
res.send({ user });
})
.catch(e => res.status(400).send(e));
Note that you don't really need the userId to find the User document, you can get it by searching for the one that has an address subdocument that matches addressId as follows:
User.findOne({
'addresses._id': addressId,
})
// .then() ... the same as the example above
Remember that in MongoDB the subdocument is saved only when the parent document is saved.
Read more on the topic on the official documentation.
If you don't want separate collection, just embed the permissionSchema into the folderSchema.
var folderSchema = new Schema({
name: { type: string },
permissions: [ {
role: { type: String },
create_folders: { type: Boolean },
create_contents: { type: Boolean }
} ]
});
If you need separate collections, this is the best approach:
You could have a Permission model:
var mongoose = require('mongoose');
var PermissionSchema = new Schema({
role: { type: String },
create_folders: { type: Boolean },
create_contents: { type: Boolean }
});
module.exports = mongoose.model('Permission', PermissionSchema);
And a Folder model with a reference to the permission document.
You can reference another schema like this:
var mongoose = require('mongoose');
var FolderSchema = new Schema({
name: { type: string },
permissions: [ { type: mongoose.Schema.Types.ObjectId, ref: 'Permission' } ]
});
module.exports = mongoose.model('Folder', FolderSchema);
And then call Folder.findOne().populate('permissions') to ask mongoose to populate the field permissions.
Now, the following:
savePermission: function (folderId, permission, callback) {
Folder.findOne({ _id: folderId }).populate('permissions').exec(function (err, data) {
var perm = _.findWhere(data.permissions, { _id: permission._id });
_.extend(perm, permission);
data.markModified("permissions");
data.save(callback);
});
}
The perm field will not be undefined (if the permission._id is actually in the permissions array), since it's been populated by Mongoose.
just try
let doc = await Folder.findOneAndUpdate(
{ "_id": folderId, "permissions._id": permission._id },
{ "permissions.$": permission},
);

Categories

Resources