ActiveRecord STI - How to Prevent the Base Class Being Saved.


Programming in Ruby tutorials and examples

ActiveRecord STI - How to Prevent the Base Class Being Saved.

MaxCDN Content Delivery Network

When using ActiveRecord and Single Table Inheritance sometimes it may not be desirable to have the base class ever persisted to the database, which by default is able to occur.

In the following contrived example we can see that we have an animal base class as well as dog and cat classes that inherit from animal.

class Animal < ActiveRecord::Base
  
end

class Dog < Animal
  
end


class Cat < Animal
  
end

Here is the migration that is used back these objects. Take note of the type column. The type column will be saved with the name of the class, in this case either Dog or Cat depending on which type of object is being persisted.

create_table :animals, :force => true do |t|
  t.string   :name
  t.string   :type
end
Animal.create(:name => "Fido")
=> # 

As the example above shows the Animal class is able to be saved to the database. This may not be particularly desirable (dependant on requirements) as you may want to lock down your model so only the inherited classes can be persisted (in this case Dog or Cat).

So how do we stop this?

Firstly make sure you application loads the following folder by adding app/modules to your autoload_paths in your application.rb

module ApplicationName
  class Application < Rails::Application
    config.autoload_paths += %W(#{config.root}/app/modules)
  end
end

Then add the following folder structure to your applications app folder.

--app
---modules
----active_record
-----extensions
-------abstract_class.rb

In the abstract_class.rb file add the following code. This is a module that extends ActiveSupport::Concern.

module ActiveRecord
  module Extensions
    module AbstractClass
      extend ActiveSupport::Concern
      
      included do
        before_save :abstract_class?
      end
  
      def abstract_class?
        raise "Do not try to save #{self.class} as it is an abstract class" if self.read_attribute(:type).nil?
      end
    end
  end
end

Using the module and including it into any base class (in this case Animal) will stop it from ever being able to be persisted to the database. It works because the base class is never saved with a type so we are able to check for this and then throw an error if necessary.

class Animal < ActiveRecord::Base
  include ActiveRecord::Extensions::AbstractClass
end
Animal.create(:name => "Fido")
RuntimeError: Do not try to save Animal as it is an abstract class

I really like this kind of model level constraint as it locks down your data API for other developers you share code with, without the need for too much documentation or explanation.