Rails order by count based on values of column

Viewed 1164

Rails 5.1.2

I have a two models Student and Award this way:

class Student < ApplicationRecord
  has_many :awards
end

class Award < Application Record
  belongs_to :students

  # Categories
  scope :attendance, -> { where(category: 0) }
  #...(other award categories)
  scope :talent,     -> { where(category: 8) }
  # Ranks
  # Gold
  scope :rank_1, -> { where(rank: 1) }
  # Silver
  scope :rank_2, -> { where(rank: 2) }
  # Bronze
  scope :rank_3, -> { where(rank: 3) }
end

Award has these columns: rank and category.

Now, I want to get the top student for a given category. The criteria for this is, order by count of "gold" awards (rank 1), then order by count of "silver" (rank 2) awards, and then order by count of "bronze" (rank 3) awards.

So, if I were to get the Student who meets the top criteria for category 0 (which is handled by the attendance scope as described in the model above), this is what I thought the query should look like:

Student.joins(:awards).where(awards: { category: 0 }).group('students.id').order('COUNT(awards.rank == 1) DESC', 'COUNT(awards.rank == 2) DESC', 'COUNT(awards.rank == 3) DESC').take

However, this returns the Student with the highest count of awards, regardless of rank. So for example, if I remove take, the order looks like this:

# |St.ID | Gold  | Slvr. | Brnz. |
----------------------------------
1 |  12  |   4   |   12  |   8   |
----------------------------------
2 |   1  |   9   |   0   |   4   |
----------------------------------
3 |   6  |   9   |   1   |   0   |
----------------------------------
4 |  18  |   5   |   2   |   2   |
----------------------------------
 ...

So, the order I'm getting are IDs 12, 1, 6, 18, ..., when it should be IDs 6, 1, 18, 12, ....

I realize the order('COUNT(awards.rank == 1) DESC', 'COUNT(awards.rank == 2) DESC', 'COUNT(awards.rank == 3) DESC') part is simply ordering by the count of awards total (rather than count of a awards with a particular value in column rank).

I can easily solve this by adding a counter cache for each category of awards, but that isn't an elegant nor flexible solution.

As a bonus, after this query returns a successful result, I will search the database again to find all students who have the same score (as there could be ties). I'm not aware of a way to do all this in one query (perhaps by means of subqueries after getting the values for each rank).

1 Answers
Related