Rails' named_scope: Picking Up Where Associations Leave Off
With the addition of named_scope to Edge Rails a while ago (merged from the has_finder plugin), many doors were opened and many codes were DRY'd up (presumably). Fast-forward to today when I was faced with an interesting situation…
In my scenario I have Account, Document, and DocumentFilter classes:class Account < ActiveRecord::Base
has_many :documents
has_many :document_filters
end
class Document < ActiveRecord::Base
belongs_to :account
end
class DocumentFilter < ActiveRecord::Base
belongs_to :account
has_many :rules
def to_sql()
... produces a string for the WHERE clause based on its rules ...
end
end
The DocumentFilter can easily be changed to add/remove rules like "document type is Spreadsheet" (SQL: "type = 'Spreadsheet'). Given that, I needed to create a method called DocumentFilter#documents that would:
a) return all its account's documents
b) filter the documents based on its rules
One might consider creating a join table called DocumentFiltersDocuments, which would create a "has_many :documents, :through => :document_filters_documents" on the DocumentFilter class. But we really don't want to keep an extra table row for each match between a Document and a DocumentFilter.
Both Document and DocumentFilter share no foreign_key relationship in the database besides that they might both belong to the same Account, so my first idea was that I could do a has_many :documents, :through => :account (based on the "DocumentFilter#belongs_to :account" relationship). This would not work though, since has_many :through doesn't work with a belongs_to in the middle (and was really meant to work with join tables, afaik).
There are traces of attempts to change this, but I didn't want to rely on a hack or try to rewrite a new AssociationCollection class for ActiveRecord. So here's my solution (which required just a few more lines of code than a has_many):
class Document < ActiveRecord::Base
belongs_to :account
named_scope :for_document_filter, lambda { {|df| :conditions => df.to_sql]} }
end class DocumentFilter < ActiveRecord::Base
belongs_to :account
def documents(force_reload=false)
@documents = account.documents.for_document_filter(self) if @documents.nil? || force_reload
end
end
And if you're on the latest Edge Rails, you can also shorten it further with memoize():
def documents
account.documents.for_document_filter(self)
end
memoize :documents
Now you can almost treat DocumentFilter#documents as you would a has_many.
df = DocumentFilter.first # account_id is 1 and df.to_sql() returns "type = 'Spreadsheet'"df.documents
#=> SELECT * FROM `documents` WHERE (`documents`.account_id = 1 AND (type = 'Spreadsheet')
df.documents
#=> same results, but cached in @documents, so there is no SQL
df.documents(true)
#=> SELECT * FROM `documents` WHERE (`documents`.account_id = 1 AND (type = 'Spreadsheet')
df.documents.find(:all, :conditions => "true", :limit => 5)
#=> SELECT * FROM `documents` WHERE (`documents`.account_id = 1 AND (true)) AND (type = 'Spreadsheet') LIMIT 5
Conclusion: named_scope is rule.
Comments
IT OK BUT HAVE A LITTLE PROMBLE GET ON MY LIVE WIRE ,BUT OTHER THAN THAT U ARE REAL GOOD AKA MARTY