How to update MongoDB documents with arrays of sub-documents
Asked Answered
G

1

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.
Girasol answered 2/7, 2016 at 0:17 Comment(0)
R
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

Rematch answered 2/7, 2016 at 5:32 Comment(6)
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!Girasol
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.Rematch
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!Girasol
Yes, you can do that, just remember each populate is a separate query to mongoose.Rematch
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.Girasol
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.Girasol

© 2022 - 2024 — McMap. All rights reserved.