Extend Your ActiveRecord Association Methods

Posted by ryan
at 3:32 PM on Sunday, December 03, 2006



Much of the beauty of ActiveRecord associations is in the collection methods that are provided for you when you define has_many relationships.

For instance, when we say:

class Organization < ActiveRecord::Base
  has_many :people
end

We now have an organization.people method that returns the collection of associated people within the organization. Easy enough. We also get nice little methods on the collection of people like organization.people<<, organization.people.build, organization.people.create and organization.people.find (among others). Well what if you wanted to define your own method on this auto-magically provided collection method? You do this through Association Extensions which let you define methods to add to the collection. You can define an association extension either with a block or a module – we’ll use a block provided to the has_many call here as it’s the most common way to do so:


class Organization < ActiveRecord::Base
  has_many :people do
    def find_active
      find(:all, :conditions => ["active = ?", true])
    end
  end
end

I’ve defined a find_active method which will retrieve all people in the organization that have the active column set to true. This can be invoked very intuitively with:

organization.people.find_active

This is a great way to provide convenience finder methods for common retrievals on the associated collection.

If you want to define an extension as a module just provide the :extend option in your has_many definition:

module FindActiveExtension
  def find_active
    find(:all, :conditions => ["active = ?", true])
  end
end

class Organization < ActiveRecord::Base
  has_many :people, :extend => FindActiveExtension
end

You can customize to your heart’s content – these are just some simplistic examples of how to plug into this nifty feature. I just recently stumbled upon it and thought it might be worth spreading the word since I found myself smitten by it.

Resources

tags: , ,

Comments

Leave a response

  1. dubnik@tvns.co.yuDecember 03, 2006 @ 10:14 PM
    You can also use one of features in the Rails 1.2 RC1 Added simple hash conditions to #find that will just convert a hash to an equality/AND-based condition string. Example: Person.find(:all, :conditions => { :last_name => "Catlin", :status => 1 }) ...is the same as: Person.find(:all, :conditions => [ "last_name = ? and status = ?", "Catlin", 1 ]) This makes it easier to pass in the options from a form or otherwise outside. You can also use a hash conditions feature to #find, added in the Rails 1.2 RC1. It converts a hash to an equality/AND-based condition string. So, instead of def find_active find(:all, :conditions => ["active = ?", true]) end a more ellegant approach would be: def find_active find(:all, :conditions => {:active=>true}) end
  2. dubnik@tvns.co.yuDecember 03, 2006 @ 10:15 PM
    You can also use a hash conditions feature to #find, added in the Rails 1.2 RC1. It converts a hash to an equality/AND-based condition string. So, instead of def find_active find(:all, :conditions => ["active = ?", true]) end a more ellegant approach would be: def find_active find(:all, :conditions => {:active=>true}) end
  3. Michael SchuerigDecember 04, 2006 @ 12:17 AM
    I'd turn this upside down and instead of adding a method, find_active, on associations, I'd add it as a method on the singleton class of Person: class Person < ActiveRecord::Base class << self def find_all_active find(:all, :conditions => ["active = ?", true]) end end end Or, using your FindActiveExtension class Person < ActiveRecord::Base extend FindActiveExtension end
  4. http://www.vdomck.org/blogDecember 04, 2006 @ 03:01 AM
    Perfect timing. This popped up in my RSS feed just after writing my own clunky code to do the same. I have since stripped it out and am very happy. Keep blogging!
  5. Ryan DaigleDecember 04, 2006 @ 03:35 AM
    Michael: Adding a singleton method to the Person class doesn't quite give us what we need. Using @organization.people.find_active@ gives us all the active people _that belong to that organization_. So, to do it your way you'd have to say:
    
    class Person < ActiveRecord::Base
      class << self
        def find_all_active(organization)
          find(:all, :conditions => ["active = ? and organization_id = ?", true, organization.id])
        end
      end
    end
    
    See the difference? Using the association extension lets us not have to re-specify the organization context, which we already have with the instance of @organization@.
  6. Ryan DaigleDecember 04, 2006 @ 03:38 AM
    dubnik: You're right about using a parameter hash to specify SQL parameters - I previously "wrote about it here":/articles/2006/06/06/whats-new-in-edge-rails-convenient-finder-parameter-hashes . I didn't use it, however, because association extensions aren't v1.2 specific and the SQL parameter hash is. Still, thanks for pointing that out. We should start using that much cleaner syntax.
  7. Michael SchuerigDecember 04, 2006 @ 04:56 AM
    Ryan: try it for yourself and be prepared for a surprise. I don't think it's documented anywhere and I only looked into it after I noticed that "couldn't work" in a blog post (by DHH?). Then I looked into the AR implementation of associations and found that appropriate singleton class methods are run in a with_scope block. Very clever. And obviously not at all obvious.
  8. Ryan DaigleDecember 04, 2006 @ 05:00 AM
    Wow - you're right Michael - that is a surprise. Great, undocumented (?) find!
  9. Jason L.December 04, 2006 @ 06:54 AM
    Michael - is this:
    
    class Person < ActiveRecord::Base
      class << self
        def find_all_active(organization)
          find(:all, :conditions => ["active = ? and organization_id = ?", true, organization.id])
        end
      end
    end
    
    any different from:
    
    class Person < ActiveRecord::Base
      def Person.find_all_active(organization)
          find(:all, :conditions => ["active = ? and organization_id = ?", true, organization.id])
      end
    end
    
    Just curious - the second way is usually how I do it (and it makes it clearer to me that the 'find_all_active' method is a class method), but I'm wondering if it makes a difference.
  10. Michael SchuerigDecember 04, 2006 @ 10:25 AM
    Jason: The two ways are interchangeable. Whether you write class Person class << self def something end end end or class Person def Person.something end end the result is the same. But as a matter of style, if you look at the Rails source code, you find that the first way is preferred. For a general explanation see http://www.rubycentral.com/book/classes.html I'm not sure if you're aware of it, but the call to find should look like this find(:all, :conditions => ["active = ?, true]) Yes, the "organization_id = ?" clause is unnecessary. But, and that's the beauty of it, when find_all_active is used on an association collection (has_many, has_and_belongs_to_many), the find will be scoped with the appropriate conditions so that only associated objects are considered.
  11. Jason L.December 04, 2006 @ 11:18 AM
    I see now - the cleanliness in the first example becomes more apparent when you group multiple class methods inside the class << self segment. (Oops - yeah, I was aware of the other part - I when I posted I meant to copy the code out of your comment, Michael, but accidentally copied Ryan's.)
  12. AnonymouseDecember 17, 2006 @ 05:51 AM
    The two notations ("def Person.something" and the << syntax) look similar to a novice. But an expert knows one style makes it easier to rename the class or cut/paste to another project.
  13. Adam T.December 28, 2006 @ 08:57 AM
    I'm going to put in my vote *against* adding singleton methods in the associated model in favor of putting them inside the has_many block for this reason: Caching. I learned the hard way (the really hard way) that this code will not work like you want it to: class Activity < ActiveRecord::Base has_many :actions end class Action < ActiveRecord::Base belongs_to :activity def self.first_inbound return @first_inbound ||= find( :first, :conditions => "type = 'inbound'", :order => "action_date asc" ) end end Even though you can still reference that method as activity.actions.first_inbound, the first time you run it, the instance variable gets set *regardless of controlling object*. What this means is that if you're iterating through a collection of Activity objects, every time you call the first_inbound method after the first, you're going to get the same data. This is very, very bad. Encapsulating your association extensions inside the code block of the association call keeps the instance variables separately scoped through calls to the collection. That way, activities[0].actions.first_inbound will give you what you expect, as will activities[1].actions.first_inbound. While it might not look as good, the behavior is what you will ultimately want.