Reputation: 2773
I have the following models:
class Rating < ActiveRecord::Base
belongs_to :item
belongs_to :user
end
class Item < ActiveRecord::Base
has_many :ratings
end
I want to fetch all items, and the ratings made by a specific user to show the current user's rating (if it exists!) next to each item.
I've tried...
Item.includes(:ratings).where('ratings.user_id = ?', user_id)
...but that don't give me the items with no ratings.
My first thought was a has_many association with an argument, and then pass that argument with the includes method. But that doesn't seem to exist.
How do I get all posts and eager loaded association filtered on a parameter without doing N+1 queries or loading all entities into memory?
Upvotes: 6
Views: 5271
Reputation: 5528
As I recently wrote in this blog post, I'd suggest the following in your case:
items = Item.all
ActiveRecord::Associations::Preloader.new.preload(items, :ratings, Rating.where(user_id: user_id))
you can use the custom scopes of the preloader and access items.each { |i| i.ratings }
already scoped by the user.
Upvotes: 2
Reputation: 64363
Step 1
Make the current_user
accessible in the model layer (one technique is outlined here: https://stackoverflow.com/a/2513456/163203)
Step 2
Add an association with a condition that gets evaluated during run time.
Rails 2
class Item < ActiveRecord::Base
has_many :ratings
has_many :current_user_ratings,
:class_name => "Rating",
:conditions => 'ratings.user_id = #{User.current_user.try(:id)}'
end
Rails 3.1 and above
class Item < ActiveRecord::Base
has_many :ratings
has_many :current_user_ratings,
:class_name => "Rating",
:conditions => proc { ["ratings.user_id = ?", User.current_user.try(:id)] }
end
Step 3
Item.includes(:current_user_ratings)
Upvotes: 4
Reputation: 11588
It looks like you're basically modeling a has_many :through
relationship: Item has_and_belongs_to_many User, and Rating is the join model. You can read about :through
relationships in the Rails Guide to Active Record Associations.
If that is the case, I would recommend structuring your model relationships using has_many :through
as follows:
class Rating < ActiveRecord::Base
attr_accessible :item_id, :user_id
belongs_to :item
belongs_to :user
end
class User < ActiveRecord::Base
has_many :ratings
has_many :rated_items, :through => :ratings
end
class Item < ActiveRecord::Base
has_many :ratings
has_many :rated_by_users, :through => :ratings, :source => :user
end
Then, say you have the following records in the DB:
$ sqlite3 db/development.sqlite3 'SELECT * FROM items';
1|2013-03-22 03:21:31.264545|2013-03-22 03:21:31.264545
2|2013-03-22 03:24:01.703418|2013-03-22 03:24:01.703418
$ sqlite3 db/development.sqlite3 'SELECT * FROM users';
1|2013-03-22 03:21:28.029502|2013-03-22 03:21:28.029502
$ sqlite3 db/development.sqlite3 'SELECT * FROM ratings';
1|1|1|2013-03-22 03:22:01.730235|2013-03-22 03:22:01.730235
You could request all Items, along with their associated Rating and User instances, with this statement:
items = Item.includes(:rated_by_users)
This executes 3 SQL queries for you:
Item Load (0.1ms) SELECT "items".* FROM "items"
Rating Load (0.2ms) SELECT "ratings".* FROM "ratings" WHERE "ratings"."item_id" IN (1, 2)
User Load (0.2ms) SELECT "users".* FROM "users" WHERE "users"."id" IN (1)
And trying to access the User(s) that rated each Item can be done by calling the #rated_by_users
association method on each Item:
> items.map {|item| item.rated_by_users }
=> [[#<User id: 1, created_at: "2013-03-22 03:21:28", updated_at: "2013-03-22 03:21:28">], []]
Upvotes: 0