161

I've done some reading about how to extend ActiveRecord:Base class so my models would have some special methods. What is the easy way to extend it (step by step tutorial)?

BenMorel
  • 34,448
  • 50
  • 182
  • 322
xpepermint
  • 35,055
  • 30
  • 109
  • 163

9 Answers9

339

There are several approaches :

Using ActiveSupport::Concern (Preferred)

Read the ActiveSupport::Concern documentation for more details.

Create a file called active_record_extension.rb in the lib directory.

require 'active_support/concern'

module ActiveRecordExtension

  extend ActiveSupport::Concern

  # add your instance methods here
  def foo
     "foo"
  end

  # add your static(class) methods here
  class_methods do
    #E.g: Order.top_ten        
    def top_ten
      limit(10)
    end
  end
end

# include the extension 
ActiveRecord::Base.send(:include, ActiveRecordExtension)

Create a file in the config/initializers directory called extensions.rb and add the following line to the file:

require "active_record_extension"

Inheritance (Preferred)

Refer to Toby's answer.

Monkey patching (Should be avoided)

Create a file in the config/initializers directory called active_record_monkey_patch.rb.

class ActiveRecord::Base     
  #instance method, E.g: Order.new.foo       
  def foo
   "foo"
  end

  #class method, E.g: Order.top_ten        
  def self.top_ten
    limit(10)
  end
end

The famous quote about Regular expressions by Jamie Zawinski can be re-purposed to illustrate the problems associated with monkey-patching.

Some people, when confronted with a problem, think “I know, I'll use monkey patching.” Now they have two problems.

Monkey patching is easy and quick. But, the time and effort saved is always extracted back sometime in the future; with compound interest. These days I limit monkey patching to quickly prototype a solution in the rails console.

Harish Shetty
  • 64,083
  • 21
  • 152
  • 198
  • Hum... for second example when I run ./scripts/console I get an error "`include':TypeError: wrong argument type Class (expected Module)". – xpepermint Feb 24 '10 at 21:17
  • @xpepermint Sounds like you started it with `class MyActiveRecordExtensions` instead of `module MyActiveRecordExtensions`. – Jimmy Feb 24 '10 at 23:59
  • 3
    You have to `require` the file at the end of `environment.rb`. I have added this extra step to my answer. – Harish Shetty Feb 25 '10 at 08:32
  • Why is using Concerns seen as more idiomatic? I'm new to Rails but coming from other languages/frameworks, it seems like simple class inheritance would be the *right* way to go. – Hartley Brody Nov 06 '13 at 14:08
  • 1
    @HartleyBrody it is just a matter of preference. If you use inheritance, you have to introduce a new `ImprovedActiveRecord` and inherit from that, when you are using `module`, you are updating the definition of the class in question. I used to use inheritance(cause of years of Java/C++ experience). These days I mostly use modules. – Harish Shetty Nov 06 '13 at 15:23
  • 2
    It's a little ironic that your link was actually contextualizing and pointing out how people mis- and overuse the quote. But in all seriousness I'm having trouble understanding why "monkey patching" wouldn't be the best way in this case. If you want to add onto multiple classes then a module is obviously the way to go. But if your goal is to extend one class then isn't that why Ruby made extending classes in this way so easy? – MCB Nov 25 '13 at 16:38
  • 1
    @MCB, Every big project has few stories about a hard to locate bug introduced due to monkey patching. Here is an article by Avdi about the evils of patching: http://devblog.avdi.org/2008/02/23/why-monkeypatching-is-destroying-ruby/. Ruby 2.0 introduces a new feature called `Refinements` which addresses most of the issues with monkey patching(http://yehudakatz.com/2010/11/30/ruby-2-0-refinements-in-practice/). Sometimes a feature is there just to compel you to tempt fate. And sometimes you do. – Harish Shetty Nov 25 '13 at 16:54
  • @Dennis, yes it will. – Harish Shetty Mar 14 '14 at 20:27
  • Typically an abstract class is used when changing the db connection because of connection pooling issues if you don't. Has this been addressed allowing the Concern to truly be the preferred method? Or will those swapping the connection end up kicking themselves down the road when they discover the connection pooling problem? – Altonymous Mar 26 '14 at 16:31
  • Rails 4.1 has [Module#concerning](http://edgeguides.rubyonrails.org/4_1_release_notes.html#module-concerning), which can be used instead of defining a module inline, extending it with `ActiveSupport::Concern`, then mixing it in to a class. Maybe it's possible to use that here? – Dennis Apr 10 '14 at 17:22
  • How to use query control in the module? I want to write a `list` method it gets 10 rows from database in the module, then I don't need to copy it in every subclass. – worldask Sep 12 '14 at 06:28
  • @worldask, I have modified the example to add a method called `top_ten`. You can chain this method with other AR methods eg: `Order.where('qty > ?', 10).top_ten` – Harish Shetty Sep 12 '14 at 17:29
  • @HarishShetty How about the instance method is already defined? I want to exclude some fields from model by modifying **serializable_hash** method, would concern works in this case? – fuyi Mar 05 '15 at 09:47
  • @xiaopang if you want to exclude some fields from `serializable_hash` you should use the `exclude` parameter of the function. If you are trying to target just one class then it might be better to override the method in that class. Here is an example: https://robots.thoughtbot.com/better-serialization-less-as-json – Harish Shetty Mar 05 '15 at 17:24
  • @HarishShetty explained very clearly with examples. Here I would like to add one more way via gem called https://github.com/AndyObtiva/super_module. – Selvamani Mar 30 '15 at 05:28
  • You shouldn't use inheritance either, you should use a mix-in module. –  Apr 24 '15 at 16:32
  • I found this article to be useful to supplement this answer: http://www.fakingfantastic.com/2010/09/20/concerning-yourself-with-active-support-concern/. – Donato Apr 30 '15 at 21:50
  • @HarishShetty According to the doc you provided, it should be `class_methods do ` instead of `module ClassMethods` right? – Trantor Liu Jul 11 '16 at 01:49
  • 1
    @TrantorLiu Yes. I have updated the answer to reflect the latest documentation ( looks like class_methods was introduced in 2014 https://github.com/rails/rails/commit/b16c36e688970df2f96f793a759365b248b582ad) – Harish Shetty Jul 11 '16 at 21:36
  • To make first answer work I had to prepend `require 'active_support/concern'` to the `lib/active_record_extension.rb ` file. See [ActiveSupport Concern documentation](http://api.rubyonrails.org/classes/ActiveSupport/Concern.html) – a.barbieri Aug 29 '17 at 08:41
  • Tried to extend ActiveRecord::Result using the first method, but it did not work. – fgblomqvist Sep 26 '18 at 15:16
  • 1
    @HarishShetty no error, it just complained that my method did not exist (when I tried to use it). As in, it did not seem to load the extension. I used the exact code above, except I changed ActiveRecord::Base to ActiveRecord::Result at the last line. – fgblomqvist Sep 27 '18 at 04:21
  • This answer is 10 years old, is it still recommended? – fatfrog Jan 30 '21 at 18:03
  • At least as per the official rails doc: https://api.rubyonrails.org/classes/ActiveSupport/Concern.html – Harish Shetty Feb 02 '21 at 20:11
69

You can just extend the class and simply use inheritance.

class AbstractModel < ActiveRecord::Base  
  self.abstract_class = true
end

class Foo < AbstractModel
end

class Bar < AbstractModel
end
Toby Hede
  • 36,755
  • 28
  • 133
  • 162
  • I like this idea because is a standard way of doing it but... I get an error Table 'moboolo_development.abstract_models' doesn't exist: SHOW FIELDS FROM `abstract_models`. Where should I put it? – xpepermint Feb 25 '10 at 07:59
  • 23
    Add `self.abstract_class = true` to your `AbstractModel`. Rails will now recognize the model as an abstract model. – Harish Shetty Feb 25 '10 at 17:53
  • Wow! Didn't think this was possible. Tried it earlier and gave up when ActiveRecord choked looking for the `AbstractModel` in the database. Who knew a simple setter would help me DRY things up! (I was starting to cringe...it was bad). Thanks Toby and Harish! – dooleyo Jun 28 '13 at 04:04
  • In my case it is definitely the best way to do this: I'm not extending my model abilities with foreign methods here, but refactoring commmon methods for similar behaving objects of my app. **Inheritance** makes much more sense here. There is no *prefered way* but 2 solutions depending on what you want to achieve! – Augustin Riedinger Sep 26 '14 at 11:07
  • This does not work for me in Rails4. I created **abstract_model.rb** and put in in my models directory. inside the model it had the **self.abstract_class = true** Then i made on of my other models inherit... **User < AbstractModel**. In console I get: **User (call 'User.connection' to establish a connection)** – Joel Grannas Jan 21 '15 at 03:24
  • Inheritance decreases performance because you end up with class with many ancestors and it will increase time of method lookup. The best way to extend ActiveRecord is to use modules. – Lev Lukomsky Apr 20 '16 at 13:48
21

With Rails 4, the concept of using concerns to modularize and DRY up your models has been in highlights.

Concerns basically allow you to group similar code of a model or across multiple models in a single module and then use this module in the models. Here is a example:

Consider a Article model, a Event model and a Comment Model. A article or A event has many comments. A comment belongs to either article or event.

Traditionally, the models may look like this:

Comment Model:

class Comment < ActiveRecord::Base
  belongs_to :commentable, polymorphic: true
end

Article Model:

class Article < ActiveRecord::Base
  has_many :comments, as: :commentable 

  def find_first_comment
    comments.first(created_at DESC)
  end

  def self.least_commented
   #return the article with least number of comments
  end
end

Event Model

class Event < ActiveRecord::Base
  has_many :comments, as: :commentable 

  def find_first_comment
    comments.first(created_at DESC)
  end

  def self.least_commented
   #returns the event with least number of comments
  end
end

As we can notice, there is a significant piece of code common to both Event and Article Model. Using concerns we can extract this common code in a separate module Commentable.

For this create a commentable.rb file in app/model/concerns.

module Commentable
    extend ActiveSupport::Concern

    included do 
        has_many :comments, as: :commentable 
    end

    # for the given article/event returns the first comment
    def find_first_comment
        comments.first(created_at DESC)
    end

    module ClassMethods     
        def least_commented
           #returns the article/event which has the least number of comments
        end
    end 
end

And Now your models look like this :

Comment Model:

    class Comment < ActiveRecord::Base
      belongs_to :commentable, polymorphic: true
    end

Article Model:

class Article < ActiveRecord::Base
    include Commentable
end

Event Model

class Event < ActiveRecord::Base    
    include Commentable
end

One point I will like to highlight while using Concerns is that Concerns should be used for 'domain based' grouping rather than 'technical' grouping. For example, a domain grouping is like 'Commentable', 'Taggable' etc. A technical based grouping will be like 'FinderMethods', 'ValidationMethods'.

Here is a link to a post that I found very useful for understanding concerns in Models.

Hope the writeup helps :)

yesnik
  • 4,085
  • 2
  • 30
  • 25
Aaditi Jain
  • 6,969
  • 2
  • 24
  • 26
21

You can also use ActiveSupport::Concern and be more Rails core idiomatic like:

module MyExtension
  extend ActiveSupport::Concern

  def foo
  end

  module ClassMethods
    def bar
    end
  end
end

ActiveRecord::Base.send(:include, MyExtension)

[Edit] following the comment from @daniel

Then all your models will have the method foo included as an instance method and the methods in ClassMethods included as class methods. E.g. on a FooBar < ActiveRecord::Base you will have: FooBar.bar and FooBar#foo

http://api.rubyonrails.org/classes/ActiveSupport/Concern.html

nikola
  • 5,286
  • 3
  • 22
  • 19
8

Step 1

module FooExtension
  def foo
    puts "bar :)"
  end
end
ActiveRecord::Base.send :include, FooExtension

Step 2

# Require the above file in an initializer (in config/initializers)
require 'lib/foo_extension.rb'

Step 3

There is no step 3 :)
codenamev
  • 2,203
  • 18
  • 24
Vitaly Kushner
  • 9,247
  • 8
  • 33
  • 41
  • 1
    I guess step 2 must be placed into config/environment.rb. It's not working for me :(. Can you please write some more help? Thx. – xpepermint Feb 24 '10 at 20:22
5

Rails 5 provides a built-in mechanism for extending ActiveRecord::Base.

This is achieved by providing additional layer:

# app/models/application_record.rb
class ApplicationRecord < ActiveRecord::Base
  self.abstract_class = true
  # put your extensions here
end

and all models inherit from that one:

class Post < ApplicationRecord
end

See e.g. this blogpost.

Adobe
  • 12,967
  • 10
  • 85
  • 126
5

With Rails 5, all models are inherited from ApplicationRecord & it gives nice way to include or extend other extension libraries.

# app/models/concerns/special_methods.rb
module SpecialMethods
  extend ActiveSupport::Concern

  scope :this_month, -> { 
    where("date_trunc('month',created_at) = date_trunc('month',now())")
  }

  def foo
    # Code
  end
end

Suppose the special methods module needs to be available across all models, include it in application_record.rb file. If we wants to apply this for a particular set of models, then include it in the respective model classes.

# app/models/application_record.rb
class ApplicationRecord < ActiveRecord::Base
  self.abstract_class = true
  include SpecialMethods
end

# app/models/user.rb
class User < ApplicationRecord
  include SpecialMethods

  # Code
end

If you want to have the methods defined in the module as class methods, extend the module to ApplicationRecord.

# app/models/application_record.rb
class ApplicationRecord < ActiveRecord::Base
  self.abstract_class = true
  extend SpecialMethods
end

Hope it help others !

Ashik Salman
  • 1,819
  • 11
  • 15
4

Just to add to this topic, I spent a while working out how to test such extensions (I went down the ActiveSupport::Concern route.)

Here's how I set up a model for testing my extensions.

describe ModelExtensions do
  describe :some_method do
    it 'should return the value of foo' do
      ActiveRecord::Migration.create_table :test_models do |t|
        t.string :foo
      end

      test_model_class = Class.new(ActiveRecord::Base) do
        def self.name
          'TestModel'
        end

        attr_accessible :foo
      end

      model = test_model_class.new(:foo => 'bar')

      model.some_method.should == 'bar'
    end
  end
end
Will Tomlins
  • 1,436
  • 16
  • 12
0

I have

ActiveRecord::Base.extend Foo::Bar

in an initializer

For a module like below

module Foo
  module Bar
  end
end
Ed Richards
  • 111
  • 6