6

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?

thejaz
  • 2,763
  • 2
  • 26
  • 40

3 Answers3

4

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)
Community
  • 1
  • 1
Harish Shetty
  • 64,083
  • 21
  • 152
  • 198
  • 1
    Worked perfectly! A small note: The answer in step 1 you linked to does not take precautions that the thread variable isn't shared between requests. I used the gem request_store to make it simple to store the current user and make sure it is not leaked. – thejaz Apr 23 '13 at 20:18
  • Yeh it looks like `thin` and `puma` servers have issues. BTW, the gem uses `Thread.current` store, they clear it after the call. – Harish Shetty Apr 23 '13 at 23:36
  • I have fixed the thread leak issue in the solution suggested in Step 1. – Harish Shetty Apr 23 '13 at 23:44
  • I think bringing the `current_user` to the model layer is wrong. there are better solution for this case. – coorasse Oct 31 '19 at 14:57
1

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.

coorasse
  • 5,278
  • 1
  • 34
  • 45
0

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">], []] 
Stuart M
  • 11,458
  • 6
  • 45
  • 59
  • It seems to work fine! I guess this is the answer to [this question](http://stackoverflow.com/questions/9642900/eager-loading-the-right-way-to-do-things) as well? This seems to be a problem with no current answer on Stack Overflow (the answer on the link loads all associated objects into memory and filters programmatically) – thejaz Mar 21 '13 at 09:21
  • I've run into a problem with this solution - if item A has one rating and I'm querying for item B, I don't get item B. Seems the existence of the rating blocks the retrieval of the other item? – thejaz Mar 21 '13 at 19:27
  • If user with user_id 1 has a rating on item 485, the request for item 485 with filter on user_id 2 doesn't return any item. NO RESULT: `SELECT DISTINCT "items".id FROM "items" LEFT OUTER JOIN "ratings" ON "ratings"."item_id" = "items"."id" WHERE (items = 485) AND (ratings.user_id = 2 OR ratings.user_id IS NULL)` RESULT ITEM 485: `SELECT DISTINCT "items".id FROM "items" LEFT OUTER JOIN "ratings" ON "ratings"."wine_id" = "items"."id" WHERE (items.id = 485) AND (ratings.user_id = 1 OR ratings.user_id IS NULL)` – thejaz Mar 21 '13 at 19:33
  • I see now. I've completely rewritten my answer with a better solution, please look again – Stuart M Mar 22 '13 at 03:44
  • The problem is that an item can be connected to a large number of ratings, so I don't want to do the filtering in the code. If I want to do the filtering client side with N+1, I guess I could do `Items.where(...)` and then `item.ratings_by_user_id(user_id)` using a scope with parameter. But I would like to solve it without N+1 queries.. Do you think that is possible? – thejaz Mar 22 '13 at 15:05
  • Right but this still does not do N+1 queries, it does exactly 3 queries. The SELECT statements from `ratings` and `users` are using an `IN (...)` clause to look up N records from each in a single query. – Stuart M Mar 22 '13 at 16:19
  • 2
    But your solution doesn't filter on a user? Is that possible while not doing N+1 queries? – thejaz Mar 23 '13 at 10:49