How to extend Rails associations

How to extend Rails associations

You might have extended classes or instances in Rails, but do you know you can also extend Rails associations?

class Account < ActiveRecord::Base
    has_many :people, -> { extending FindOrCreateByNameExtension }
end

What exactly does extending an association do?

The proxy objects that control access to associations can be extended through anonymous modules. This is especially beneficial for adding new finders, creators, and other factory-type methods that are only used as part of an association.

Let’s look at the following code, Here we are adding a new finder method called find_or_create_by_name on this association with Person.

class Account < ActiveRecord::Base
    has_many :people do
        def find_or_create_by_name(name)
            first_name, last_name = name.split(" ", 2)
            find_or_create_by_first_name_and_last_name(first_name, last_name)
        end
    end
end

person = Account.first.people.find_or_create_by_name("David Heinemeier Hansson")
person.first_name # => "David"
person.last_name  # => "Heinemeier Hansson"

This method can only be used as part of this association. if you try to access this method through the Person object directly you will not get it. It is available only when you access it through the association.

If you have a business requirement that says you want to create a new Person by name when it’s for an existing Account, this provides a straightforward solution.

# Not available as a class method
irb(main):005:0> Person.methods.grep /find_or_create_by_name/
=> []

# Not accessible directly through an instance of that class
irb(main):010:0> Person.first.methods.grep /find_or_create_by_name/
=> []

# Accessible through association
irb(main):011:0> person = Account.first.people.methods.grep /find_or_create_by_name/
=> [:find_or_create_by_name]

If you need to share the same method between many associations, you can use a named extension module.

You can put the finder method inside a module, like this:

module FindOrCreateByNameExtension
    def find_or_create_by_name(name)
        first_name, last_name = name.split(" ", 2)
        find_or_create_by_first_name_and_last_name(first_name, last_name)
    end
end

On Rails 4.0 and above, you can use the following syntax to add your extension module:

class Account < ActiveRecord::Base
    has_many :people, -> { extending FindOrCreateByNameExtension }
end

class Company < ActiveRecord::Base
    has_many :people, -> { extending FindOrCreateByNameExtension }
end

On Rails 3.2 and lower versions, use the following syntax:

class Account < ActiveRecord::Base
    has_many :people, :extend => FindOrCreateByNameExtension
end

Applying conditions in an extension module

If you have some condition applied to an association, you can chain that condition in an extension module like this:

# before:
belongs_to :clinic, -> { clinic_id: @clinic_id }, primary_key: :person_id, foreign_key: :person_id

# After:
belongs_to :clinic,  -> { extending(MultiAssociation::OnTestId).where(clinic_id: @clinic_id) }, primary_key: :person_id, foreign_key: :person_id

Now you know how to use extension modules with associations.

Happy Learning!

Get the book