Displaying articles with tag

Problem with named_scope and :include?

Posted by andy, Thu Jun 26 12:30:00 UTC 2008

Recently I've been using the great Rails 2.1 addition called named_scope to help simplify the process of building queries with meaningful names. Certainly you could do something similar by creating a custom method, but as I wrote earlier, the advantage of named_scope is that the scopes are composable: you can chain them together to build ever more powerful queries.

That's why I've been really frustrated trying to get named scopes to work with the :include option. In one scenario I have been trying to sort a collection by a field that exists in an included table. If I were doing freight-forwarding the models involved look something like this.

class Customer < ActiveRecord::Base
  has_many :product_offerings
  has_many :products, :through=>:product_offerings
end

class Product
  has_many :product_offerings
end

class ProductOffering
  belongs_to :customer
  belongs_to :product

  named_scope :by_name, :include=>:product, :order=>:name
  named_scope :for_customers, lambda{|customer_ids| {:conditions=>{:customer_id=>customer_ids}}}
end

The objective I have in mind is to build a named_scope that allows me to list all the products for a particular customer alphabetically by the name of the product so each customer can check their inventory. In the day and age where holding companies are involved an individual user may be authorized for several companies and thus want to be able to get a unified inventory list for all the companies for whom he works. That's where the second named_scope comes into play. I'd like to be able to do this:

# all the product offerings listed by product name
ProductOffering.by_name

# all the product offerings for a set of customers
ProductOffering.for_customers([1, 2, 3])

# all the product offerings by name for a user's companies
ProductOffering.by_name.for_customers(@current_user.customer_ids)

But it doesn't work. For some reason the :include option seems to get dropped and I end up getting SQL errors reporting an unknown column name. I've found one stray comment that suggests that you may be able to fix the issue by adding the conditions necessary to make the SQL join work. That is we could expand the named_scope to something like

named_scope :by_name, :include=>:product, :order=>:name, :conditions=>["products.id = product_offerings.product_id"]

That's ugly. Really ugly. It also pushes perilously close to letting ProductOffering peek into Product too much. If you begin to go that route be careful because you'll be on the slippery slope of a brittle solution that won't survive refactoring.

The simple solution is to bypass :include in favor of :join. I don't understand why :join works more reliably but I'm sure the answer is down there in the source code if you want to dig around. I suspect that the answer lies in the way that Rails 2.1 breaks joins into two distinct fetches now in order to reduce the cost of spinning up redundant ActiveRecord instances (see the Relationship Optimised Eager Loading discussion.). Whatever the case, the workable, concise solution looks like this:

named_scope :by_name, :joins=>:product, :order=>:name

If you already need to include conditions on the join table you can probably continue along as you normally would. In this situation the only requirement on the join table is that the IDs match so I prefer the concise solution.

1 comment | Filed Under: | Tags: