views:

359

answers:

1

Two models (Rails 2.3.8):

  • User; username & disabled properties; User has_one :profile
  • Profile; full_name & hidden properties

I am trying to create a named_scope that eliminate the disabled=1 and hidden=1 User-Profiles. The User model is usually used in conjunction with the Profile model, so I attempt to eager-load the Profile model (:include => :profile).

I created a named_scope on the User model called 'visible':

named_scope :visible, {
  :joins => "INNER JOIN profiles ON users.id=profiles.user_id",
  :conditions => ["users.disabled = ? AND profiles.hidden = ?", false, false]
}

I've noticed that when I use the named_scope in a query, the eager-loading instruction is ignored.

Variation 1 - User model only:

 # UserController
 @users = User.find(:all)

 # User's Index view
 <% for user in @users %>
  <p><%= user.username %></p>
 <% end %>

 # generates a single query:
 SELECT * FROM `users`

Variation 2 - use Profile model in view; lazy load Profile model

 # UserController
 @users = User.find(:all)

 # User's Index view
 <% for user in @users %>
  <p><%= user.username %></p>
  <p><%= user.profile.full_name %></p>
 <% end %>

 # generates multiple queries:
  SELECT * FROM `profiles` WHERE (`profiles`.user_id = 1) ORDER BY full_name ASC LIMIT 1
  SHOW FIELDS FROM `profiles`
  SELECT * FROM `profiles` WHERE (`profiles`.user_id = 2) ORDER BY full_name ASC LIMIT 1
  SELECT * FROM `profiles` WHERE (`profiles`.user_id = 3) ORDER BY full_name ASC LIMIT 1
  SELECT * FROM `profiles` WHERE (`profiles`.user_id = 4) ORDER BY full_name ASC LIMIT 1
  SELECT * FROM `profiles` WHERE (`profiles`.user_id = 5) ORDER BY full_name ASC LIMIT 1
  SELECT * FROM `profiles` WHERE (`profiles`.user_id = 6) ORDER BY full_name ASC LIMIT 1

Variation 3 - eager load Profile model

  # UserController
  @users = User.find(:all, :include => :profile)

  #view; no changes

  # two queries
  SELECT * FROM `users` 
  SELECT `profiles`.* FROM `profiles` WHERE (`profiles`.user_id IN (1,2,3,4,5,6)) 

Variation 4 - use name_scope, including eager-loading instruction

  #UserConroller
  @users = User.visible(:include => :profile)

  #view; no changes

  # generates multiple queries
  SELECT `users`.* FROM `users` INNER JOIN profiles ON users.id=profiles.user_id WHERE (users.disabled = 0 AND profiles.hidden = 0) 
  SELECT * FROM `profiles` WHERE (`profiles`.user_id = 1) ORDER BY full_name ASC LIMIT 1
  SELECT * FROM `profiles` WHERE (`profiles`.user_id = 2) ORDER BY full_name ASC LIMIT 1
  SELECT * FROM `profiles` WHERE (`profiles`.user_id = 3) ORDER BY full_name ASC LIMIT 1
  SELECT * FROM `profiles` WHERE (`profiles`.user_id = 4) ORDER BY full_name ASC LIMIT 1

Variation 4 does return the correct number of records, but also appears to be ignoring the eager-loading instruction.

Is this an issue with cross-model named scopes? Perhaps I'm not using it correctly.

Is this sort of situation handled better by Rails 3?

+1  A: 

From railsapi.com:

Eager loading of associations

[...] Since only one table is loaded at a time, conditions or orders cannot reference tables other than the main one. If this is the case Active Record falls back to the previously used LEFT OUTER JOIN based strategy. For example

Post.find(:all, :include => [ :author, :comments ], :conditions => ['comments.approved = ?', true])

will result in a single SQL query with joins along the lines of: LEFT OUTER JOIN comments ON comments.post_id = posts.id and LEFT OUTER JOIN authors ON authors.id = posts.author_id.

I believe this answers your question... there's no eager loading in "variation #4" because you references profiles table on your named_scope.

j.
OK. I could move the hidden attribute to the User model to avoid this situation altogether, but it is probably best that I find another approach for this type of situation. Can you think of one?
Craig
You can create another association: `User has_one :visible_profile`. Then you call @user.visible(:include => :visible_profile), when `visible` is the named_scope with only this condition: `["disabled = ?", false]`. It's like the example on `railsapi.com`.
j.
@j: I added an association to the User model - has_one :visible_profile, :class_name => 'Profile', :conditions => ["profiles.hidden=?",false]. Changed the User model's named_scope - named_scope :visible, { :conditions => ["disabled = ?", false] }Changed UserController's Index action - @users = User.visible(:all, :include => :visible_profile). Restarted server. Lazy loading still occurring. Profile-model filter ignored. Thoughts?
Craig
Absolutely no idea. This is the same example from railsapi.com :/
j.