4

I have a lot of cases in my app where a user has no more than one object (say, a "Description") within its association to another object (a "Group").

For example:

class User < ActiveRecord::Base
  has_many :descriptions
  has_many :groups

class Group < ActiveRecord::Base
  has_many :users
  has_many :descriptions

class Description < ActiveRecord::Base
  belongs_to :user
  belongs_to :group

If I wanted to render all the users in certain group and include their relevant descriptions, I could do something this:

#users model
def description_for(group_id)
  descriptions.find_by_group_id(group_id)
end

#view
@group.users.each do |user|
   user.name
   user.description_for(@group.id).content

But this generates a huge number of Description queries. I've tried using joins:

#controller
@group = Group.find(params[:id], :joins => [{:users => :descriptions}], :conditions => ["descriptions.group_id = ?", params[:id]])

But since I'm still calling user.description_for(@group.id) it doesn't help with the page loading.

UPDATE: Sample generated SQL

Rendered users/_title.html.haml (1.6ms)
CACHE (0.0ms)  SELECT "users".* FROM "users" WHERE "users"."id" = 37 LIMIT 1
User Load (0.2ms)  SELECT "users".* FROM "users" WHERE "users"."id" = 7 LIMIT 1
CACHE (0.0ms)  SELECT "groups".* FROM "groups" WHERE "groups"."id" = 28 LIMIT 1
Description Load (0.1ms)  SELECT "descriptions".* FROM "descriptions" WHERE "descriptions"."target_id" = 7 AND "descriptions"."group_id" = 28 LIMIT 1
CACHE (0.0ms)  SELECT "descriptions".* FROM "descriptions" WHERE "descriptions"."target_id" = 7 AND "descriptions"."group_id" = 28 LIMIT 1
Rendered users/_title.html.haml (1.7ms)
CACHE (0.0ms)  SELECT "users".* FROM "users" WHERE "users"."id" = 37 LIMIT 1
User Load (0.3ms)  SELECT "users".* FROM "users" WHERE "users"."id" = 51 LIMIT 1
CACHE (0.0ms)  SELECT "groups".* FROM "groups" WHERE "groups"."id" = 28 LIMIT 1
Description Load (0.1ms)  SELECT "descriptions".* FROM "descriptions" WHERE "descriptions"."target_id" = 51 AND "descriptions"."group_id" = 28 LIMIT 1
CACHE (0.0ms)  SELECT "descriptions".* FROM "descriptions" WHERE "descriptions"."target_id" = 51 AND "descriptions"."group_id" = 28 LIMIT 1
Rendered users/_title.html.haml (1.8ms)
CACHE (0.0ms)  SELECT "users".* FROM "users" WHERE "users"."id" = 37 LIMIT 1
User Load (0.2ms)  SELECT "users".* FROM "users" WHERE "users"."id" = 5 LIMIT 1
CACHE (0.0ms)  SELECT "groups".* FROM "groups" WHERE "groups"."id" = 28 LIMIT 1
Description Load (0.1ms)  SELECT "descriptions".* FROM "descriptions" WHERE "descriptions"."target_id" = 5 AND "descriptions"."group_id" = 28 LIMIT 1
CACHE (0.0ms)  SELECT "descriptions".* FROM "descriptions" WHERE "descriptions"."target_id" = 5 AND "descriptions"."group_id" = 28 LIMIT 1
Rendered users/_title.html.haml (1.7ms)
CACHE (0.0ms)  SELECT "users".* FROM "users" WHERE "users"."id" = 37 LIMIT 1
User Load (0.2ms)  SELECT "users".* FROM "users" WHERE "users"."id" = 52 LIMIT 1
CACHE (0.0ms)  SELECT "groups".* FROM "groups" WHERE "groups"."id" = 28 LIMIT 1
Description Load (0.2ms)  SELECT "descriptions".* FROM "descriptions" WHERE "descriptions"."target_id" = 52 AND "descriptions"."group_id" = 28 LIMIT 1
CACHE (0.0ms)  SELECT "descriptions".* FROM "descriptions" WHERE "descriptions"."target_id" = 52 AND "descriptions"."group_id" = 28 LIMIT 1
Rendered users/_title.html.haml (1.7ms)
kateray
  • 2,066
  • 4
  • 18
  • 23
  • Can you include the generated SQL queries (or at least a sample thereof)? The goal is to read the data already selected by the first query rather than issue new queries with find_by_*. – Ben Hughes Jul 26 '11 at 15:53
  • `@group.description_for(user.id)` instead of `user.description_for(@group.id)` and `@group = Group.where(:id => params[:id]).joins(:descriptions)`?? Maybe, maybe not... – Robin Jul 26 '11 at 20:07
  • Ben - updated to include – kateray Jul 27 '11 at 17:36
  • Robin - still doing just as many queries, I guess because model.description_for still has a "find" call in it – kateray Jul 27 '11 at 17:37
  • The `description_for user_id` method in group would be `description_for(user_id); descriptions.where(:user_id => user_id).first; end`. I thought that, since descriptions are already loaded, it wouldn't have to query the database again. – Robin Aug 03 '11 at 12:43

2 Answers2

1

Right, I think that actually you don't need the joins clause in rails 3. If you use include and where, Arel will do the hard work for you.

I've tested this (albeit using a different set of models (and attributes) than yours) using models with the same underlying arrangement of associations, and I think this should work:

in models/user.rb:

scope :with_group_and descriptions, lambda { |group_id| includes(:groups, :descriptions).where(:groups => { :id => group_id }, :descriptions => { :group_id => group_id }) }

Then in your controller you call:

@users = User.with_group_and_descriptions(params[:id])

Finally in the view you can then do:

@users.each do |user|
  user.name
  user.descriptions.each do |desc|
    desc.content
# or
@users.each do |user|
  user.name
  user.descriptions[0].content

If I've gotten my thinking right then this should only make 2 db calls. One to get a list of user_ids and the second to get the user, group and description data, and even though you're calling a user object's descriptions method, which would ordinarily have all the descriptions in (not just the ones for a particular group), because you've already populated the association rails won't go off an grab all the associations again when you call user.descriptions, instead it'll just list the ones you've pulled from the DB using the descriptions.group_id where clause. Calling user.descriptions(true) however will force a reload of the descriptions leading to it returning an array of all the description associations for a user.

Chris Bailey
  • 4,126
  • 24
  • 28
-1

Take a look at include--it specifies an association that should be eager-loaded.

Michelle Tilley
  • 157,729
  • 40
  • 374
  • 311
  • Yes, but 'joins' is recommended when you want to use conditions. Anyway, it still doesn't solve my problem of calling the correct description for a given user and group without a huge number of queries. – kateray Jul 25 '11 at 21:07