ActiveRecord Association Loaded?

ActiveRecord association classes extend ActiveRecord::Associations::AssociationProxy. The Association::Proxy class is mostly transparent to developers, but it provides the base for some of the magic of associations. It also has a method I find particularly useful for writing efficient code: loaded?

For an arbitrary example to demonstrate the usefulness, let's say we have two models: Team and Player. A Team has many Players, and a Player has an attribute we're interested in: "leader", a boolean value indicating if the player is a leader or not.

Now, let's implement a method on Team called "has_leader?" which returns true if a leader is on the team, false if not. Here is what the implementation might look like:

class Player
  belongs_to :team
  scope_out :leader
end

class Team
  has_many :players

  def has_leader?
    !self.players.find_leader(:first).nil?
  end
end

Note: find_leader is made available through the scope_out plugin.

This implementation is simple and clean, but it may not always be efficient. What's the problem? Think of a page listing teams, which places an asterisks next to each team which has at least one leader on it. As you're looping through an array of 25 teams, you're going to have to run a database query for each one to know if the team has a leader.

We can fix that - let's eager load the players and loop through them looking for a leader.
class Team
  has_many :players

  def has_leader?
    self.players.detect(&:leader?)
  end
end

Now we can do @teams = Team.find(:all, :include => :players) and be able to display if each team has a leader without running additional queries.

This solves the original problem, but now creates another one. What if we're only displaying one team and it's more efficient to run 1 extra database query than to eager load the players? Then the first implementation is better!

The solution: use both implementations and check for eager loading!

class Team
  has_many :players

  def has_leader?
    if players.loaded?
      # players have been eager loaded
      # loop through them looking for a leader
      players.detect(&:leader?)
    else
      # players have not been loaded
      # run a query to look for a leader 
      !players.find_leader(:first).nil?
    end
  end
end
Ah, much better.

Disclaimer: this practice may vary depending on your server hardware and application models. If each team has a 100-person roster, eager loading those players for 25 teams is not a good idea. However, if each team only has a few players, it may be better to eager load players rather than executing more queries.