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:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class User < ActiveRecord::Base | |
attr_accessible :name | |
has_many :accounts | |
end | |
class Account < ActiveRecord::Base | |
attr_accessible :kind, :user_id | |
belongs_to :user | |
end |
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:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
has_many :accounts do | |
def recent | |
where('accounts.created_at > ?', 5.days.ago) | |
end | |
end |
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:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
has_many :accounts do | |
def <<(account) | |
account.kind = "UserAccount" | |
proxy_association.owner.accounts += [account] | |
end | |
end |
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:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
Nice, clean and handy.
I’m wondering how to test this extension, can you add some exemple too please?
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.
I wasn’t aware of association callbacks… looks like a great tool for the job.
Thanks for the comment 🙂
It seems like you could use a condition on the association instead of overriding #<<.
Very interesting thing! Thanks for this tip )