Avoiding individual database calls for count - sql

My models look like this:
class Movie < ActiveRecord::Base
attr_accessible :title, :year, :rotten_id, :audience_score,
:critics_score, :runtime, :synopsis, :link, :image
has_many :jobs, :dependent => :destroy
has_many :actors, :through => :jobs
end
class Actor < ActiveRecord::Base
attr_accessible :name
has_many :movies, :through => :jobs
has_many :jobs, :dependent => :destroy
end
class Job < ActiveRecord::Base
attr_accessible :movie_id, :actor_id
belongs_to :movie
belongs_to :actor
end
When I'm displaying my index of Actors, I'd like to show the number of movies each actor has starred in. I can do this with #actor.movies.count, however this generates an SQL query for each actor. With, say, 30 actors, this will result in 30 extra queries in addition to the initial.
Is there any way to include the count of movies each actor has participated in, in the initial Actor.all call? And thereby getting things done with only one call. Extra bonus if this was sorted by said count.
Update:
All answers provided has been helpful, and though it turned into some dirt-slinging-contest at some point, it worked out well. I did a mish-mash of all your suggestions. I added a movies_counter column to my Actor model. In my Job model I added belongs_to :actor, :counter_cache => :movies_counter. This works brilliantly, and is automatically updated when i create or destroy a movie, without me adding any further code.

As #Sam noticed, you should add new column to actors table movies_counter
rails g migration add_movies_counter_to_actor movies_counter:integer
Now you can edit your migration
class AddMoviesCounterToActor < ActiveRecord::Migration
def self.up
add_column :actors, :movies_counter, :integer, :default => 0
Actor.reset_column_information
Actor.all.each do |a|
a.update_attribute :movies_counter, a.movies.count
end
end
def self.down
remove_column :actors, :movies_counter
end
end
And run it
rake db:migrate
Then you should add two callbacks: after_save and after_destroy
class Movie < ActiveRecord::Base
attr_accessible :title, :year, :rotten_id, :audience_score,
:critics_score, :runtime, :synopsis, :link, :image
has_many :jobs, :dependent => :destroy
has_many :actors, :through => :jobs
after_save :update_movie_counter
after_destroy :update_movie_counter
private
def update_movie_counter
self.actors.each do |actor|
actor.update_attribute(:movie_count, actor.movies.count)
end
end
end
Then you can call some_actor.movies_counter

Add a column to your Actor table called 'movie_count'. Then add a call back in your Actor model that updates that column.
class Movie < ActiveRecord::Base
has_many :actors, :through => :jobs
before_save :update_movie_count
def update_movie_count
self.actor.update_attribute(:movie_count, self.movies.size)
end
end
That way your just have an integer that gets updated instead of calling all records.

Related

Use target's scope as a condition of association

I have models like
class Employee < ActiveRecord::Base
belongs_to :branch, :foreign_key => :branch_code, :primary_key => :branch_code,
:conditions => proc{["? BETWEEN enabled_day AND expiration_day", Date.current]}
end
class Branch < ActiveRecord::Base
has_many :employees, :foreign_key => :branch_code, :primary_key => :branch_code
scope :valid, lambda {where(["? BETWEEN enabled_day AND expiration_day", Date.current])}
end
employee belongs to an branch (simple), but branch has several records of same branch_code, and one that is "valid at this moment" should always be used.
(as you may guess, the project is porting of an old app and it succeeds the old schema)
Now, it does work, but I have to write exact same where condition twice (actually branch is associated to more tables, so 5 or 6 times).
wonder if I could use Branch's scope as condition of an association, or any other way to DRY things up?
Would using has_many_through with default_scope work?
Something along the lines of:
class Employee < ActiveRecord::Base
has_many :assignments
has_one :branch, :through => :assignments
end
class Branch < ActiveRecord::Base
has_many :assignments
has_many :employees, :through => :assignments
end
class EmployeeAssignments < ActiveRecord::Base
attr_accessible :enabled_day, :expiration_day
belongs_to :employee
belongs_to :branch
def self.default_scope
where '? BETWEEN enabled_day AND expiration_day', Date.current
end
end

Ordering records by the number of associated records

Users can submit resources and post comments.
I want to show the users who are most 'active' by selecting the users who have submitted resources and comments and order the results from the user that has submitted the most resources and comments combined to the least.
**Resource**
has_many :users, :through => :kits
has_many :kits
belongs_to :submitter, class_name: "User"
**User**
has_many :resources, :through => :kits
has_many :kits
has_many :submitted_resources, class_name: "Resource", foreign_key: "submitter_id"
**Kits**
belongs_to :resource
belongs_to :user
**Comments**
belongs_to :user
I am new to this kind of sql in Rails. How can I get this record set?
First, you will need to add the comments association to the User model:
has_many :comments
With this, the simplest solution is to do this:
User.all.sort do |a,b|
(a.submitted_resources.count + a.comments.count) <=> (b.submitted_resources.count + b.comments.count)
end
This will get very slow, so if you want to do better you will want to add counter caches. In a migration:
def up
add_column :users, :submitted_resources_count, :integer
add_column :users, :comments_count, :integer
User.reset_column_information
User.find_each do |u|
u.update_attributes! \
:submitted_resources_count => u.submitted_resources.count,
:comments_count => u.comments.count
end
end
def down
add_column :users, :submitted_resources_count
add_column :users, :comments_count
end
Once you run this migration, you can change the original query to:
User.select('*, (submitted_resources_count + comments_count) AS activity_level').order('activity_level DESC')
This will very efficiently return all users in the proper order, and as a bonus each user will have a read-only attribute called activity_level that will give the exact submitted resources + comments count.

Rails ActiveRecord Query (Cross model)

I have an app that lets users input dates & interests that relate to those dates .I need to send them deals (a few days before the date - Via Email) that are based off of their interests and location. I have all the models setup and recording the data properly, just wondering how to query the models for the dates and then send the appropriate deal based off of the city and interests.
Notes:
*Each city and interest category has only 1 deal
*I have several different models for types of dates (Holidays, Occasions, Friends Birthdays ect).. all are pretty much identical in structure.
*All interests for each type of date are stored in person_interests.
Models:
Class User
belongs_to :province
belongs_to :city
has_many :friends_bdays
has_many :occasions
has_many :person_interests, :as => :person
has_many :interests, :through => :person_interests
has_many :user_holidays
has_many :holidays, :through => :user_holidays
has_many :anniversaries
end
class Deal < ActiveRecord::Base
belongs_to :interest
belongs_to :city
belongs_to :store
end
class Store < ActiveRecord::Base
has_many :deals
belongs_to :city
belongs_to :province
end
class PersonInterest < ActiveRecord::Base
belongs_to :interest
belongs_to :person, :polymorphic => true
end
class Interest < ActiveRecord::Base
has_many :person_interests
has_many :deals
end
class Occasion < ActiveRecord::Base
belongs_to :user
belongs_to :admin_user
has_many :person_interests, :as => :person
has_many :interests, :through => :person_interests
end
class Anniversary < ActiveRecord::Base
belongs_to :user
has_many :person_interests, :as => :person
has_many :interests, :through => :person_interests
end
class Friend_bday < ActiveRecord::Base
belongs_to :user
has_many :person_interests, :as => :person
has_many :interests, :through => :person_interests
end
You can achieve this using a variation of the solution below:
Install the squeel gem
class User
def deals(reload=false)
#deals = nil if
#deals ||= Deal.where{
( (:city => city_id) | ( :interest_id => interest_ids) ) &
:deal_date => (Time.now..3.days.from_now)
}
end
end
Now, user.deals returns the deals that will be active in next 3 days matching the user's city OR interests.
Edit 1:
Based on your comment it looks like you don't need the squeel gem. You can achieve what you want using regular AR syntax.
class User
def deals(reload=false)
#deals = nil if reload
#deals ||= Deal.where(
:city => city_id,
:interest_id => interest_ids,
:deal_date => (Time.now..3.days.from_now)
)
end
end

What's the rails way to include a field in a join model when listing an association?

So if I have the following relationship
class Item < ActiveRecord::Base
has_many :item_user_relationships
has_many :users, :through => :item_user_relationships
end
class User < ActiveRecord::Base
has_many :item_user_relationships
has_many :items, :through => :item_user_relationships
end
class ItemUserRelationship < ActiveRecord::Base
belongs_to :item
belongs_to :user
attr_accessible :role
end
What's the rails way to include the role attribute when listing all the Users of an Item?
#users = #item.users # I want to include the role as part of a user
Thanks!
UPDATE: I'm still having trouble with this. My goal is to get an array of User models that have their role included in the attributes.
I'm note sure if I understand you correctly, but maybe this is what you want?
#users = #item.users
#users.each do |user|
puts user.item_user_relationships.first.role
end

How to store the 'total' count of this polymorphic model in its associated models (Rails)?

I have a model called Vote that has a polymorphic association with other two models: Microposts and Comments.
Here is their association:
micropost.rb:
class Micropost < ActiveRecord::Base
belongs_to :user
has_many :comments, :dependent => :destroy
has_many :votes, :as => :votable, :dependent => :destroy
end
comment.rb:
class Comment < ActiveRecord::Base
belongs_to :micropost, :counter_cache => true
belongs_to :user
has_many :votes, :as => :votable, :dependent => :destroy
end
vote.rb:
class Vote < ActiveRecord::Base
belongs_to :votable, :polymorphic => true
belongs_to :user
end
The Vote model has a column called polarity (for voting up +1 and voting down -1).
I would like to add a total (the sum of the polarity of all the votes of a post or comment) to each instances of the Post and Comment model.
What's the best way of accomplishing this?
Example:
Post (:id = 2)
Vote (:polarity = 1) Vote (:polarity = -1) Vote (:polarity = 2)
Post with ID 2 has a :total of 2 votes
You can create an instance method in Post and Comment models like:
def total
self.votes.map {|v| v.polarity }.sum
end
Then put #post.total or #comment.total
Or you can create a helper which can do the same:
def total object
object.votes.map {|v| v.polarity }.sum
end
Then put <%= total #post %> or <%= total #comment %>