1

I am working with a MongoDB database whose collections model classes, students, subjects, and [academic] performance. Below are the Mongoose based schema and models:

var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var ObjectId = Schema.Types.ObjectId;

// SUBJECT collection
var subjectSchema = new Schema({
    name        : { type: String, required: true },
    category    : { type: String, required: true }
});
var Subject = mongoose.model('Subject', subjectSchema);

// STUDENT collection
var studentSchema = new Schema({
    name        : { type: String, required: true },
    grade       : { type: Number, required: true }
});
var Student = mongoose.model('Student', studentSchema);

// PERFORMANCE collection
var performanceSchema = new Schema({
    subjectId   : { type: ObjectId, ref: Subject.modelName, required: true },
    score       : { type: Number, required: true },
    maximum     : { type: Number, required: true },
    grade       : { type: String, required: true }
});
var Performance = mongoose.model('Performance', performanceSchema);

// *Note*: This is just to use as a sub-document schema, and not as a collection
var classStudentSchema = new Schema({
    studentId   : { type: ObjectId, ref: Student.modelName, required: true },
    performance : [performanceSchema]
}, { _id: false });

// CLASS collection
var classSchema = new Schema({
    name        : { type: String, required: true },
    scores      : [classStudentSchema]
});
var Class = mongoose.model('Class', classSchema);

The class collection's documents are the most complex of the lot; an example document would be:

{
  "_id" : ObjectId("57758f15f68da08c254ebee1"),
  "name" : "Grade 5 - Section A",
  "scores" : [{
    "studentId" : ObjectId("5776bd36ffc8227405d364d2"),
    "performance": [{
      "subjectId" : ObjectId("577694ecbf6f3a781759c54a"),
      "score" : 86,
      "maximum" : 100,
      "grade" : "B+"
    }, {
      "subjectId" : ObjectId("5776ffe1804540e29c602a62"),
      "score" : 74,
      "maximum" : 100,
      "grade" : "A-"
    }]
  }, {
    "studentId" : ObjectId("5776bd36ffc8227405d364d5"),
    "performance": [{
      "subjectId" : ObjectId("577694ecbf6f3a781759c54a"),
      "score" : 94,
      "maximum" : 100,
      "grade" : "A"
    }, {
      "subjectId" : ObjectId("5776ffe1804540e29c602a62"),
      "score" : 81,
      "maximum" : 100,
      "grade" : "A"
    }]
  }]
}

I was able to retrieve an existing class document and add a student to it's scores using the following code:

Class.findOne({ name: 'Grade 5 - Section A' }, function(err, class) {
  if (err) throw err;
  Student.findOne({ name: 'John Doe' }, function(err, student) {
    if (err) throw err;
    class.scores.push({
      studentId: student.id
    });
  };
});

But how do I add/update/delete that particular student's performance? I need to be able to interact with the class collection in the following ways:

  • Retrieve the scores for all students, or for a specific student (retrieve specific element in the scores array)
  • Add/ update/ delete a specific student's score, for a specific subject (in case of update or delete, retrieve a specific element in the scores[n].performance array; for add, append to the same array.
Web User
  • 7,438
  • 14
  • 64
  • 92

1 Answers1

2

There are a few way to do this I'll answer point by point

Retrieve the scores for all students, or for a specific student (retrieve specific element in the scores array)

Class.findOne({ name: 'Grade 5 - Section A'})
     .populate('scores.studentId')
     .exec(function(err, class) {
       if (err) throw err;
       //now class.scores.studentId becomes ObjectStudent
       //hence you have all scores for all students
});

Add/ update/ delete a specific student's score, for a specific subject (in case of update or delete, retrieve a specific element in the scores[n].performance array; for add, append to the same array.

Class.findOneAndUpdate({name: 'Grade 5 - Section A'
                        ,'scores.studentId': ObjectId('5776bd36ffc8227405d364d2')
                        , 'scores.performance.subjectId' : ObjectId('577694ecbf6f3a781759c54a')}
                        , {$set: {scores.performance. score: 50}}
                        , function(err, data) {
           if (err) throw err
    });

I hope that helps

Tim
  • 2,695
  • 3
  • 33
  • 39
  • First of all, the first part of your answer worked just great. I don't exactly understand how `populate('scores.studentId')` retrieves the performances as well for each student; seems very powerful. I have some questions: (a) how do I fetch a particular subject's score for a single student? (b) How do I add a new student to `scores` array with an empty `performance` array? (c) How do I add the performance on a single subject for a specific student? (d) and (e) are like (b) and (c) but removing instead of adding. You have given me an idea on how to update. Thanks a bunch for your help! – Web User Jul 02 '16 at 06:52
  • 1
    No worries glad to help. At the end of the day minimum document that you can pull out is class. If you want to pull particular fields you can pass them as second parameter inside find. findOneAndUpdate(query, {'scores.performance.grade' : 1}, function(err){}). To add scores just use 'push' operator inside mongoose model i.e. model.scores.push({studentId: ObjectId('xxxxxxxxx'), performance:[]}) or use find and update and use $push operator either or second method is better because it's atomic. – Tim Jul 02 '16 at 08:04
  • can I use populate to resolve all the foreign key references throughout the hierarchy, beyond `scores.studentId`. Doing this allowed me to get the student corresponding to the `studentId` reference and similarly, I want to get the entire subject document corresponding to the `subjectId` reference each student's `performance` array? My goal is the fetch a completely de-normalized `class` document, if I can use that term in the context of MongoDB! – Web User Jul 05 '16 at 15:06
  • Yes, you can do that, just remember each populate is a separate query to mongoose. – Tim Jul 05 '16 at 23:27
  • Can you please suggest how to perform a multi-level populate? I hope it can be done in a single statement, so `studentId` resolves to the corresponding _student_ and `subjectId` resolves to the corresponding _subject_ in the object returned from the query. – Web User Jul 06 '16 at 15:40
  • I was unable to update. Here is the error I am getting: `MongoError: cannot use the part (sceneLocations of scores.performance.score) to traverse the element ({scores: [ { studentId: ObjectId('5776bd36f...`. I am wondering if positional operator `$` is needed, but with the update I am trying to perform two levels deep, multiple positional operators will be needed, which MongoDB does not support. – Web User Jul 15 '16 at 07:33