Smart sum in Rails based on Eager Loading? - ruby-on-rails

Rails has a sum function that goes directly to SQL like this:
User.find(1).books.sum(:pages_count)
SELECT SUM("books"."pages_count") AS sum_id FROM "books" WHERE "books"."user_id" = 1
=> 3024
But if you are in a controller you might want to do eager loading like this :
users = User.limit(10).includes(:books)
users.first.books.map(&:pages_count).sum
=> 4325
This way you only do two SQL queries and the rest is ruby code.
I'm trying to do a method that can detect if the relation has been eager loaded and select the right sum function but with no such luck.
Here is my code:
module ActiveRecord
module Calculations
def smart_sum(column = :value)
puts self.loaded?
if loaded?
collect {|x| x[column] }.sum
else
sum(column)
end
end
end
end
Strangely the loaded? bit does not work in the method and so the SQL sum is always returned.
Any help?

Related

Rails - How can I avoid using the database while performing a detect method?

When performing detect on a int array, it works:
#number = [1,2,3,4,5,6,7].detect{|n| n == 4}
Variable #number becomes 4.
But when I do something like this:
#categories = Category.all
#current_category = #categories.detect{|cat| cat.id == params[:category]}
The program outputs
Category Load (0.2ms) SELECT "categories".* FROM "categories"
Which means it's using the database to find it.
However, the element I'm trying to find is already in the collection #categories, I just want to find it to assign it to a variable.
Of course another solution would be to implement a linear search algorithm, but I just want to keep the code as clean as possible.
How can I avoid using the database for this search?
EDIT: I just realized that this could be lazy fetching. Because before detect, I never use #categories, so it does the query when I do detect. Could this be true?
Rails is actually performing a SELECT COUNT(*) query when you call #categories.all, essentially performing a lazy-fetch.
Your #categories object still needs to query the database for the data.
See the documentation here: http://apidock.com/rails/ActiveRecord/Scoping/Named/ClassMethods/all
posts = Post.all
posts.size # Fires "select count(*) from posts" and returns the count
posts.each {|p| puts p.name } # Fires "select * from posts" and loads post objects
fruits = Fruit.all
fruits = fruits.where(color: 'red') if options[:red_only]
fruits = fruits.limit(10) if limited?
In your case, you should use active record and SQL requesting.
#current_category = #categories.find_by(id: params[:category])
Using array methods on Active Record relations tend to fetch all the data then apply the algorithm in-memory, while SQL filtering is faster.
In you case I love to define the operator [] on my model:
#in category.rb
def self.[](x)
self.find_by(id: x)
end
# anywhere after:
if c = Category[params[:id]]
puts "Category found and it's #{c.name} !"
else
puts "Not found :("
end

Sorting users by score - rails

In my rails app, each user has a karma/score that i'm calculating through the user model as follows:
after_invitation_accepted :increment_score_of_inviter
def increment_score_of_inviter
invitation_by.increment!(:score, 10)
end
def comment_vote_count
Vote.find(comment_ids).count * 2
end
def calculated_vote_count
base_score + comment_vote_count
end
def recalculate_score!
update_attribute(:score, calculated_vote_count)
end
I'm trying to create paginated list of all the users, sorted by their scores. With thousands of users, how do I do this efficiently?
I was think of using:
User.all.sort_by(&:calculated_vote_count)
But, this would be pretty heavy.
Well...using User.all upon a table full of records will be a memory hog for your application. Instead you should try to accomplish what you want on the DB layer.
At this point I'm assuming base_score is one of the table columns (is base_score same as score?), so you'd have to do something like the following (using LEFT JOIN):
User.select("users.*, (COUNT(votes.id) * 2 + users.base_score) AS calculated_vote_count").joins("LEFT JOIN votes ON votes.user_id = user.id").order("calculated_vote_count DESC")
And then you can paginate the results the way you like.
I didn't test it, but it should work. Let me know if doesn't.
It's pretty straight forward:
User.order('score DESC').all
Obviously you'd have pagination, User.order('score DESC').page(params[:page]).per(20) with Kaminari.

Help converting Rails 2 Database logic to Rails 3.1/ PostgreSQL

How do I select a single random record for each user, but order the Array by the latest record pr. user.
If Foo uploads a new painting, I would like to select a single random record from foo. This way a user that uploads 10 paintings won't monopolize all the space on the front page, but still get a slot on the top of the page.
This is how I did it with Rails 2.x running on MySQL.
#paintings = Painting.all.reverse
first_paintings = []
#paintings.group_by(&:user_id).each do |user_id, paintings|
first_paintings << paintings[rand(paintings.size-1)]
end
#paintings = (first_paintings + (Painting.all - first_paintings).reverse).paginate(:per_page => 9, :page => params[:page])
The example above generates a lot of SQL query's and is properly badly optimized. How would you pull this off with Rails 3.1 running on PostgreSQL? I have 7000 records..
#paintings = Painting.all.reverse = #paintings = Painting.order("id desc")
If you really want to reverse the order of the the paintings result set I would set up a scope then just use that
Something like
class Painting < ActiveRecord::Base
scope :reversed, order("id desc")
end
Then you can use Painting.reversed anywhere you need it
You have definitely set up a belongs_to association in your Painting model, so I would do:
# painting.rb
default_scope order('id DESC')
# paintings_controller.rb
first_paintings = User.includes(:paintings).collect do |user|
user.paintings.sample
end
#paintings = (first_paintings + Painting.where('id NOT IN (?)', first_paintings)).paginate(:per_page => 9, :page => params[:page])
I think this solution results in the fewest SQL queries, and is very readable. Not tested, but I hope you got the idea.
You could use the dynamic finders:
Painting.order("id desc").find_by_user_id!(user.id)
This is assuming your Paintings table contains a user_id column or some other way to associate users to paintings which it appears you have covered since you're calling user_id in your initial code. This isn't random but using find_all_by_user_id would allow you to call .reverse on the array if you still wanted and find a random painting.

rails - activerecord ... grab first result

I want to grab the most recent entry from a table. If I was just using sql, you could do
Select top 1 * from table ORDER BY EntryDate DESC
I'd like to know if there is a good active record way of doing this.
I could do something like:
table.find(:order => 'EntryDate DESC').first
But it seems like that would grab the entire result set, and then use ruby to select the first result. I'd like ActiveRecord to create sql that only brings across one result.
You need something like:
Model.first(:order => 'EntryDate DESC')
which is shorthand for
Model.find(:first, :order => 'EntryDate DESC')
Take a look at the documentation for first and find for details.
The Rails documentation seems to be pretty subjective in this instance. Note that .first is the same as find(:first, blah...)
From:http://api.rubyonrails.org/classes/ActiveRecord/Base.html#M002263
"Find first - This will return the first record matched by the options used. These options can either be specific conditions or merely an order. If no record can be matched, nil is returned. Use Model.find(:first, *args) or its shortcut Model.first(*args)."
Digging into the ActiveRecord code, at line 1533 of base.rb (as of 9/5/2009), we find:
def find_initial(options)
options.update(:limit => 1)
find_every(options).first
end
This calls find_every which has the following definition:
def find_every(options)
include_associations = merge_includes(scope(:find, :include), options[:include])
if include_associations.any? && references_eager_loaded_tables?(options)
records = find_with_associations(options)
else
records = find_by_sql(construct_finder_sql(options))
if include_associations.any?
preload_associations(records, include_associations)
end
end
records.each { |record| record.readonly! } if options[:readonly]
records
end
Since it's doing a records.each, I'm not sure if the :limit is just limiting how many records it's returning after the query is run, but it sure looks that way (without digging any further on my own). Seems you should probably just use raw SQL if you're worried about the performance hit on this.
Could just use find_by_sql http://api.rubyonrails.org/classes/ActiveRecord/Base.html#M002267
table.find_by_sql "Select top 1 * from table ORDER BY EntryDate DESC"

Using will_paginate with multiple models (Rails)

Pretty sure that I'm missing something really simple here:
I'm trying to display a series of pages that contain instances of two different models - Profiles and Groups. I need them ordering by their name attribute. I could select all of the instances for each model, then sort and paginate them, but this feels sloppy and inefficient.
I'm using mislav-will_paginate, and was wondering if there is any better way of achieving this? Something like:
[Profile, Group].paginate(...)
would be ideal!
Good question, I ran into the same problem a couple of times. Each time, I ended it up by writing my own sql query based on sql unions (it works fine with sqlite and mysql). Then, you may use will paginate by passing the results (http://www.pathf.com/blogs/2008/06/how-to-use-will_paginate-with-non-activerecord-collectionarray/). Do not forget to perform the query to count all the rows.
Some lines of code (not tested)
my_query = "(select posts.title from posts) UNIONS (select profiles.name from profiles)"
total_entries = ActiveRecord::Base.connection.execute("select count(*) as count from (#{my_query})").first['count'].to_i
results = ActiveRecord::Base.connection.select_rows("select * from (#{my_query}) limit #{limit} offset #{offset}")
Is it overkilled ? Maybe but you've got the minimal number of queries and results are consistent.
Hope it helps.
Note: If you get the offset value from a http param, you should use sanitize_sql_for_conditions (ie: sql injection ....)
You can get close doing something like:
#profiles, #groups = [Profile, Group].map do |clazz|
clazz.paginate(:page => params[clazz.to_s.downcase + "_page"], :order => 'name')
end
That will then paginate using page parameters profile_page and group_page. You can get the will_paginate call in the view to use the correct page using:
<%= will_paginate #profiles, :page_param => 'profile_page' %>
....
<%= will_paginate #groups, :page_param => 'group_page' %>
Still, I'm not sure there's a huge benefit over setting up #groups and #profiles individually.
in my last project i stuck into a problem, i had to paginate multiple models with single pagination in my search functionality.
it should work in a way that the first model should appear first when the results of the first model a second model should continue the results and the third and so on as one single search feed, just like facebook feeds.
this is the function i created to do this functionality
def multi_paginate(models, page, per_page)
WillPaginate::Collection.create(page, per_page) do |pager|
# set total entries
pager.total_entries = 0
counts = [0]
offsets = []
for model in models
pager.total_entries += model.count
counts << model.count
offset = pager.offset-(offsets[-1] || 0)
offset = offset>model.count ? model.count : offset
offsets << (offset<0 ? 0 : offset)
end
result = []
for i in 0...models.count
result += models[i].limit(pager.per_page-result.length).offset(offsets[i]).to_a
end
pager.replace(result)
end
end
try it and let me know if you have any problem with it, i also posted it as an issue to will_paginate repository, if everyone confirmed that it works correctly i'll fork and commit it to the library.
https://github.com/mislav/will_paginate/issues/351
Have you tried displaying two different sets of results with their own paginators and update them via AJAX? It is not exactly what you want, but the result is similar.

Resources