MongoDB schema design(mongoose): User, Event, several members and moderators by event - mongodb

I have an app with a mongoDB. A user can create events. An Event has one or more members, and one or more moderators as well. I am trying to decide which is the best schema for this app. I'm using mongoose. I tried to think the "mongo" way but I ended up with a highly relational schema.
var mongoose=require('mongoose');
//user log in using facebook, what should I store here?
var userSchema = new mongoose.Schema({
user data:......
});
var eventSchema= new mongoose.Schema({
name:String ,
description:String,
type:String,
user_id:Number,
created_at: {type:Date,default:Date.now}
});
var eventParticipantsSchema=new mongoose.Schema({
users:Array,
event_id:Number
});
var moderatorParticipantSchema=new mongoose.Schema({
users:Array,
event_id:Number
});
Which would be a more NoSQL schema?. I read about using the User as the main object containing array of events and within the events array a prop for an array of moderators and another prop of an array of members
Users{
events_array:{
moderators_array:
members_array:
}
}
but I read it was not recommended if you have to push into the contained arrays too many times. So, what would be a nice design for this app?.....Thank you very much....

Related

MongoDb: In a many to many relation, when does it make sense to make the ids of both models in the definition of the other model

I have this relation:
User has many courses
I have implemented it by integrating the user_id in the definition of the Course model:
const mongoose = require("mongoose");
const Schema = mongoose.Schema;
//Create Schema
const CourseSchema = new Schema({
user: {
//This will associate the user by his id
type: Schema.Types.ObjectId,
ref: "users",
},
date: {
type: Date,
default: Date.now,
},
});
module.exports = Course = mongoose.model("course", CourseSchema);
This way, if I want to get the courses created by a user X, I just use his id and look it up in the courses documents.
I thought with a big data base, this operation may be costly.
So I should add the course_id, to an array property called courses in the User model:
const mongoose = require("mongoose");
const Schema = mongoose.Schema;
const UserSchema = new Schema({
courses: [
{
type: Schema.Types.ObjectId,
ref: "course",
},
],
});
module.exports = User = mongoose.model("users", UserSchema);
This way, I can simply populate that array with the .populate operation, instead of going with the appraoach mentioned above.
I would like to know if my way of thinking makes sense.
And whether there other reasons for me to add the course_id, to courses property in the User model.
both approach is correctly I have seen both methods that uses
but when you want to using populate, you should update two collections(users and courses) when you want to insert a new Course, so you should use transaction because if one was updated and the other did not(An error occurred) rollback be done
the first thing to understand about mongoose population is that it is not magic, but just a convenience method that allows you to retrieve related information without doing it all yourself
if user id be index in couresSchema you can find() all courses very fast,
but the generally recommendation is to consider the data usage patterns of your application and choose what is best

how to instert populated documents without: casting it in ObjectId then populate

I would like to avoid this (see comments below):
var UserSchema = Schema({
name: String
});
var UserGroupSchema = Schema({
users: [ { type: Schema.Types.ObjectId, ref: 'user' } ]
});
var user = new User({ name: 'John' });
var userGroup = new userGroup();
userGroup.users.push(user); // auto cast user in its objectId ! How to avoid that ?
UserGroup.populate(userGroup, { path: 'users', model: 'User' }); // Get back the user object (this step should be obsolete)
I do not want to declare this:
var UserGroupSchema = Schema({
users: [ User ]
});
Because i want to be able to find my users without looking into UserGroups.
I really hope there is something to avoid that because i don't see how to write clean code this way.
Because i want to be able to find my users without looking into UserGroups.
Specifically that means that you can only use references, which means saving new users separately before pushing a reference onto (and saving) a UserGroup instance.
I assume that a user can belong to more than one UserGroup, which means using subdocs (the users : [ User ] variant) can't really be used anyway.

Embedded document without Array?

I understand how to embed documents in Mongoose, and they seem fairly straightforward if storing as arrays, for which the use case is fairly obvious:
var CommentSchema = new Mongoose.Schema({...});
var BlogPostSchema = new Mongoose.Schema({
comments : [CommentSchema],
});
But, what I don't see how to do after looking over the documentation forward and backward, is how to store a single sub-document that doesn't need or want to be in an Array.
var UserSchema = new Mongoose.Schema({...});
var BlogPostSchema = new Mongoose.Schema({
author: ??? // 'UserSchema' and UserSchema do not work here.
});
Is there any way to make this work? I don't want to just store the ObjectId, but rather, store a complete copy of the User record, but don't need or want an array.
You cannot embed schemas in this way, with the reasoning that those child docs would be confused with full documents, see this bug thread, where it is stated:
the reason we haven't added this support in the past is b/c this leaves us wondering if all pre-hooks will be executed the same way for the pseudo-child document as well as it implies that we can call save() on that child.
The answer here is to share not the schema, but just the definition.
var userdef = { name: String };
var UserSchema = new Schema(userdef);
var BlogPostSchema = new Schema({author: userdef});
This would result in a nested user object, without actually nesting the Schema.
Just sharing information doesn't support validation bubbling. And you may need validation of UserSchema also.
Instead I recommend array length validation
author: {type:[UserSchema], validate: function (arr) { return arr.length == 1 }},
UPDATE:
In case anyone comes across this now, as of Mongoose 4.2.0 single embedded subdocuments exist! :)
http://mongoosejs.com/docs/subdocs.html#single-embedded

Mongoose - update after populate (Cast Exception)

I am not able to update my mongoose schema because of a CastERror, which makes sence, but I dont know how to solve it.
Trip Schema:
var TripSchema = new Schema({
name: String,
_users: [{type: Schema.Types.ObjectId, ref: 'User'}]
});
User Schema:
var UserSchema = new Schema({
name: String,
email: String,
});
in my html page i render a trip with the possibility to add new users to this trip, I retrieve the data by calling the findById method on the Schema:
exports.readById = function (request, result) {
Trip.findById(request.params.tripId).populate('_users').exec(function (error, trip) {
if (error) {
console.log('error getting trips');
} else {
console.log('found single trip: ' + trip);
result.json(trip);
}
})
};
this works find. In my ui i can add new users to the trip, here is the code:
var user = new UserService();
user.email = $scope.newMail;
user.$save(function(response){
trip._users.push(user._id);
trip.$update(function (response) {
console.log('OK - user ' + user.email + ' was linked to trip ' + trip.name);
// call for the updated document in database
this.readOne();
})
};
The Problem is that when I update my Schema the existing users in trip are populated, means stored as objects not id on the trip, the new user is stored as ObjectId in trip.
How can I make sure the populated users go back to ObjectId before I update? otherwise the update will fail with a CastError.
see here for error
I've been searching around for a graceful way to handle this without finding a satisfactory solution, or at least one I feel confident is what the mongoosejs folks had in mind when using populate. Nonetheless, here's the route I took:
First, I tried to separate adding to the list from saving. So in your example, move trip._users.push(user._id); out of the $save function. I put actions like this on the client side of things, since I want the UI to show the changes before I persist them.
Second, when adding the user, I kept working with the populated model -- that is, I don't push(user._id) but instead add the full user: push(user). This keeps the _users list consistent, since the ids of other users have already been replaced with their corresponding objects during population.
So now you should be working with a consistent list of populated users. In the server code, just before calling $update, I replace trip._users with a list of ObjectIds. In other words, "un-populate" _users:
user_ids = []
for (var i in trip._users){
/* it might be a good idea to do more validation here if you like, to make
* sure you don't have any naked userIds in this array already, as you would
*/in your original code.
user_ids.push(trip._users[i]._id);
}
trip._users = user_ids;
trip.$update(....
As I read through your example code again, it looks like the user you are adding to the trip might be a new user? I'm not sure if that's just a relic of your simplification for question purposes, but if not, you'll need to save the user first so mongo can assign an ObjectId before you can save the trip.
I have written an function which accepts an array, and in callback returns with an array of ObjectId. To do it asynchronously in NodeJS, I am using async.js. The function is like:
let converter = function(array, callback) {
let idArray;
async.each(array, function(item, itemCallback) {
idArray.push(item._id);
itemCallback();
}, function(err) {
callback(idArray);
})
};
This works totally fine with me, and I hope should work with you as well

Searching embedded documents Mongoose + nodejs

Im new to Mongoose, and i'm facing a problem in searching.
These are my Schemas:
var CommentSchema = new Schema({
body : String
, comments : [CommentSchema]
});
var PostSchema = new Schema({
body : String
, comments : [CommentSchema]
});
There is a deep nesting of comments. When somebody answers to the existing comment, how can I find that one?
you can look at the mongoose test suite on github for examples.
model_querying_test
Here is what you are looking for:
test finding based on embedded document fields:
function () {
var db = start(), BlogPostB = db.model('BlogPostB', collection);
BlogPostB.create({comments: [{title: 'i should be queryable'}]}, function (err, created) {
should.strictEqual(err, null);
BlogPostB.findOne({'comments.title': 'i should be queryable'}, function (err, found) {
should.strictEqual(err, null);
found._id.should.eql(created._id);
db.close();
});
});
},
One solution to this is to store Comments as a separate Model which you can query directly, and store references to the related ObjectIds and paths between Comments and Posts.
Using the Populate feature in Mongoose related documents can function similarly to embedded documents, although there are some important differences in the way you query them, and you have to be more careful to keep the relationships populated.
Set it up like this:
var mongoose = require('mongoose')
, Schema = mongoose.Schema
, ObjectId = Schema.Types.ObjectId;
var PostsSchema = new Schema({
body : String,
stories : [{ type: ObjectId, ref: 'Story' }]
});
var CommentsSchema = new Schema({
body : String,
post : { type: ObjectId, ref: 'Post' },
comments : [{ type: ObjectId, ref: 'Comment' }]
});
var Story = mongoose.model('Post', PostsSchema);
var Comment = mongoose.model('Comment', CommentsSchema);
If you do it this way it requires more queries to get the post with all its comments (which will be slower than being able to load the Post and its complete comment hierarchy with a single query) however you'll be able to query comments directly and retrieve the Post they were made on (but not easily find the full path to the comment when its nested).
These are all trade-offs; the best decision (either to recursively search for comments, or store them independently then recursively load them) should be made in the context of your application and its expected usage patterns.
One other caveat; the populate feature is currently limited to a single-level of linked ObjectIds; you have to call it on each comment that is returned to get the full nested dataset. There are several plugins that help with this, such as mongoose-subpopulate, and soon enough it'll be supported natively in Mongoose - see the github issue here.