Alexandre
Alexandre

Reputation: 5135

Loading ONE record for has_many and checking it

I'm implementing a Blog with Post and votable Comments.

When loading a Post, I want to eagerly load all votes by the current user for the Post's Comments.

Something like this (which doesn't work):

@post.comments.all(:joins => :votes, :conditions => ['votes.user_id = ?', current_user.id])

Each Comment has a method called rated_by?

def rated_by?(actor)
  votes.find_by_user_id(actor.id)
end

The problem is that ActiveRecord will run a query for each rated_by? call, even though my @post.comments finder joined all the relevant votes.

I had a look at the act_as_rateable plugin but it has the same problem, running a query for each record, not using joins.

Upvotes: 0

Views: 223

Answers (2)

EmFi
EmFi

Reputation: 23450

Double Secret Edit: I was answering another question and came across something that should work for you. It's a bit of a crazy hack involving the Thread.current global hash. And probably not advised at all, but it works.

It involves creating a second has_many votes association on Comments

class Comment < ActiveRecord::Base
  has_many :votes
  belongs_to :post
  has_many :current_user_votes, :class_name => "Vote",
    :conditions => '`#{Vote.table_name}`.user_id = \
    #{Thread.current[:current_user].id}'
end

It also requires you to set Thread.current[:current_user] = current_user in the controller where you're going to be calling these methods.

Then you should be able to do

@post.comments.find(:all, :include => :current_user_votes) 

To get a list of comments, that have eager loaded only the :current_user_votes. All in one query. If you're getting multiple posts at once, you can do this.

Post.find(:all, :include => { :comments => :current_user_votes},
  :conditions => ...) 

Which will populate a list of posts, and eager load their comments which in turn will each have their current_user_votes eager loaded.

Original Answer (preserved for posterity)

I don't think it's possible to select all of one model eager load only the relevant associations in one query.

The best you're going to get is pretty much what you've done. Select all of one model and then for each them load only the relevant association with a named scope or finder.

This statement that doesn't work is only selecting comments the user has voted on.

@post.comments.all(:joins => :votes, 
  :conditions => ['votes.user_id = ?', current_user.id])

This statement selects the same set of comments, but also eager loads all votes for the comments it selects.

@post.comments.all(:include => :votes, 
  :conditions => ['votes.user_id = ?', current_user.id])

Really what you're going to have to do is call rated_by? on each comment. You might be able to minimize database impact by using a named scope. But I honestly don't think it's going to make an improvement.

If you're so worried about hitting the database so hard you could do something like this:

class Post < ActiveRecord::Base
  has_many :comments
  has_many :votes, :through => :comments
  ...
end

class Vote < ActiveRecord::Base
  belongs_to :comments
  ...

  named_scope :made_by_user, lambda {|user| 
    {:conditions => {:user_id => user}}
  }
end

@users_votes = @post.votes.made_by_use(current_user)

@comments = @post.comments.find(:all, :include => :votes)

@comments.each{|comment|
  user_voted_this_on_this_comment = comment.votes & @user_votes
  ...
}

Honestly I don't think it's worth the effort.

P.S. There's a Ruby convention regarding methods names that end in a question mark should always return a boolean value.

Upvotes: 3

amitkaz
amitkaz

Reputation: 2712

you need to use

:include => :votes

joins doesn't load your data, it just join the query in the db.

Upvotes: 1

Related Questions