Elegant Summing/Grouping/Etc in Rails - ruby-on-rails

I have a number of objects which are associated together, and I'd like to layout some dashboards to show them off. For the sake of argument:
Publishing House - has many books
Book - has one author and is from one, and goes through many states
Publishing House Author - Wrote many
books
I'd like to get a dashboard that said:
How many books a publishing house put
out this month?
How many books an
author wrote this month?
What state (in progress, published) each of the books are in?
To start with, I'm thinking some very simple code:
#all_books = Books.find(:all, :joins => [:author, :publishing_house], :select => "books.*, authors.name, publishing_houses.name", :conditions => ["books.created_at > ?", #date])
Then I proceed to go through each of the sub elements I want and total them up into new arrays - like:
#ph_stats = {}
#all_books.map {|book| #ph_stats[book.publishing_house_id] = (#ph_stats[book.publishing_house_id] || 0) + 1 }
This doesn't feel very rails like - thoughts?

I think your best bet is to chain named scopes together so you can do things like:
#books = Books.published.this_month
http://api.rubyonrails.org/classes/ActiveRecord/NamedScope/ClassMethods.html#M001683
http://m.onkey.org/2010/1/22/active-record-query-interface

You should really be thinking of the SQL required to write such a query, as such, the following queries should work in all databases:
Number of books by publishing house
PublishingHouse.all(:joins => :book, :select => "books.publishing_house_id, publishing_houses.name, count(*) as total", :group => "1,2")
Number of books an author wrote this month
If you are going to move this into a scope - you WILL need to put this in a lambda
Author.all(:joins => :books, :select => "books.author_id, author.name, count(*) as total", :group => "1,2", :conditions => ["books.pub_date between ? and ?", Date.today.beginning_of_month, Date.today.end_of_month])
this is due to the use of Date.today, alternatively - you could use now()::date (postgres specific) and construct dates based on that.
Books of a particular state
Not quite sure this is right wrt your datamodel
Book.all(:joins => :state, :select => "states.name, count(*) as total", :group => "1")
All done through the magic of SQL.

Related

Simple ActiveRecord Question

I have a database model set up such that a post has many votes, a user has many votes and a post belongs to both a user and a post. I'm using will paginate and I'm trying to create a filter such that the user can sort a post by either the date or the number of votes a post has. The date option is simple and looks like this:
#posts = Post.paginate :order => "date DESC"
However, I can't quite figure how to do the ordering for the votes. If this were SQL, I would simply use GROUP BY on the votes user_id column, along with the count function and then I would join the result with the posts table.
What's the correct way to do with with ActiveRecord?
1) Use the counter cache mechanism to store the vote count in Post model.
# add a column called votes_count
class Post
has_many :votes
end
class Vote
belongs_to :post, :counter_cache => true
end
Now you can sort the Post model by vote count as follows:
Post.order(:votes_count)
2) Use group by.
Post.select("posts.*, COUNT(votes.post_id) votes_count").
join(:votes).group("votes.post_id").order(:votes_count)
If you want to include the posts without votes in the result-set then:
Post.select("posts.*, COUNT(votes.post_id) votes_count").
join("LEFT OUTER JOIN votes ON votes.post_id=posts.id").
group("votes.post_id").order(:votes_count)
I prefer approach 1 as it is efficient and the cost of vote count calculation is front loaded (i.e. during vote casting).
Just do all the normal SQL stuff as part of the query with options.
#posts = Post.paginate :order => "date DESC", :join => " inner join votes on post.id..." , :group => " votes.user_id"
http://apidock.com/rails/ActiveRecord/Base/find/class
So I don't know much about your models, but you seem to know somethings about SQL so
named scopes: you basically just put the query into a class method:
named_scope :index , :order => 'date DESC', :join => .....
but they can take parameters
named_scope :blah, {|param| #base query on param }
for you, esp if you are more familiar with SQL you can write your own query,
#posts = Post.find_by_sql( <<-SQL )
SELECT posts.*
....
SQL

Is there an idiomatic way to cut out the middle-man in a join in Rails?

We have a Customer model, which has a lot of has_many relations, e.g. to CustomerCountry and CustomerSetting. Often, we need to join these relations to each other; e.g. to find the settings of customers in a given country. The normal way of expressing this would be something like
CustomerSetting.find :all,
:joins => {:customer => :customer_country},
:conditions => ['customer_countries.code = ?', 'us']
but the equivalent SQL ends up as
SELECT ... FROM customer_settings
INNER JOIN customers ON customer_settings.customer_id = customers.id
INNER JOIN customer_countries ON customers.id = customer_countries.customer_id
when what I really want is
SELECT ... FROM customer_settings
INNER JOIN countries ON customer_settings.customer_id = customer_countries.customer_id
I can do this by explicitly setting the :joins SQL, but is there an idiomatic way to specify this join?
Besides of finding it a bit difficult wrapping my head around the notion that you have a "country" which belongs to exactly one customer:
Why don't you just add another association in your model, so that each setting has_many customer_countries. That way you can go
CustomerSetting.find(:all, :joins => :customer_countries, :conditions => ...)
If, for example, you have a 1-1 relationship between a customer and her settings, you could also select through the customers:
class Customer
has_one :customer_setting
named_scope :by_country, lambda { |country| ... }
named_scope :with_setting, :include => :custome_setting
...
end
and then
Customer.by_country('us').with_setting.each do |cust|
setting = cust.customer_setting
...
end
In general, I find it much more elegant to use named scopes, not to speak of that scopes will become the default method for finding, and the current #find API will be deprecated with futures versions of Rails.
Also, don't worry too much about the performance of your queries. Only fix the things that you actually see perform badly. (If you really have a critical query in a high-load application, you'll probably end up with #find_by_sql. But if it doesn't matter, don't optimize it.

Need help optimizing some Rails 2.3 ActiveRecord code

(Hi Dr. Nick!)
I'm trying to tighten things up for our app admin, and in a few places we have some pretty skeezy code.
For example, we have Markets, which contain Deals. In several places, we do something like this:
#markets = Market.find(:all, :select => ['name, id'])
#deals = Deal.find(:all, :select => ['subject, discount_price, start_time, end_time'], :conditions => ['start_time >= ? AND end_time <= ?', date1 date2])
Then in the corresponding view, we do something like this:
#markets.each do |m|
=m.name
end
#deals.sort!{ |a,b| a.market.name <=> b.market.name }
#deals.each do |d|
=d.subject
=d.market.name
end
This runs a stupid amount of queries: one to get the market names and ids, then another to get all the deal info, and then for each deal (of which there are thousands), we run yet another query to retrieve the market name, which we already have!
Tell me there is a way to get everything I need with just one query, since it's all related anyway, or at least to clean this up so it's not such a nightmare.
Thanks
You can write like this way ..
#deals_with_market_name = Deal.find(:all, :include => :market,
:select => ['subject, discount_price, start_time, end_time,market.name as market_name'],
:conditions => ['start_time >= ? AND end_time <= ?', date1 date2],
:order => "market.name")
And in view ...
#deals.each do |a|
=a.subject
=a.market_name
end
Try it...
If you use :include => :market when searching the deals you won't run a query to retrieve the market name for each deal. It'll be eager loaded.
#deals = Deal.find(:all, :include => :market)
Hope it helps.

Sorting by properties of a has_many association

Suppose Songs have_many Comments; How can I:
Pull a list of all songs from the database sorted by the number of comments they each have? (I.e., the song with the most comments first, the song with the least comments last?)
Same, but sorted by comment creation time? (I.e., the song with the most recently created comment first, the song with the least recently created comment last?)
1) There are a couple of ways to do this, easiest would be counter cache, you do that my creating a column to maintain the count and rails will keep the count up to speed. the column in this case would be comments_count
songs = Song.all(:order => "comments_count DESC")
OR you could do a swanky query:
songs = Song.all(:joins => "LEFT JOIN comments ON songs.id = comments.song_id",
:select => "song.name, count(*)",
:group => "song.name",
:order => "count(*) DESC")
a few caveats with the second method, anything you want to select in the songs you will need to include in the group by statement. If you only need to pull songs with comments then you can:
songs = Song.all(:joins => :comments,
:select => "song.name, count(*)",
:group => "song.name",
:order => "count(*) DESC")
Which looks nicer but because it does an inner join you would not get songs that had no comments
2) just an include/joins
songs = Song.all(:include => :comments, :order => "comment.created_at"
I hope this helps!
If you need to sort by number of comments they have - while you can do it directly using SQL - I strongly recommend you use counter_cache - See: http://railscasts.com/episodes/23-counter-cache-column
Ofter that, just set the order by option of find like so:
Song.all(:order => "comments_count DESC");
This is different in Rails 3 so it depends what you're using.
I would also recommend caching the latest comment created thing on the Song model to make your life easier.
You would do this with an after_save callback on the Comment model with something like:
self.song.update_attributes!({:last_comment_added_at => Time.now.to_s(:db)})

Select, group and sum results from database

I have a database with some fields I'd like to sum. But that's not the big problem, I want to group those fields by the month they were created. ActiveRecord automaticaly created a field named "created_at". So my question; how can I group the result by month, then sum the fields for each month?
Updated with code
#hours = Hour.all(:conditions => "user_id = "+ #user.id.to_s,
:group => "strftime('%m', created_at)",
:order => 'created_at DESC')
This is the code I have now. Managed to group by month, but doesn't manage to sum two of my fields, "mins" and "salary" which I need to sum
You can use active record calculations to do this. Some example code might be
Model.sum(:column_name, :group => 'MONTH("created_at")')
Obviously with the caveat MONTH is mysql specific, so if you were developing on an SQLite database this would not work.
I don't know if there's a SQL query you use to do it (without changing your current table structure). However, you do it with some lines of code.
records = Tasks.find(:conditions => {..})
month_groups = records.group_by{|r| r.created_at.month}
month_groups.each do |month, records|
sum stuff.. blah blah blah..
end
I saw this link on the right side of this question. I assume other databases, besides MySQL have similar functions.
mysql select sum group by date
Fixed it by using :select when getting the query, inputing selects manually
#hours = Hour.all(:conditions => "user_id = "+ #user.id.to_s,
:select => "created_at, SUM(time) time",
:group => "strftime('%m', created_at)",
:order => 'created_at DESC')

Resources