Extending ActiveModel via ActiveSupport::Concern

In a project I’ve been working on we have some functionality that is shared across multiple models – assigning a default record on a has_many association. Obviously we didn’t want to duplicate the methods in each model, so we explored our options for sharing the logic. We figure we could:

  1. Setup a HasDefault base class that we could extend our models from (i.e. class CreditCard < HasDefault)
  2. Extend the functionality of ActiveRecord::Base

Inspired by how has_secure_password is setup, we decided to extend the functionality of ActiveRecord::Base so we could just call a method inside of a model we wanted to expose the default record assignment methods and callbacks to. Here’s what we ended up with:

File: lib/active_model_extensions.rb

module HasDefault
  extend ActiveSupport::Concern

  module ClassMethods
    def has_default
      attr_accessible :is_default

      before_create  :set_default_if_none_exists, unless: :is_default?
      before_save    :set_default,                if: :is_default?
      before_destroy :set_fallback_default,       if: :is_default?
    end
  end

  def set_default
    current_default = self.class.first conditions: { user_id: self.user_id, is_default: true }
    current_default.update_column(:is_default, false) if current_default
    self.update_column(:is_default, true) unless self.new_record?
  end

  def set_fallback_default
    if self.is_default?
      fallback = self.class.first conditions: { user_id: self.user_id, is_default: false }
      fallback.update_column(:is_default, true) if fallback
    end
  end

  def set_default_if_none_exists
    current_default = self.class.first conditions: { user_id: self.user_id, is_default: true }
    self.is_default = true unless current_default
  end
end

class ActiveRecord::Base
  include HasDefault
end

This allows us to call has_default in any model to expose the three callback methods for assigning the default record. Now all we need to do to in our model is

class CreditCard < ActiveRecord::Base
    has_default
end

and the default record assignment will be taken care of automatically. This assumes there is an is_default column in the table, but has_secure_password makes similar assumptions so that doesn’t really bother me.

The Breakdown

The big thing here is to make sure we are using ActiveSupport::Concern to extend the core Rails classes. This makes our lives a litter easier by helping us setup our class and instance methods. It takes everything inside the ClassMethods module and makes them available as, you guessed it, class methods. All the other methods in the HasDefault module get turned into instance methods.

From the code above, you can see we setup the has_default class methods

module ClassMethods
  def has_default
    attr_accessible :is_default

    before_create  :set_default_if_none_exists, unless: :is_default?
    before_save    :set_default,                if: :is_default?
    before_destroy :set_fallback_default,       if: :is_default?
  end
end

and then setup the methods used in the callbacks in the HasDefault module.

def set_default
  current_default = self.class.first conditions: { user_id: self.user_id, is_default: true }
  current_default.update_column(:is_default, false) if current_default
  self.update_column(:is_default, true) unless self.new_record?
end

def set_fallback_default
  if self.is_default?
    fallback = self.class.first conditions: { user_id: self.user_id, is_default: false }
    fallback.update_column(:is_default, true) if fallback
  end
end

def set_default_if_none_exists
  current_default = self.class.first conditions: { user_id: self.user_id, is_default: true }
  self.is_default = true unless current_default
end

Note: ActiveSupport::Concern used to support a module called InstanceMethods, but recently changed this to make anything outside of the ClassMethods module an instance method.

The last part of this example includes the HasDefault module in ActiveRecord::Base, which actually adds the module and exposes the has_default method to all classes extending ActiveRecord::Base, in other words, all of your models.

class ActiveRecord::Base
  include HasDefault
end

Why ActiveSupport?

It’s true, there are other ways to extend classes in Ruby, but using ActiveSupport::Concern makes things a little easier and definitely cleaner. It can also handle dependency resolution, which we didn’t need here, but is quite useful.

Further Reading

  • Chris Gat

    Thanks for the tutorial. When I tried to run this code though, I got an undefined ‘has_default’ method error. Rails 3.2.8. Not sure why exactly. I was able to get it working by adding ‘require “active_model_extensions” ‘ in an config/intializer file (see this somewhat dated stackoverflow post http://stackoverflow.com/questions/2328984/rails-extending-activerecordbase). Any ideas why I had to add the require?

    • Yeah, the extension methods are not autoloaded, so the file needs to required explicitly. You can also configure your app to autoload everything in lib in config/application.rb by setting it as an autoload path:

      i.e. config.autoload_paths += %W(#{config.root}/lib)

      • Chris Gat

        that’s the head scratcher for me, as I do have the config.autoload_paths set up this way. Seems to autoload other modules in my lib folder as long as I include them in my classes. However, not in this case. I’m happy to have it working though.

        • The Tweeting Lion

          I think its becuase ActiveSupport::Concens has changed a little bit.

          This worked for me https://gist.github.com/cannapages/5359221

          The big difference is that your instance variables are defined in the concerns module name space and only class variables need their own inner module. I am sure there is a more technical way phrasing that ;)

        • The Tweeting Lion

          I think its becuase ActiveSupport::Concens has changed a little bit.

          This worked for me https://gist.github.com/cannapages/5359221

          The big difference is that your instance variables are defined in the concerns module name space and only class variables need their own inner module. I am sure there is a more technical way phrasing that ;)

  • Chris Gat

    Thanks for the tutorial. When I tried to run this code though, I got an undefined ‘has_default’ method error. Rails 3.2.8. Not sure why exactly. I was able to get it working by adding ‘require “active_model_extensions” ‘ in an config/intializer file (see this somewhat dated stackoverflow post http://stackoverflow.com/questions/2328984/rails-extending-activerecordbase). Any ideas why I had to add the require?

    • Yeah, the extension methods are not autoloaded, so the file needs to required explicitly. You can also configure your app to autoload everything in lib in config/application.rb by setting it as an autoload path:

      i.e. config.autoload_paths += %W(#{config.root}/lib)

  • Great, I also found few more ways to keep your code modular – http://amolnpujari.wordpress.com/2013/04/09/184/

  • I feel like there’s a third option. You could build the module and then include it in the necessary ActiveRecord models. You avoid inheritance (as you already have) and it doesn’t pollute models that don’t use `has_default`.

    • Yeah, that’s a good point. I should have clarified that. This certainly doesn’t need to be included in `ActiveRecord::Base`. In fact, I think we are including it on a per model basis now, as you mentioned.