2

In my app I have Students who can Enroll in Classes.

An Enrollment records the start and end dates along with a reference to which Class.

The Class has details like a class name and description.

Student (1) - (N) Enrollment (1) - (1) Class

Therefore

Student (1) - (N) Class

This is what I would like the object to look like.

{
  "studentId": 1,
  "name": "Henrietta",
  "enrolledClasses": [
    {
      "enrollmentId": 12,
      "startDate": "2015-08-06T17:43:14.000Z",
      "endDate": null,
      "class": 
        { 
          "classId": 15,
          "name": "Algebra",
          "description": "..."
        }
    },
      "enrollmentId": 13,
      "startDate": "2015-08-06T17:44:14.000Z",
      "endDate": null,
      "class": 
        {
          "classId": 29,
          "name": "Physics",
          "description": "..."
        }
    }
}

But my RESTful API cannot (easily) output the full nested structure of the relationship because it's ORM can't do Many to Many with Through relationships. So I get the student and their multiple enrollments, but only a reference to the class for each, not the details. And I need to show the details, not just an Id.

I could wait for the student object to be available and then use the references to make additional calls to the API for each classId to get details, but am not sure how, or even if, to then integrate it with the student object.

This is what I have so far.

function findOne() {
  vm.student = StudentsResource.get({
    studentId: $stateParams.studentId
  });
};

This is what I get back from my API

{
  "studentId": 1,
  "name": "Henrietta",
  "enrolledClasses": [
    {
      "enrollmentId": 12,
      "startDate": "2015-08-06T17:43:14.000Z",
      "endDate": null,
      "class": 15
    },
      "enrollmentId": 13,
      "startDate": "2015-08-06T17:44:14.000Z",
      "endDate": null,
      "class": 29
    }
}

And for the class details I can them one at a time but haven't figured out how to wait until the student object is available to push them into it. Nor how to properly push them...

{ 
  "classId": 15,
  "name": "Algebra",
  "description": "..."
}

{
  "classId": 29,
  "name": "Physics",
  "description": "..."
}

Question

Is it good practice to combine the student and class(es) details through the enrollments, as one object?

  • If so, whats the most clear way to go about it?
  • If not, what is another effective approach?

Keep in mind that the app will allow changes to the student details and enrollments, but should not allow changes to the details of the class name or description. So if Angular were to send a PUT for the object, it should not try to send the details of any class, only the reference. This would be enforced server side to protect the data, but to avoid errors, the client shouldn't try.

For background, I'm using SailsJS and PostgreSQL for the backend API.

shanemgrey
  • 2,280
  • 1
  • 26
  • 33
  • How many classes in the system? Can you get all of them in one request? – Ori Drori Aug 06 '15 at 21:08
  • A lot of this depends on what you need to do with this in the app. Chances are you don't need the data combined and can make requests for details on demand. Or, as suggested above get all the classes up front and do lookups as needed. A hashmap using class ID as keys would make lookup very simple. If you do want them combined it wouldn't be difficult to remove the details in an interceptor when sending updates to server – charlietfl Aug 06 '15 at 21:34
  • @OriDrori In the case of this example, any given student would only enroll in less than 10 classes at a time. But over the years, that could increase to less than 50. – shanemgrey Aug 06 '15 at 21:44
  • As @charlietfl said - I would get a list of all classes at app load, hash them, and use as needed. Anyhow, you don't to combine them with the student object to display the info. – Ori Drori Aug 06 '15 at 21:51
  • @charlietfl I'm looking for ideas for making this clean and understandable. It seems sloppy to manually tack on an object to replace the reference value, only to have to catch and strip it later. But it also seems sloppy to not be storing all three layers of depth in one object when the 1-to-Many relationship between Class and Enrollments is clear. This M-to-M through an association seems a common enough need that there would be a well established pattern. I just haven't found it yet. – shanemgrey Aug 06 '15 at 21:52
  • I wouldn't replace the reference value if you did combine them. Just add an extra property that is simple to remove. Data transforms are not unusual but a lot depends on use case – charlietfl Aug 06 '15 at 21:55

1 Answers1

2

So basically on the Sailsjs side you should override your findOne function in your StudentController.js. This is assuming you are using blueprints.

// StudentController.js
module.exports = {
  findOne: function(req, res) {
    Student.findOne(req.param('id')).populate('enrollments').then(function(student){
      var studentClasses = Class.find({
        id: _.pluck(student.enrollments, 'class')
      }).then(function (classes) {
        return classes
      })
      return [student, classes]
    }).spread(function(student, classes){
      var classes = _.indexBy(classes, 'id')
      student.enrollments = _.map(student.enrollments, function(enrollment){
        enrollment.class = classes[enrollment.class]
        return enrollment
      })
      res.json(200, student)
    }).catch(function(err){
      if (err) return res.serverError(err)
    })
  }
}

// Student.js

module.exports = { 
  attributes: {
    enrollments: {
      collection: 'enrollment',
      via: 'student'
    } 
    // Rest of the attributes..   
  }
}

// Enrollment.js

module.exports = {
  attributes: {
    student: {
      model: 'student'
    }
    class: {
      model: 'class'
    }
    // Rest of the attributes...
  }
}

// Class.js

module.exports: {
  attributes: {
    enrollments: {
      collection: 'enrollment',
      via: 'class'
    }
    // Rest of the attrubutes
  }
}

Explanation:
1. the _.pluck function using Lo-dash retuns an array of classIds and we find all of the classes that we wanted populated. Then we use the promise chain .spread(), create an array indexed by classid and map the classIds to the actual class instances we wanted populated into the enrollments on the student returned from Student.findOne().
2. Return the student with enrollments deep populated with the proper class.

source: Sails.js populate nested associations

Community
  • 1
  • 1
willjleong
  • 153
  • 6
  • For the Read action, this works great (after the .then correction). But a side effect is that when the client removes an enrollment from a student record, the record for it in the database isn't removed. It's orphaned with a null value for the student and still shows up in a list of enrollments for the class, but with null value studentId. Seems I'd have to override the Update blueprint to handle this too. And now it strikes me as hacky again. As a different approach I'm looking at using the enrollment model as it's own angular controller inside the student and class controllers. – shanemgrey Aug 13 '15 at 19:10
  • When you remove a record why don't you call the delete /enrollment/:id method rather than updating the Student record. This is automatically cascade delete the associated enrollments in the Student record. Another solution is to not actually delete and have a field called isDeleted and make it false. You then have to override all blueprints to always query with isDeleted == true. Unfortunately, these things are not super easy in Sails as they are in rails. Once you write the code once though it's easy to repurpose. – willjleong Aug 14 '15 at 00:50