Topic: DRY up working with arrays of models

So I've read all the canonical association proxy posts.  I really enjoy changing this:

class Company < ActiveRecord::Base

  has_many :administrators, :class_name => "User", :conditions => 'is_admin = 1'

  def admins_emails
    emails = []
    self.administrators.each do |admin|
      emails << admin.email_with_name if admin.is_active = true
    end
    emails
  end
end


To this:

class Company < ActiveRecord::Base

  has_many :administrators, :class_name => "User", :conditions => 'is_admin = 1' do
    def active(reload=false)
      @active_administrators = nil if reload
      @active_administrators ||= find(:all, :conditions => "is_active = 1")
    end
  end

  def admins_emails
    emails = []
    self.administrators.active.each do |admin|
      emails << admin.email_with_name
    end
    emails
  end
end


But then I add a users association in which I also want to subset active users:

  has_many :users, :dependent => :destroy do
    def active(reload=false)
      @active_users = nil if reload
      @active_users ||= find(:all, :conditions => "is_active = 1")
    end
  end

And if I want similar functionality with any other class, like Project or Client, there I go repeating myself again.

It sure seems like there should be a way to centralize that logic.  In The Rails Way post, the only commentor who dissented was Dan Manges, who suggested the scope_out plugin.  Apparently this will handle all the caching things for you as well.

Are there any other options out there?  I feel this topic has been talked about a fair amount in the community, but there certainly isn't any consensus.

Re: DRY up working with arrays of models

Another way to do this is through class methods. Simply add a class method to your User model for doing the find:

# in user model
def self.active
  find(:all, :conditions => "is_active = 1")
end

And then you can just call this on the association:

@company.administrators.active
@project.users.active

The only downside is you can't really cache it because an instance variable would be global inside the User class.

Alternatively you may want to try moving it into a module and use the :extend option on the association. I haven't done much with that though.

BTW, that admin_emails method can be shortened to this:

def admins_emails
  administrators.active.collect(&:email_with_name)
end

Railscasts - Free Ruby on Rails Screencasts

Re: DRY up working with arrays of models

Thanks, Ryan.

Caching is a fairly significant downside.  I think the module idea may work, but it's probably only feasible with a reasonable number of similar associations.

ryanb wrote:

BTW, that admin_emails method can be shortened to this ...

Thanks.  How embarrassing!

Last edited by bjhess (2007-10-10 21:27:25)

Re: DRY up working with arrays of models

Take a look at the second presentation Obie Fernandez posted the other day, Advanced ActiveRecord.  He tackles this on page 53-57.

To paraphrase with similar logic to what I posted above.  Starting with this:

class Company < ActiveRecord::Base
 
  has_many :users do
    def active(reload=false)
      @active_users = nil if reload
      @active_users ||= find(:all, :conditions => "is_active = 1")
    end
  end
 
  has_many :administrators, :class_name => "User", :conditions => 'is_admin = 1' do
    def active(reload=false)
      @active_administrators = nil if reload
      @active_administrators ||= find(:all, :conditions => "is_active = 1")
    end
  end

  . . .
end

class Project < ActiveRecord::Base

  has_many :users do
    def active(reload=false)
      @active_users = nil if reload
      @active_users ||= find(:all, :conditions => "is_active = 1")
    end
  end

  has_many :administrators, :class_name => "User", :conditions => 'is_admin = 1' do
    def active(reload=false)
      @active_administrators = nil if reload
      @active_administrators ||= find(:all, :conditions => "is_active = 1")
    end
  end

  . . .
end


Obie would refactor to a module, leaving:

class Company < ActiveRecord::Base
  include UserRelated 
  . . .
end

class Project < ActiveRecord::Base
  include UserRelated 
  . . .
end

module UserRelated

  def self.included(base)
    base.class_eval do
   
      has_many :users do
        def active(reload=false)
          @active_users = nil if reload
          @active_users ||= find(:all, :conditions => "is_active = 1")
        end
      end
     
      has_many :administrators, :class_name => "User", :conditions => 'is_admin = 1' do
        def active(reload=false)
          @active_administrators = nil if reload
          @active_administrators ||= find(:all, :conditions => "is_active = 1")
        end
      end
    end
  end
end


I imagine there is a way to DRY up those method definitions for "active" as well...

Of course, Obie finished up with:

Obie wrote:

There's a fine balance to strike here.

Magic like include Commentable certainly save on typing and makes your model look less
complex, but it can also mean that your association code is doing things you don't know
about.

This can lead to confusion and hours of head-scratching while you track down code in a
separate module.

Naturally he doesn't come to a conclusion in the presentation materials.  The discussion would have been rather interesting.  Anyone attend RubyEast?

Last edited by bjhess (2007-10-11 12:22:15)

Re: DRY up working with arrays of models

Not that this community needs a reminder to check railscasts.com, but Ryan put up a 'cast about scope_out that I can't help but think was inspired by this thread.

Or perhaps not, I'm sure it was probably in the can long before this thread started.