ActiveRecord Association Extensions

Today I want to address what I consider to be one of the most misused features of ActiveRecord.
It is called ActiveRecord Association Extensions.

How to Define An Association Extension

Association Extensions lets you easily add functionality or alter exisiting functionality when dealing with ActiveRecord association. I will refer the following simple code in the post:


class User < ActiveRecord::Base
attr_accessible :name
has_many :accounts
end
class Account < ActiveRecord::Base
attr_accessible :kind, :user_id
belongs_to :user
end

view raw

gistfile1.rb

hosted with ❤ by GitHub

What we have here is a User model which has_many Account models. Dead simple.

In order to extend the accounts association all we have to do is add a block to it as in:


has_many :accounts do
def recent
where('accounts.created_at > ?', 5.days.ago)
end
end

view raw

gistfile1.rb

hosted with ❤ by GitHub

Which lets us access @user.accounts.recent as expected.

Use Scopes When It’s Right

The first (and obvious) thought about the latest code snippet is “Why would you wanna do that? Why not just use scope :recent on Account model?!”.
To that I’d answer You’re damn right! This logic should definitely belong to the Account model and be used as a scope so that other references to Account will be able to use it as well.

A Good Case For Association Extensions

This was an easy one and probably not the reason you’re reading this – I want to introduce to you with another case which yells association extenstion a bit louder: Let’s say I want that every account which is added to a user accounts collection will have the kind “UserAccount” as a property.

I will note here that every has_many association includes the << method which may be used as @user.accounts << Account.new

One way to achieve my goal would be to verify that every time I add an account to a user accounts collection I remember to set its kind property to "UserAccount". Counting on your memory skills (or even worse – on other programmers memory skills) is a bad habbit, believe me 🙂

A better way would be to define a method named add_account(account) in the User model which will set account.kind = "UserAccount" before adding it to the accounts collection.

But why should we invent the wheel? ActiveRecord has already taken care of this, presenting you the ultimate solution:


has_many :accounts do
def <<(account)
account.kind = "UserAccount"
proxy_association.owner.accounts += [account]
end
end

view raw

gistfile1.rb

hosted with ❤ by GitHub

We've practically overriden the << method of ActiveRecord association, and by doing this we have the following benefits:

1. We're using ActiveRecord's conventions. Rails is known for its "Convention Over Configuration" attitude and although it may seem negligible this is a huge benefit on its own.

2. We don't have to remember to set anything manually, the setting of the kind proprety will be transparent to us in future uses.

3. Isn't that just beautiful?

One thing to note here that the self reference inside the extension method is NOT the user instance, in order to get that we used proxy_association.owner

Resusable Extensions

Another great thing about association extensions is they are reusable. In order to make an extension reusable all you have to do is to extract the methods into a module and :extend that module in the association statement as follows:


module AccountOfUser
def <<(account)
account.kind = "UserAccount"
proxy_association.owner.accounts += [account]
end
end
class User < ActiveRecord::Base
attr_accessible :name
has_many :accounts, :extend => AccountOfUser
end

view raw

gistfile1.rb

hosted with ❤ by GitHub

Nice, clean and handy.

6 thoughts on “ActiveRecord Association Extensions

  1. For this example, I’d use an Association Callback (http://guides.rubyonrails.org/association_basics.html#association-callbacks) `after_add`. The Rails Guide examples for extensions are also poorly designed.

    The place that I would consider using an extension is when you want something like a scope but that needs to work on collection members _before_ they have been stored. For example, instead of `-> { where(active: true) }` (a scope on the child class which would filter stored objects), have an extension `select { |o| o.active? }` which would filter all items in the collection. The latter is going to be slow though if the collection is very large.

    1. I wasn’t aware of association callbacks… looks like a great tool for the job.

      Thanks for the comment 🙂

Leave a comment