Advanced activerecord query where association with a limit - ruby-on-rails

I'm trying to do a more advanced query with activerecord and can't figure out the best way to write it. Hopefully this makes sense...
My POSTGRES/rails app has models:
Company has_many :employees
Employee has_many :evaluations
Evaluations has decimal column :score. I want to get the 10 lowest evaluation scores for a company by name. BUT I also only want to pull a SINGLE lowest score from each employee. I don't want to have 5 of the 10 scores in my results list all coming from the same employee ID.
I have:
#list = Company.where(:name => 'Google').joins(:evaluations)
But I am struggling with how to limit the evaluations to a single low value on a single employee.id
What is the best way to do this?

Here's the 'off-the-top-of-my-head' guess:
Employee.
select('employees.*, min(evaluations.score) as lowest_score').
joins(:company, :evaluations).
where(:company => {:name => 'Google'}).
group('employees.id').
order('lowest_score').
limit(10)

#list = Evaluation.joins(:employee => :company)
.where(:company => { :name => "Google" })
.order(:score)
.group(:employee => :id)
.first(10)
IIRC, ordering by score should mean you don't have to bother with min functions and stuff as it will pick the first record when doing the grouping, which will be the lowest score. Give it a whirl.

Related

count and retrieve objects of polymorphic association

So i've got three models
"Text" has_many :statistics, :as => :loggable
"Document" has_many :statistics, :as => :loggable
"Statistic" belongs_to :loggable, :polymorphic => true
"Company" has_many "Documents"/"Texts"
now i want to get different restults, f.e.
get the total count of statistic objects of all text/document objects of a company
get the total count of statistic objects of all text/document objects of a company
this month
get the first five Texts/Documents of a company with the most statistic
objects
get the first five Texts/Documents of a company with the most statistic
objects this month
this month is related to the creation date of the statistic object.
i really have no idea how to achieve this. i tried different things in the rails console but without luck.
company.texts
company.documents
any ideas how to do this?
thanks in advance. please leave a comment if something is unclear.
The first 2 are pretty simple.
get the total count of statistic objects of all text/document objects
Statistic.where(:loggable_type => "Text").where(:loggable_id => company.texts)
get the total count of statistic objects of all text/document objects this month
Statistic.where('created_at between ? and ?', 1.month.ago, Time.now).where(:loggable_type => "Text").where(:loggable_id => d.texts).count
get the first five Texts/Documents of a company with the most statistic objects
ids = Statistic.select("COUNT(*) AS count_all").where(:loggable_type => "Text").where(:loggable_id => company.texts).group(:loggable_id).order("count_all desc").limit(5).size
Text.where(:id => ids)
get the first five Texts/Documents of a company with the most statistic objects this month
ids = Statistic.select("COUNT(*) AS count_all").where('created_at between ? and ?', 1.month.ago, Time.now).where(:loggable_type => "Text").where(:loggable_id => d.texts).group(:loggable_id).order("count_all desc").limit(5).size
Text.where(:id => ids)
Try it with
company.texts.join(:statistics).where('statistics.created_at between ? and ?', Time.now, 1.month.ago)
and
company.documents.join(:statistics).where('statistics.created_at between ? and ?', Time.now, 1.month.ago)

(rails) Order by Number of Likes on a Website

I have a model called websites that has_many :likes, and of course another model called likes with belongs_to :website. I want to get an array of all the websites but order them by the number of likes a website has.
helpful info:
I have it set up so #website.likes will return the likes from #website. (#website.likes.count is the number of likes a website has)
I want to get an array of all the websites but order them by the number of likes a website has.
As others have posted, you can do a join onto likes and then order by the count. Performance may be a bit iffy depending on indexing etc. You'll have slightly different syntax depending on if you're running Rails 2 or 3.
An alternative would be to maintain a denormalised likes_count column on websites which is updated when a Like model object is saved.
Then you just need to query on Website and specify an order likes_count descending (and is easily indexed).
To do this, create a likes_count integer column on the websites and specify the :counter_cache option on the belongs_to declaration in the Likes model. e.g:
class Likes
belongs_to :website, :counter_cache => true
end
Check out http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html for more info
This query should give you what you need:
all(:select => 'websites.*, count(*) as count',
:joins => :likes,
:group => :websites,
:order => 'count DESC')
Something along the lines of:
SELECT Websites.*, COUNT(Like.ID) AS Liked
FROM websites
LEFT OUTER JOIN Like ON websites.ID = Like.website_id
GROUP BY Like.website_id ORDER BY Liked DESC

Sum associated tables using Active Record

How does one sum all of the "total" columns in an association?
My SQL-fu sucks, so I would like to learn how to do so using Active Record for my rails 2.3.5 app (so no fancy syntax just yet please ;-) And I'm on MySQL.
Let's say I have:
Shop
has_many :customers
has_many :transactions, :through => :customers
So normal stuff.
shop = Shop.first
shop.transactions
=> 100
Ok, all that background for the question:
I want to SUM the total column in the transactions from the past year (Jan 1st 2010..Dec 31 2010) and display them by customer.
Whilst I know how to group transactions and find with conditions, it's the sum part where my lack of SQL lets me down.
first = Date.new(2010, 01, 01)
last = Date.new(2010, 12, 31)
shop.transactions(:conditions => {:created_at => first..last}, :group => :customer_id, :include => sum(:total))
I just took a stab, am i on the right track?
shop.transactions.sum(:total, :conditions => {:created_at => first..last}, :group => :customer_id)
This looks like an easier way. I now know that sum can take AR attributes too. cool.
Look into collect methods.
You can do things like:
transactions = Shop.transactions
total = 0
sum = transactions.collect{|i| total+=i.transaction.amount}
Replace amount with your property that contains the amount of the transaction.
You could also use .sum
sum = transactions.to_a.sum(&:amount)

Ruby on Rails: using nested named_scopes

I just got referred to stackoverflow by a friend here to help with a problem I am having. I am fairly new to ruby on rails and I am working on a collaborative project where we have a script (medal_worker.rb) that is scheduled to run at a fixed intervals to award people various medals based on various participation and success on our website. One of the new medals I am working on rewards people for "milestones". For the purpose of this problem, let's say we want to give them medals when they make 100, 1000, and 10000 comments. I would like to do this by using named_scopes from the User model (user.rb) to give me filtered lists of the users I am looking for.
My question is: How do I find the users who do not have the respective medals for the respective milestone comment level (preferably using the named_scopes from the User model)?
Here is an exerpt from my model_worker.rb file:
def award_comment_milestone(comments)
users = Users.frequent_comment_club_members(comments).not_awarded_medal(Medal.find_by_id(medal.id))
for user in users do
award_medal(medal, nil, user) if #award
end
end
Here is where I am at with the named_scopes in the user model (user.rb):
named_scope :frequent_comment_club_members, lambda { |*args|
{:include => comment_records, :conditions => ['comment_records.comment_type = ? and comment_records.comments >= ?', 'User', (args.first || 0)]}
}
named_scope :not_awarded_medal, lambda { |medal|
{:include => :awards, :conditions => ['awards.medal_id not in (select awards.medal_id from awards where awards.medal_id = ?)", medal.id] }
}
This is not working as I would like, but I don't know if the problem is in the named_scopes or how I am passing arguements or what. Thanks.
Your named_scopes look fine. Except you are starting with a single apostrophe and ending with a double apostrophe in the not_awarded_medal condition statement.
EDIT:
Take it back. Your not_awarded_medal named_scope is off.
Try something like this:
named_scope :not_awarded_medal, lambda { |medal_id|
{ :include => :awards,
:conditions => [
"? not in (select awards.id from awards where awards.user_id = users.id)", medal_id
]
}
}
untested
Now this is assuming that you have the following relationships:
User: has_many :awards
Award: belongs_to :user
If you are using has_many :through then you are going to have to change the SQL to look at the join (users_awards) table.
--
But I do see a couple of things in the award_comment_milestone function.
What is the parameter coming into award_comment_milestone? Is it an array of comments or is it a count of comments? Also where is medal defined?
If comments is an array then you need to pass comments.length into frequent_comment_club_members. If it's the count then I would rename it to comments_count so the next person can understand the logic more quickly.
Some general observations:
not_awarded_medal should just take a medal_id and not the whole object (no need to do multiple queries)
Why are you doing Medal.find_by_id(medal.id)? You already have the medal object.

Ruby on Rails activerecord find average in one sql and Will_Paginate

I have the following model association: a student model and has_many scores.
I need to make a list showing their names and average, min, max scores. So far I am using
student.scores.average(:score) on each student, and I realise that it is doing one sql per student. How can I make the list with one joined sql?
Also how would I use that with Will_Paginate plugin?
Thank you
You want the :group and :select options to Student.find. This should work for you:
students = Student.all(
:select => "
students.*,
AVG(scores.score) as avg_score,
MIN(scores.score) as min_score,
MAX(scores.score) as max_score",
:joins => :scores
:group => 'students.id')
The calculated columns are available just like the real columns though they obviously won't be saved
students.first.avg_score
students.first.min_score
students.first.max_score
For using WillPaginate, just include your :page, :per_page, ... options and call Student.paginate instead of find. If it turns out that the pagination is getting the wrong number of pages because of the :group option, just add this: :total_entries => Student.count to your arguments

Resources