Collect many 'counts' in one query? - ruby-on-rails

I need to do the following:
<% for customer in #customers do %>
<%= customer.orders.count %>
<% end %>
This strains the server, creating n queries, where n = number of customers.
How can I load these counts along with my customers in one query? Thanks.

You could use an eager join:
#customers = Customers.paginate :page => 1, :per_page => 20, :include => [:orders]
By specifying the :include parameter to the join, the orders will be preloaded, preventing the n+1 problem. You can then use customer.orders.length.
If loading all those orders is too memory-intensive, then you should explore counter_cache. This is designed to keep a count of a model on an associated model:
class Order
belongs_to :customer, :counter_cache => true
end
This will increment and decrement a orders_count field on the owning customer record when orders are added or removed from the associaition.
If you don't want to use the counter_cache, you'll need custom finder SQL which joins the orders table and groups on orders.customer_id, and then selects the count as an extra field. This will not perform nearly as well as the counter cache, though.

Related

rails order by association through another association?

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?
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.

How to list top 10 school with active record - rails

I have two models: School and Review.
The School model looks like this: {ID, name, city, state}
The Review model looks like this: {ID, content, score, school_id}
How do I list the top ten schools based on the score from the review model?
I thought maybe a method in the school-model, with something like this:
class School < ActiveRecord::Base
def top_schools
#top_schools = School.limit(10)
...
end
end
And then loop them in a <li> list:
<div>
<ul>
<% #top_schools.each do |school| %>
<li>school.name</li>
<%end>
</ul>
</div>
But, I dont really know how to finish the top_schools method.
You should make an average of reviews of each school.
The SQL query if you are running with MySQL should be something like:
SELECT schools.* FROM schools
JOIN reviews ON reviews.school_id=schools.id
GROUP BY schools.id
ORDER BY AVG(reviews.score) DESC
LIMIT 10
Translated in Rails:
In your School model:
scope :by_score, :joins => :reviews, :group => "schools.id", :order => "AVG(reviews.score) DESC"
In your controller:
#top_schools = School.by_score.limit(10)
The choice not to include the limitation in scope, can be more flexible and allow the display of 5 or 15.
I have only tested MySQL request. I am not sure on my rails translation.
Assuming that each school only has one review ( has_one and belongs_to), you'd want to order the reviews first and then find the corresponding school:
Review.order('score DESC').first(10).each do |r|
School.find_by_id(r.school_id)
end
I would add a new field total_score to the schools table - set to 0 by default. And then add a callback in the Review model to calculate the total score for the school when a new review is added/updated to that school.
Then do this:
School.order("total_score DESC").limit(10)
Edit: I totally missed the reviews model in that answer.
School.all(:select => "schools.*, AVG(reviews.score) as avg_score",
:joins => :reviews,
:group => 'schools.id',
:order => 'avg_score desc',
:limit => 10)
But this will get slower as you add reviews. I like the total_score answer.

how to append database records in to a variable in ruby on rails

I fetched all users from the database based on city name.
Here is my code:
#othertask = User.find(:all, :conditions => { :city => params[:city]})
#othertask.each do |o|
#other_tasks = Micropost.where(:user_id => o.id).all
end
My problem is when loop gets completed, #other_task holds only last record value.
Is it possible to append all ids record in one variable?
You should be using a join for something like this, rather than looping and making N additional queries, one for each user. As you now have it, your code is first getting all users with a given city attribute value, then for each user you are again querying the DB to get a micropost (Micropost.where(:user_id => o.id)). That is extremely inefficient.
You are searching for all microposts which have a user whose city is params[:city], correct? Then there is no need to first find all users, instead query the microposts table directly:
#posts = Micropost.joins(:user).where('users.city' => params[:city])
This will find you all posts whose user has a city attribute which equals params[:city].
p.s. I would strongly recommend reading the Ruby on Rails guide on ActiveRecord associations for more details on how to use associations effectively.
you can do it by following way
#othertask = User.find(:all, :conditions => { :city => params[:city]})
#other_tasks = Array.new
#othertask.each do |o|
#other_tasks << Micropost.where(:user_id => o.id).all
end
Here is the updated code:
#othertask = User.find_all_by_city(params[:city])
#other_tasks = Array.new
#othertask.each do |o|
#other_tasks << Micropost.find_all_by_user_id(o.id)
end
You are only getting the last record because of using '=' operator, instead you need to use '<<' operator in ruby which will append the incoming records in to the array specified.
:)
Try:
User model:
has_many :microposts
Micropost model:
belongs_to :user
Query
#Microposts = Micropost.joins(:user).where('users.city' => params[:city])

First 10 items in a many-to-many relationship in RoR

I have a database with two tables: tags and items.
Each Item has a score, the highest scoring items being the most popular. There is a many-to-many relationship between tags and items.
Getting all items belonging to a tag is easy. (= tag.items) But how do I retrieve the 10 most popular items belonging to this tag?
So in fact I need the ruby equivalent of
SELECT * from items INNER JOIN item_tags ON items.id = item_tags.item WHERE item_tags.tag = :tagid ORDER BY items.score DESC LIMIT 10
Since a tag might have a lot of items, I prefer to let the database do this work instead of retrieving all items and then filtering them manually. (and if there is a faster way to perform this operation, it is certainly welcome!)
Assuming the following setup:
class Item < ActiveRecord::Base
has_and_belongs_to_many :tags
end
class Tag < ActiveRecord::Base
has_and_belongs_to_many :items
end
You should be able to do:
#tag = Tag.find(PARAMS)
#tag.items.find(:all, :order => "items.score DESC", :limit => 10)
If you want to make it even slicker, add this line to your Item class:
named_scope :popular, :order => "items.score DESC", :limit => 10
You can then call
#tag.items.popular

Creating "feeds" from multiple, different Rails models

I'm working on an application that has a few different models (tickets, posts, reports, etc..). The data is different in each model and I want to create a "feed" from all those models that displays the 10 most recent entries across the board (a mix of all the data).
What is the best way to go about this? Should I create a new Feed model and write to that table when a user is assigned a ticket or a new report is posted? We've also been looking at STI to build a table of model references or just creating a class method that aggregates the data. Not sure which method is the most efficient...
You can do it one of two ways depending on efficiency requirements.
The less efficient method is to retrieve 10 * N items and sort and reduce as required:
# Fetch 10 most recent items from each type of object, sort by
# created_at, then pick top 10 of those.
#items = [ Ticket, Post, Report ].inject([ ]) do |a, with_class|
a + with_class.find(:all, :limit => 10, :order => 'created_at DESC')
end.sort_by(&:created_at).reverse[0, 10]
Another method is to create an index table that's got a polymorphic association with the various records. If you're only concerned with showing 10 at a time you can aggressively prune this using some kind of rake task to limit it to 10 per user, or whatever scope is required.
Create an Item model that includes the attributes "table_name" and "item_id". Then create a partial for each data type. After you save, let's say, a ticket, create an Item instance:
i = Item.create(:table_name => 'tickets', :item_id => #ticket.id)
In your items_controller:
def index
#items = Item.find(:all, :order => 'created_on DESC')
end
In views/items/index.erb:
<% #items.each do |item| %>
<%= render :partial => item.table_name, :locals => {:item => item} %><br />
<% end %>

Resources