Given an array of ActiveRecord objects, can I easily collect their relationships through a method call?
-
03-07-2019 - |
Question
Let's say I have the following code:
@sites = Site.find(session[:sites]) # will be an array of Site ids
@languages = Language.for_sites(@sites)
for_sites is a named_scope in the Language model that returns the languages associated with those sites, and languages are associated with sites using has_many through. The goal is for @languages to have a distinct array of the languages associated with the sites.
Instead of calling the Language object on the second line, I'd ideally like to say
@sites.languages
and have the same list returned to me. Is there any way to do that cleanly in Rails 2.1 (or edge)? I know associations and named scopes can extend the array object to have attributes, but unless I'm missing something that doesn't apply here. Any plugins that do this would be welcome, it doesn't have to be in core.
Solution
You could extend the array returned by Site.find.
class Site
def find(*args)
result = super
result.extend LanguageAggregator if Array === result
result
end
end
module LanguageAggregator
def languages
Language.find(:all, :conditions => [ 'id in (?)', self.collect { |site| site.id } ])
end
end
OTHER TIPS
Why not use named_scopes for both?
class Site
named_scope :sites, lambda{|ids| :conditions => "id in (#{ids.join(',')})"}
named_scope :languages, :include => :languages ... (whatever your named scope does)
end
call:
Site.sites(session[:sites]).languages
or, if you want language objects back
Site.sites(session[:sites]).languages.collect{|site| site.languages}.flatten
You can also do it directly on the Language object. I'm using :joins because Rails 2.1 splits up and :include into two queries which means we can't use sites in the :conditions
class Language
named_scope :for_sites, lambda{|site_ids| :joins => 'inner join sites on languages.site_id = sites.id' :conditions => "sites.id in (#{site_ids.join(',')})"}
end
call:
Language.for_sites(session[:sites])
In both examples I've assumed that session[:sites] is completely controlled by you and not subject to SQL injection. If not, make sure you deal with cleaning up the ID's
Your instance variable @sites is an Array object and not Site so I don't think named_scope can be used. You can open up Array class to achieve this effect though (yikes)
class Array
def languages
...
end
end
If you added a has_many
or has_and_belongs_to_many
linking languages to sites, then you could use an include and do something like this:
Site.find( :all, :conditions =>{:id => session[:sites]}, :include => :languages )
You can make a named scope to do the :id => session[:sites] thing, eg:
class Site
named_scope :for_ids, lambda{ |x| {:conditions => {:id => x }
end
and then do
Site.for_ids(session[:sites]).find(:all, :include => :languages)
Hope this gives you some ideas