6097 comments
2357 subscribers
6253 on Twitter
Subscribe! Feed reader E-mail

Ruby on Rails: Extending ActiveRecord::Base to define your own ActiveRecord association methods

One of the things I really like about Rails is the ability to add to existing classes so that your code can be cleaner. For example, in the app we’re working on, I need to be able to display a list of offers associated with an organization. I also need to filter that list of offers by different criteria. If the user is not in tutorial mode, I need to filter out any tutorial-related offers. I want to show offers with different workflow states, too.

If I had to do this in straight SQL, I would need to write many queries to cover the different cases, or write my own query-building engine that takes conditions into account. In the Drupal world, I might try to build a View with lots of arguments, and then use a views_pre_execute hook to monkey around with the generated SQL.

In the Rails world, things are much simpler. I started off by chaining queries, because you can add conditions to the end of an ActiveRelation and go from there. That gave me code that looked like this:

base = Offer.includes(:donation).where("organization_id = ? AND (donations.deadline IS null OR donations.deadline >= ?) AND (NOT (offers.workflow_state IN (?, ?, ?)))", @organization.id, Time.now, Offer::DRAFT, Offer::ALLOCATED, Offer::CONFIRMED).order('offers.deadline')
@direct_offers = base.where("offers.workflow = ?", Donation::DIRECT)
@open_offers = base.where("offers.workflow = ?", Donation::OPEN)

Then I asked myself: How can I make this code even cleaner? I thought about adding instance methods. For example, in my Organization class, I could define the following:

class Organization
  # Other stuff goes here
  def current_offers
    self.offers.includes(:donation).where("(donations.deadline IS null OR donations.deadline >= ?) AND (NOT (offers.workflow_state IN (?, ?, ?)))", Time.now, Offer::DRAFT, Offer::ALLOCATED, Offer::CONFIRMED).order('offers.deadline')
  end
  def current_offers_by_workflow(workflow)
    self.current_offers.where("offers.workflow = ?", Donation::OPEN)
  end
end

That would allow me to replace the code above with something like this:

@direct_offers = @organization.current_offers_by_workflow(Donation::DIRECT)
@open_offers = @organization.current_offers_by_workflow(Donation::OPEN)

… so if I wanted to filter out tutorial entries, I could do that in def current_offers by adding a where clause for the tutorial column.

But it seemed clunky to have to specify all these instance methods in order to filter by different ways. What I really wanted was to be able to chain my custom filters together, so that I could write code like this:

@direct_offers = @organization.offers.filter(current_user).direct
@open_offers = @organization.offers.filter(current_user).open

and then eventually be able to do things like:

list = @organization.offers.filter(current_user).current.direct.allocated

(If I really wanted to.)

I couldn’t figure out where to add the methods so that they’d be defined in the right place. If I added the methods to the Organization class, they couldn’t be called on the ActiveRecord relations. A little bit of searching, and I figured out how to do it in Rails. It turns out that you can extend ActiveRecord relations with your own methods! Here’s how.

You’ll need to extend ActiveRecord::Base with your own methods. I put this in config/initializers/activerecord_extensions.rb.

module ProjectNameActiveRecordExtensions
  def filter(control)
    exclude_tutorial = true
    # Include the tutorial offers for users in tutorial mode
    if control.is_a? User and control.tutorial
      exclude_tutorial = false
    # You can also pass filter(false) to turn off these filters for testing
    elsif !control 
      exclude_tutorial = false
    end    
    if exclude_tutorial
      scoped.joins(:donation).where('donations.tutorial=?', false)
    else
      scoped
    end
  end
  # other methods go here...
end
ActiveRecord::Base.extend ProjectNameActiveRecordExtensions

The trickiest part was figuring out how to do a conditional filter, and that’s what scoped is for. I wanted to include the tutorials if the user was in tutorial mode, so my function should be a pass-through in that case. I couldn’t return self or nil, because that broke the associations. scoped turned out to be the magic keyword that refers to the current scope of the query.

What if you want to use the same words in different contexts? For example, “pending” might need to result in two different queries depending on whether you’re asking for pending offers or pending requests. ActiveRecord::Base is used for all classes, but you can use self to find out what class is being used for scoping. For example:

def pending
  if self == StandingRequest
    scoped.where("standing_requests.workflow_state=?", StandingRequest::PENDING)
  else
    # Replace with other cases as I find the need for them
    raise "Undefined behaviour"
  end
end

I love the fact that Rails lets you modify so much in order to make building sites easier. It’s like Emacs for the Web, and it makes my brain happy.

Short URL: http://sachachua.com/blog/p/22641
  • http://sachachua.com Sacha Chua

    So it turns out there’s an even better way to do it!
    http://api.rubyonrails.org/classes/ActiveRecord/NamedScope/ClassMethods.html

  • http://www.jorgenajera.com Jorge

    Thanks for this post! I got the same problem and your solution work like a charm. Regards.

  • http://sachachua.com Sacha Chua

    Glad it helped!

  • http://twitter.com/stucorbishley Stuart Corbishley (@stucorbishley)

    Thanks for the inspiration, I followed the link to the ClassMethods documentation and came up with this:

    module SimplifyPoints
    def simplify

    end
    end

    ActiveRecord::Relation.send(:include, SimplifyPoints)

    Thanks for the post!

On This Day...

  • 2012: Made my largest sketchnote ever! Painting the MaRS Lean Startup Day banner — This video doesn’t cover everything – there’s a gap in the middle when we started painting. I have to figure [...]
  • 2010: Emacs: Recording ledger entries with org-capture-templates — I use John Wiegley’s ledger program to keep track of my personal finances. It’s quick, it’s light, and it lets [...]
  • 2009: Trying out visual notetaking at a workshop — The more I draw, the easier and more fun it gets. I helped facilitate a client workshop last week. During one [...]
  • 2006: Crashing twice — If Microsoft Office crashes, it usually gives you back the auto-recovered document. Unless, of course, it crashes again while you’re trying to [...]
  • 2006: A slice of life: laundry — Some days the laundry piles up, and up, and up, and then I realize that my favorite malong is at the [...]
  • 2005: Reflections on the lab — ([[[[2005.11.23#2]]]] [[[[teaching#5]]]] [[[[TeachingReflections#23]]]]) I discussed the grading scheme for the Decision Support Systems class today. One of the good things about [...]
  • 2005: Cook or Die — The dearth of recent CookOrDie posts doesn’t mean I’ve figured out how to cook consistently well. Rather, it means that I [...]
  • 2004: Switching back to chronological notes — I guess most of my readers (Hi Mom!) check this site once a day, or something like that. They read from [...]
  • 2003: LedgerMode — I want to be able to use Emacs for my double-entry accounting so that I don’t have to start GnuCash. I [...]
  • 2003: Text messaging for the blind — http://news.bbc.co.uk/1/hi/technology/2403913.stm accessible computing, deals with text abbreviations
  • 2003: Whew! Just reviewed the history of UNIX — … and boy, are there stories to tell. =)
  • 2003: Story about pipes for CS161 — http://www.bell-labs.com/history/unix/sohedid.html Although stymied, McIlroy didn’t drop the idea. “And over a period from 1970 to 1972, I’d from time to time [...]
  • 2003: Story ideas for CS161 — - AT&T Bell Labs gave up on MULTICS - (Ken Thompson, Dennis Ritchie, and J.F. Ossanna) - really cool filesystem idea on [...]
  • 2003: History from Dennis Ritchie for CS161 — http://cm.bell-labs.com/cm/cs/who/dmr/hist.html What we wanted to preserve was not just a good environment in which to do programming, but a system around [...]
  • 2003: Recognizing coding systems in Emacs — For when Emacs doesn’t correctly autodetect it: C-x RET c CODING-SYSTEM RET M-x revert-buffer RET
  • 2003: Tidbit for CS161 — The different versions of the UN*X brand operating system are numbered in a logical sequence: 5, 6, 7, 2, 2.9, 3, [...]
  • 2003: Funny UNIX history — http://www.cs.bgu.ac.il/~omri/Humor/unix-history.html CS161
  • 2003: Jody Klymak’s planner-mode stuff — http://pender.coas.oregonstate.edu/PlannerMode.html E-Mail from Jody Klymak
  • 2003: The Object of Java — http://www.aw-bc.com/catalog/academic/product/0,4096,0321168542-TOC,00.html The outline looks like it makes sense as part of a syllabus.
  • 2003: Running word count in Emacs buffers — http://gnufans.net/~deego/emacspub/site-lisp-not/wcount.el

Get the highlights as a PDF!

Stories from my Twenties: Highlights from a Decade of Blogging

Free sample!