Ankit Kataria
Web Developer - Security Enthusiast

Init: Assignment Grading

Task.new

This week I was working on adding assignment grading functionality to the platform. It makes the lives of the course instructors very easy but at the same time means a lot of trouble for students as their assignments will now be open to scrutiny.

There are currently 2 grading scales supported - by letter (A-F) and by percent (1-100). The instructor will have to select those during the creation of the assignment. Once the deadline of the assignment has passed the grading form will be open for the instructor where he/she will be required to enter the grades. Once finalized the grades will be visible to the students. The work for the above feature can be found at #390

Problem.tackled("n+1 queries")

While working the above feature, there were several instances where I had to tackle n+1 problem while querying for the submissions for a group member or grades corresponding to those submissions. To tackle such problems in Rails, there are primarily two tools at your disposal-

includes

Using includes a minimum of 2 queries are executed to get all the records that are associated to an active-record object. For example, the queries produced for Project.all.includes(:author) are:

SELECT "projects".* FROM "projects"
SELECT "users".* FROM "users" WHERE "users"."id" IN (2, 1)

Nested relations can also be loaded using includes like so:

$ Assignment.where(group_id: <group-id>).includes(projects: :grade)
SELECT "assignments".* FROM "assignments" WHERE "assignments"."group_id" = $1  [["group_id", 1]]
SELECT "projects".* FROM "projects" WHERE "projects"."assignment_id" = 1 
Grade Load (4.0ms)  SELECT "grades".* FROM "grades" WHERE "grades"."project_id" = 1

This alone solves most of our n+1 problems. For the other tricky cases we have joins.

joins

In order to learn joins, let’s take the example of an issue that I had come across earlier. It was to solve the n+1 count queries for the group members on the user’s group page #364. For this issue we needed the count of all the group members that were mentored by the currently logged in user. The association hierarchy is as follows - every group has_one mentor foreign_key: mentor_id, and every group has_many group members foreign_key: group_id.

Starting of with the currently logged in @user, we can get the groups that the user mentors by:

$ Group.joins(:mentor).where(mentor: @user)

This produces an inner join query. An inner join gives data from the intersection of both the left and the right tables. The query produced is -

SELECT "groups".* FROM "groups" INNER JOIN "users" ON "users"."id" = "groups"."mentor_id" WHERE "groups"."mentor_id" = 1

Now that we have the groups that the user mentors, the group members can be obtained by a left outer join query. A left outer join includes all the data from left table and the matched records on the right table(i.e. the intersection).

So the final active record query became:

$ @groups = Group.where(id: Group.joins(:mentor).where(mentor: @user))
                            .select("groups.*, COUNT(group_members.id) as group_member_count")
                            .joins("left outer join group_members on \
                              (group_members.group_id = groups.id)")
                            .group("groups.id")

Now the group member count corresponding to each group could be easily obtained by @groups.each { |group| puts group.group_member_count } without unnecessarily querying the database again.

Week.next

In the upcoming week, I’ll be working on restricting the usage of circuit elements in assignments. Stay tuned for updates!

comments powered by Disqus