Avoid scope hitting database if association already loaded - ruby-on-rails

I have 2 models like so:
class Country < ActiveRecord::Base
has_many :cities
end
class City < ActiveRecord::Base
belongs_to :country
scope :big, where("population > 1000000")
end
Then, in the code I load a country with it's cities, like so:
country = Country.include(:cities).find(id)
But when I execute:
country.cities.big
It makes a hit to the db with this query:
SELECT * FROM cities where country_id = 1 AND population > 1000000
Which works fine, but it's not necessary since the cities where all already loaded by the :include.
Is there a way to tell the scope to not hit the db if the association is already loaded?
I can do it with an association extension, but not for a regular scope. On extensions I do something like:
has_many :cities do
def big
if loaded?
detect {|city| city.population > 1000000}
else
where("population > 1000000")
end
end
end
But this would be repeating the scope in 2 places and I want to reuse the scope on the city model.

The scope logic uses methods that work with Arel under the hood, and ruby Enumerables don't know how to use them. You may be able to refactor your logic to an abstraction that can be translated to use either the Arel or Enumerable methods, but this won't always be possible:
def self.build_scope(abstracted)
where(abstracted.map(&:to_s).join(' '))
end
def self.build_enum(abstracted)
select{|city| city.send(abstracted[0]).send(*abstracted[1..2]) }
end
def self.abstract_big
[:population, ">", 10000]
end
scope :big_scope, build_scope(abstract_big)
def self.big_enum
build_enum abstract_big
end
You could then do:
country.cities.big_enum
A much better idea would be to only eagerly load according to the scope that you want (if you know it in advance):
country = Country.include(:cities).merge(City.big).find(id)

Related

Print attribute of object

I have 2 classes with:
class User < ApplicationRecord
has_one :address
end
class Address < ApplicationRecord
belongs_to :user
I need to write code, which will print the value of attribute with the name 'postcode' of 100 users from database.
I have some code on this point, but not sure that it's a good way to solve the problem:
#users = User.all
#users.limit(100).each do |user|
puts "#{user.postcode}"
end
Who has better ideas?
I'd use pluck
puts User.limit(100).pluck('postcode')
# or
puts User.joins(:address).limit(100).pluck('addresses.postcode')
Pluck is best suited for your scenario.
User.where(condition).pluck(:postcode)
(#where condition is optional)
Event if you want to fetch other column with postcode you can simply include that in pluck. for e.g.
User.where(condition).pluck(:id, :postcode)
(#using multiple column inside pluck will only work with rails4 and above)

Is overriding an ActiveRecord relation's count() method okay?

Let's say I have the following relationship in my Rails app:
class Parent < ActiveRecord::Base
has_many :kids
end
class Kid < ActiveRecord::Base
belongs_to :parent
end
I want parents to be able to see a list of their chatty kids, and use the count in paginating through that list. Here's a way to do that (I know it's a little odd, bear with me):
class Parent < ActiveRecord::Base
has_many :kids do
def for_chatting
proxy_association.owner.kids.where(:chatty => true)
end
end
end
But! Some parents have millions of kids, and p.kids.for_chatting.count takes too long to run, even with good database indexes. I'm pretty sure this cannot be directly fixed. But! I can set up a Parent#chatty_kids_count attribute and keep it correctly updated with database triggers. Then, I can:
class Parent < ActiveRecord::Base
has_many :kids do
def for_chatting
parent = proxy_association.owner
kid_assoc = parent.kids.where(:chatty => true)
def kid_assoc.count
parent.chatty_kids_count
end
end
end
end
And then parent.kids.for_chatting.count uses the cached count and my pagination is fast.
But! Overriding count() on singleton association objects makes the uh-oh, I am being way too clever part of my brain light up big-time.
I feel like there's a clearer way to approach this. Is there? Or is this a "yeah it's weird, leave a comment about why you're doing this and it'll be fine" kind of situation?
Edit:
I checked the code of will_paginate, seems like it is not using count method of AR relation, but i found that you can provide option total_entries for paginate
#kids = #parent.kids.for_chatting.paginate(
page: params[:page],
total_entries: parent.chatty_kids_count
)
This is not working
You can use wrapper for collection like here
https://github.com/kaminari/kaminari/pull/818#issuecomment-252788488​,
just override count method.
class RelationWrapper < SimpleDelegator
def initialize(relation, total_count)
super(relation)
#total_count = total_count
end
def count
#total_count
end
end
# in a controller:
relation = RelationWrapper.new(#parent.kids.for_chatting, parent.chatty_kids_count)

Rails 4.2: Eager-loading has_many relation with STI

Let's say I have a relation in Rails to a table that uses STI like:
class Vehicle < ActiveRecord::Base; end
class Car < Vehicle; end
class Truck < Vehicle; end
class Person < ActiveRecord::Base
has_many :cars
has_many :trucks
has_many :vehicles
end
... and I want to load a Person and all of its cars and trucks in one query. This doesn't work:
# Generates three queries
p = Person.includes([:cars, trucks]).first
... and this is close, but no luck here:
# Preloads vehicles in one query
p = Person.includes(:vehicles).first
# and this has the correct class (Car or Truck)
p.vehicles.first
# but this still runs another query
p.cars
I could do something like this in person.rb:
def cars
vehicles.find_all { |v| v.is_a? Car }
end
but then Person#cars isn't a collection proxy anymore, and I like collection proxies.
Is there an elegant solution to this?
EDIT: Adding this to Person gives me the items I want in arrays with one query; it's really pretty close to what I want:
def vehicle_hash
#vehicle_hash ||= vehicles.group_by {|v|
v.type.tableize
}
end
%w(cars trucks).each do |assoc|
define_method "#{assoc}_from_hash".to_sym do
vehicle_hash[assoc] || []
end
end
and now I can do Person.first.cars_from_hash (or find a better name for my non-synthetic use case).
When you use includes, it stores those loaded records in the association_cache, which you can look at in the console. When you do p = Person.includes(:vehicles), it stores those records as an association under the key :vehicles. It uses whatever key you pass it in the includes.
So then when you call p.cars, it notices that it doesn't have a :cars key in the association_cache and has to go look them up. It doesn't realize that Cars are mixed into the :vehicles key.
To be able to access cached cars as either through p.vehicles OR p.cars would require caching them under both of those keys.
And what it stores is not just a simple array—it's a Relation. So you can't just manually store records in the Hash.
Of the solutions you proposed, I think including each key is probably the simplest—code-wise. Person.includes(:cars, :trucks) 3 SQL statements aren't so bad if you're only doing it once per request.
If performance is an issue, I think the simplest solution would be a lot like what you suggested. I would probably write a new method find_all_cars instead of overwriting the relation method.
Although, I would probably overwrite vehicles and allow it to take a type argument:
def vehicles(sti_type=nil)
return super unless sti_type
super.find_all { |v| v.type == sti_type }
end
EDIT
You can get vehicles cached by Rails, so you probably can just rely on that. Your define_methods could also do:
%w(cars trucks).each do |assoc|
define_method "preloaded_#{assoc}" do
klass = self.class.reflect_on_all_associations.detect { |assn| assn.name.to_s == assoc }.klass
vehicles.select { |a| a.is_a? klass }
end
end
Even if you don't use includes, the first time you call it, it will cache the association—because you're selecting, not whereing. You still won't get a Relation back, of course.
It's not really that pretty, but I like that it's contained to one method that doesn't depend on any other ones.

Rails 3 : Sort a ActiveRelation by Ratings Counted from another table

My rating system is quite simple, either the user like or he doesn't like the article.
Basically, I have something like this and it works perfectly :
class Article
has_many :ratings
def self.by_ratings
all.sort_by{|article| (article.ratings.where(like: true).size).to_f / (article.ratings.size) }
end
end
As you can guess, our app becomes huge in database, traffic increase and this part of the code becomes a bottleneck.
I am trying to rewrite it in pure sql for performance increase. I am also trying to make it as an ActiveRelation to chain up with other conditions. Trying also to write the sql but no success. Any ideas ?
Thanks !
Using :
Rails 3.0.10
ActiveRecord
Sqlite 3 (We can switch to Postgresql)
What you need is a cache column.
1) Create a migration in order to ass the cache column:
class AddRatingCacheToArticles < ActiveRecord::Migration
def self.up
add_column :articles, :rating_cache, :decimal, :default => 0.0
end
def self.down
remove_column :articles, :rating_cache
end
end
2) Define an update method in Article which will do the count:
class Article < ActiveRecord::Base
has_many :ratings
def update_rating_cache
current_rating = ratings.where(:like => true).count.to_f/ratings.count.to_f
update_attribute(:rating_cache, current_rating)
end
end
3) Setup a callback in Rating to trigger the update_rating_cache method when they are saved:
class Rating < ActiveRecord::Base
belongs_to :article
after_save :update_article_rating_cache
after_destroy :update_article_rating_cache
def update_article_rating_cache
article.update_rating_cache if article
end
end
4) It is now super easy to sort your articles by rating:
class Article < ActiveRecord::Base
has_many :ratings
def self.by_ratings
order('rating_cache DESC')
end
def update_rating_cache
current_rating = ratings.where(:like => true).count.to_f/ratings.count.to_f
update_attribute(:rating_cache, current_rating)
end
end
And that can be used as an ActiveRelation!
Good luck :)
I haven't been able to test this SQL in your model yet, but can you try:
select articles_ratings.*, likes/total rating from (select articles.*, SUM(if(articles.like, 1, 0)) likes, count(*) total from articles JOIN ratings on article_id = articles.id GROUP BY articles.id) articles_ratings ORDER BY rating desc
That should hopefully give you a list of articles, sorted by their rating (highest to lowest). I can try and follow up with some rails if that works.
EDIT As suggesteed by #socjopa, if you're not trying to get this to production immediately, my next recommendation would be to move this query to a view. Treat it like any other ActiveRecord, and associate it to your Articles accordingly.
With the appropriate indexes, the view should be fairly quick but may not really be necessary to calculate the rating value at runtime each time. You may also want to consider storing a rating column on your Articles table, if performance isn't where you need it to be. You could simply update this rating whenever an article rating is modified or created.
That said, this performance should be night and day over your current iteration.
I am making some assumptions about your implementation, like I do assume that you have value field in your Rating model that can be 1 for a "like" and -1 for "not like", etc
Start with:
class Article
has_one :rating_value, :class_name => 'RatingValue'
end
class RatingValue < ActiveRecord::Base
belongs_to :article
set_table_name "rating_values"
end
So, in a migration you generate view (postgres example):
execute %q{CREATE OR REPLACE VIEW rating_values AS
SELECT ratings.article_id, sum(ratings.value) AS value
FROM ratings
GROUP BY ratings.article_id;}
given you have a database view like this, you can make a scope you need for sorting:
scope :ascend_by_rating, {:joins => %Q{
LEFT JOIN "rating_values"
ON rating_values.article_id = article.id },
:order => "rating_values.value ASC"}
Should be much more efficent than your sorting attempt in Ruby.
From your by_ratings method, I understand that you want the articles sorted by most liked reviews/ratings.
Can we rewrite the method into a scope like this:
scope :by_ratings, select('articles.*, ((select count(id) from ratings where article_id = articles.id) - count(article_id) ) as article_diff_count')
.joins(:ratings).group('article_id').where('like = ?',true).order('article_diff_count asc')
I chose to compare the difference instead of ratio between total ratings and liked ratings since this should be lighter on the SQL engine. Hope this helps.

Traversing HABTM relationships on ActiveRecord

I'm working on a project for my school on rails (don't worry this is not graded on code) and I'm looking for a clean way to traverse relationships in ActiveRecord.
I have ActiveRecord classes called Users, Groups and Assignments. Users and Groups have a HABTM relationship as well as Groups and Assignments. Now what I need is a User function get_group(aid) where "given a user, find its group given an assignment".
The easy route would be:
def get_group(aid)
group = nil
groups.each { |g| group = g if g.assignment.find(aid).id == aid }
return group
end
Is there a cleaner implementation that takes advantage of the HABTM relationship between Groups and Assignments rather than just iterating? One thing I've also tried is the :include option for find(), like this:
def get_group(aid)
user.groups.find(:first,
:include => :assignments,
:conditions => ["assignments.id = ?", aid])
end
But this doesn't seem to work. Any ideas?
First off, be careful. Since you are using has_and_belongs_to_many for both relationships, then there might be more than one Group for a given User and Assignment. So I'm going to implement a method that returns an array of Groups.
Second, the name of the method User#get_group that takes an assignment id is pretty misleading and un-Ruby-like.
Here is a clean way to get all of the common groups using Ruby's Array#&, the intersection operator. I gave the method a much more revealing name and put it on Group since it is returning Group instances. Note, however, that it loads Groups that are related to one but not the other:
class Group < ActiveRecord::Base
has_and_belongs_to_many :assignments
has_and_belongs_to_many :users
# Use the array intersection operator to find all groups associated with both the User and Assignment
# instances that were passed in
def self.find_all_by_user_and_assignment(user, assignment)
user.groups & assignment.groups
end
end
Then if you really needed a User#get_groups method, you could define it like this:
class User < ActiveRecord::Base
has_and_belongs_to_many :groups
def get_groups(assignment_id)
Group.find_all_by_user_and_assignment(self, Assignment.find(assignment_id))
end
end
Although I'd probably name it User#groups_by_assignment_id instead.
My Assignment model is simply:
class Assignment < ActiveRecord::Base
has_and_belongs_to_many :groups
end

Resources