Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

how should i store likes/dislikes and rating in mongoose/mongdb using node.js

I have a schema for an item and a user. I want to allow a user to like or dislike an item and I also want to store a ratings association on the item as well as the user.

var UserSchema = new Schema({
    username    : { type: String, required: true, index: { unique: true }
   , likes : [{ type: Schema.ObjectId, ref: 'Item'}]
   , dislikes : [{ type: Schema.ObjectId, ref: 'Item'}]
   , ratings: [???]
});

var ItemSchema = new Schema({
    name: { type: String},
   , likes : [{ type: Schema.ObjectId, ref: 'User'}]
   , dislikes : [{ type: Schema.ObjectId, ref: 'User'}]
   , ratings: [???]
});

The user stores an item ref, and the item stores a user ref for likes/dislikes. I am not sure how to store ratings for as an attribute though, since I want both the user and the value they rated the item.

item.ratings.forEach(function(rating){
  <%= rating.user.username %> gave <%= item.name %> a <%= rating.value %>.
});

I also want to get a list of items a user has rated along with the rating value:

user.ratings.forEach(function(rating){
  <%= user.username %> has rated <%= rating.item.name %> and gave it a <%= rating.value %>
});

What should my "ratings" schema look like? Is it possible to store two values? a user object id and a rating value (integer) and have a collection of these?

The other problem I see with my method is that mongoose doesn't support deep populate yet, so I would have to either use a module for it (https://github.com/JoshuaGross/mongoose-subpopulate), that is largely un-tested or store it in a different manner that won't have more than one level of nesting, so I can get my data with .populate()

Any feedback is appreciated, as I'm new to noSQL and perhaps I'm overcomplicating this.

like image 553
chovy Avatar asked Oct 21 '12 03:10

chovy


2 Answers

You want to avoid double cross linking because it means there is twice as much to maintain and because Mongo does not support transactions it creates the possibility of half connected data when you expected it to be doubly connected. That being said I rethink your schemas like so:

var like = {
    itemid: { type: Schema.ObjectId, ref: 'Item'}
   , rating: Number
};

var UserSchema = new Schema({
    username    : { type: String, required: true, index: { unique: true }
   , likes : [like]
   , dislikes : [like]
});
UserSchema.index({ 'likes.itemid': 1 });
UserSchema.index({ 'dislikes.itemid': 1 });
var User = db.model('User', UserSchema);

var ItemSchema = new Schema({
    name: String
});
var Item = db.model('Item', ItemSchema);

You can add likes or dislikes by saying:

var piano = new Item({name: 'Mason & Hamlin Baby Grand Piano' });
var Joe = new User({ username: 'Joe_Smith' });
Joe.likes.push({ itemid: piano._id, rating: 10 });

And when you want to look up the likes for an item:

User.find({ 'likes.itemid': piano._id }, function(err, userLikes) {
    ...
});

And if none of that feels right to you then you can instead create a third collection...

var UserSchema = new Schema({
    username    : { type: String, required: true, index: { unique: true }
});
var User = db.model('User', UserSchema);

var ItemSchema = new Schema({
    name: String
});
var Item = db.model('Item', ItemSchema);

var LikesSchema = new Schema({
   , userid: { type: Schema.ObjectId, ref: 'User'}
   , itemid: { type: Schema.ObjectId, ref: 'Item'}
   , rating: Number
});
LikesSchema.index({ userid: 1, itemid: 1 }, { unique: true });

And in this case it is probably easiest to make the rule that a positive rating is a like and a negative rating is a dislike.

like image 162
Leon Stankowski Avatar answered Oct 12 '22 23:10

Leon Stankowski


I would do as you said. I would use a Rating schema:

var RatingSchema = new Schema({
   , _user : { type: ObjectId, ref: 'User' }
   , _item : { type: ObjectId, ref: 'Item' }
   , value : Integer
});

If you want to be able to access the ratings from the User schema, you would have to add a hook so that any saved RatingSchema is added to user.ratings.

var UserSchema = new Schema({
  /* ... */
  ratings: [{ type: Schema.ObjectId, ref: 'Rating'}]
});

RatingSchema.post('save', function () {
  // push this.id to this._user.ratings
  // save this._user
});

Regarding, "The other problem I see with my method is that mongoose doesn't support deep populate yet", if you don't want to use the mongoose-subpopulate hack, I suggest you refactor the loading of your models into static methods. For example:

UserSchema.statics.findByIdAndDeepPopulate = function (i, cb) {
  UserSchema.findOne(id)
    .exec(function(err, user) {
      if (err || !user) return cb(new Error('User not found'));
      if (user._ratings.length == 0) return cb(null, user);

      // Load and populate every ratings in user._ratings
      for(var i = 0; i < user._ratings.length; i++) {
        function(i) {
          RatingSchema
            .populate('_item')
            .exec(err, function(rating) {
              user._ratings[i] = rating;
              if (i == user._ratings.length) return cb(null, user);
            });
        }(i);
      }
    });
}

EDIT: Now that I think about it again, why not simply store the ratings as an embed document in UserSchema?

var UserSchema = new Schema({
  /* ... */
  ratings: [ RatingSchema ]
});

You could then simply populate this way:

UserSchema.findOne(id)
  .populate('ratings._item')
  .exec(function(err, user) {
    if (err || !user) return next(new Error('User not found'));

    console.log(user.ratings);
  });
like image 6
Olivier Lalonde Avatar answered Oct 13 '22 01:10

Olivier Lalonde