views:

44

answers:

3

Let's say that I'm looking for a specific item in a has_many association.

Is there a way to find that won't rehit the database if what I'm requesting is already cached?

Here's my situation (which is not working and does not do what I requested):

class User
  has_many :friendships

  def some_function(buddy)
    f = friendships.detect{|friendship| friendship.user == self && 
                                        friendship.friend == buddy}
    f.do_something
  end
end

The function is susceptible to N+1 problems, but most of the time I will already have either used :include or preloaded the user's friendships elsewhere. However, if I didn't do this, then this current function will fall back to requesting all the friendships, even though it only needs one. The solution would seem to be something like this:

f = Friendship.find_by_user_id_and_friend_id id, buddy.id

However, this will hit the database again even if all the friendships are cached and I have what I need right there.

Is there any way to either use the cached copy, or if none is available, search a specific record?

A: 

If you have already accessed the @user.friendships attribute (and thus, hitting the database to get the data), those associations will be stored in memory and you will no longer need to hit the database.

When it comes to finding a specific record out of an associated :has_many dataset, I'm not quite sure. One option is to do a find_by_sql to write the sql to grab that specific record. You could also do something like this:

class User
  has_many :friendships

  def some_function(buddy)
    f = Friendships.find_by_user_id(self.id, :conditions => "buddy_id = #{buddy.id}")
    f.do_something
  end
end

Which does a basic SQL search to match any records that have the current users's ID and the buddy ID that is passed into the function.

Mike Trpcic
Are you certain? What I'm seeing here unless I'm reading my logs wrong is that whenever I do an Activerecord find, it will rerun a query, even if it's already been loaded. So if @user.friendships has been accessed, a subsequent Friendships.find_by_user_id hits the database again.
WIlliam Jones
Doing subsequent ActiveRecord finds, but when using associations it should cache the associated values in your parent object. Do @user.friendships in a controller somewhere TWO TIMES, and see if the query comes up in your server logs twice.
Mike Trpcic
A: 

If you already gave called @user.friendships and want to access the cached version, you could use

def some_function(buddy)
  f = friendships.select { |f|  f.buddy_id == buddy.id }
  f.do_something
end

The select method does not make a DB query and uses the cached friendships relationship. If you have not already called @user.friendships, the first call to this method will run the query and subsequent calls will use the cached version.

Beerlington
Right, so it's the same as the version I posted in that if it's already cached, it works great, but if it's not already cached, it pulls in a ton of extraneous friendships when there's only one specific one that it needs.
WIlliam Jones
+1  A: 

What you want is the #loaded? method of the association proxy:

class User
  has_many :friendships

  def some_function(buddy)
    if friendships.loaded? then
      f = friendships.detect{|friendship| friendship.user == self && 
                                          friendship.friend == buddy}
    else
      Friendship.find_by_user_id_and_friend_id id, buddy.id
    end
  end
end

Unfortunately, #loaded? does not appear in the public documentation. You can always guard your call to #loaded? with friendships.respond_to?(:loaded?).

Documentation for AssociationProxy#loaded? on GitHub.

François Beausoleil