Reputation: 3451
As a simple example, let's say a bookstore has books which have one author. The books has many sales through orders. Authors can have many books.
I am looking for a way to list the authors ordered by sales. Since the sales are associated with books, not authors, how can I accomplish this?
I would guess something like:
Author.order("sales.count").joins(:orders => :sales)
but that returns a column can't be found error.
I have been able to connect them by defining it in the Author model. The following displays the correct count for sales, but it does ping the database for each and every author... bad. I'd much rather eager load them, but I can't seem to get it to work properly since it will not list any authors who happen to have 0 sales if I remove the self.id and assign the join to @authors.
class Author < ActiveRecord::Base
def sales_count
Author.where(id: self.id).joins(:orders => :sales).count
end
end
And more specifically, how can I order them by the count result so I can list the most popular authors first?
Upvotes: 0
Views: 365
Reputation: 35531
Firstly, let's have all associations available on the Author class itself to keep the query code simple.
class Author < AR::Base
has_many :books
has_many :orders, :through => :books
has_many :sales, :through => :orders
end
The simplest approach would be for you to use group
with count
, which gets you a hash in the form {author-id: count}
:
author_counts = Author.joins(:sales).group("authors.id").count
=> {1 => 3, 2 => 5, ... }
You can now sort your authors and lookup the count using the author_counts
hash (authors with no sales will return nil
):
<% Author.all.sort_by{|a| author_counts[a.id] || 0}.reverse.each do |author| %>
<%= author.name %>: <%= author_counts[author.id] || 0 %>
<% end %>
UPDATE
An alternative approach would be to use the ar_outer_joins gem that allows you get around the limitations of using includes
to generate a LEFT JOIN:
authors = Author.outer_joins(:sales).
group(Author.column_names.map{|c| "authors.#{c}").
select("authors.*, COUNT(sales.id) as sales_count").
order("COUNT(sales.id) DESC")
Now your view can just look like this:
<% authors.each do |author| %>
<%= author.name %>: <%= author.sales_count %>
<% end %>
This example demonstrates how useful a LEFT JOIN can be where you can't (or specifically don't want to) eager load the other associations. I have no idea why outer_joins
isn't included in ActiveRecord by default.
Upvotes: 2