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:
- Setup a
HasDefault
base class that we could extend our models from (i.e.class CreditCard < HasDefault
) - 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.
Source: