Rails 3 ActiveRecord: Order by count on association

MysqlRuby on-RailsRuby on-Rails-3Activerecord

Mysql Problem Overview


I have a model named Song. I also have a model named Listen. A Listen belongs_to :song, and a song :has_many listens (can be listen to many times).

In my model I want to define a method self.top which should return the top 5 songs listened to the most. How can I achieve that using the has_many relation?

I'm using Rails 3.1.

Thanks!

Mysql Solutions


Solution 1 - Mysql

Using named scopes:

class Song
  has_many :listens
  scope :top5,
    select("songs.id, OTHER_ATTRS_YOU_NEED, count(listens.id) AS listens_count").
    joins(:listens).
    group("songs.id").
    order("listens_count DESC").
    limit(5)

Song.top5 # top 5 most listened songs

Solution 2 - Mysql

Even better, use counter_cache which will be faster because you'll only because using one table in your query

Here is your song class:

class Song < ActiveRecord::Base
  has_many :listens

  def self.top
    order('listens_count DESC').limit(5)
  end
end

Then, your listen class:

class Listen < ActiveRecord::Base
  belongs_to :song, counter_cache: true
end

Make sure you add a migration:

add_column :comments, :likes_count, :integer, default: 0

Bonus points, add test:

describe '.top' do
  it 'shows most listened songs first' do
    song_one = create(:song)
    song_three = create(:song, listens_count: 3)
    song_two = create(:song, listens_count: 2)

    popular_songs = Song.top

    expect(popular_songs).to eq [song_three, song_two, song_one]
  end
end

Or, if you want to go with the above method, here it is a bit more simply, and using a class method rather than scope

def self.top
    select('comments.*, COUNT(listens.id) AS listens_count').
      joins(:listens).                                                   
      group('comments.id').
      order('listens_count DESC').
      limit(5)
end

Solution 3 - Mysql

For rails 4.x try this if your rows without any association matters:

scope :order_by_my_association, lambda {
    select('comments.*, COUNT(listens.id) AS listens_total')
    .joins("LEFT OUTER JOIN listens ON listens.comment_id = comments.id")
    .group('comments.id')
    .order("listens_total DESC")
  }

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionChristoffer ReijerView Question on Stackoverflow
Solution 1 - MysqlclyfeView Answer on Stackoverflow
Solution 2 - MysqlNealView Answer on Stackoverflow
Solution 3 - MysqlBruno CasaliView Answer on Stackoverflow