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.