Multiple Table Inheritance with ActiveRecord

Update 02/02/10: There are two new sections: Fixing method_missing and Handling attributes hash, which fix a few issues that popped up since first post.

If you aren’t interested in reading all the details you may want to make the long story short.

Imagine writing an online shop with different types of products. Normally all products would have common attributes such as title and price. Some attributes will likely differ. Tee may have size such as S, M, or L, while a Pen could have an ink_color. It’s easy to see that Tee is a Product, and so is Pen. We are looking at an is_a relationship. When I program this type of relationship I usually use inheritance.

  class Product < ActiveRecord::Base
  end
 
  class Tee < Product
  end
 
  class Pen < Product
  end

This inheritance looks reasonable, but now we have to come up with relational database structure. We need to find a way to store tee’s own attributes, pen’s own attributes, as well as their common (product’s) attributes without duplication. Some databases (PostgreSQL) provide support for table inheritance, but it’s a specialized feature which ties you down to the given db.

Single Table Inheritance

ActiveRecord provides only one way to handle a is_a relationship which is Single Table Inheritance. You’d have to create a table looking somewhat like the following.

  +----+------+-------+-----------------+------+-----------+
  | id | type | price | title           | size | ink_color |
  +----+------+-------+-----------------+------+-----------+
  | 1  | Tee  | 1000  | tie-dye t-shirt | M    |           |
  | 2  | Pen  | 500   | ball pen        |      | blue      |
  +----+------+-------+-----------------+------+-----------+

The problem here is that all attributes are stored in the same table. It’s likely that soon the number of attributes will grow unmanageable, and most of them will always stay NULL since they’ll be specific to only one type.

Polymorphic has_one Association

A has_one association allows us to split out tees, pens, and products into three different tables. In fact — as you’re about to see — this is the only way to get what we want. The problem is that it creates a has_a relationship, and we want is_a. Since there isn’t much choice, we can make it look like we have an is_a relationship, which I’m about to show.

Multiple Table Inheritance (Simulated)

I was speaking with the awesome @fowlduck over at #railsbridge IRC channel about ways to achieve something like MTI with Active Record. He pointed me to a pastie where he implemented an MTI-like behavior and called it a “hydra” pattern, which I subsequently cleaned up a bit.

So we want to have 3 tables in the database.

  • product_properties
  • tees
  • pens

  class ProductProperties < ActiveRecord::Base
    belongs_to :sellable, :polymorphic => true, :dependent => :destroy
  end
 
  class Tee < ActiveRecord::Base
    has_one :product_properties, :as => :sellable, :autosave => true
  end
 
  class Pen < ActiveRecord::Base
    has_one :product_properties, :as => :sellable, :autosave => true
  end

Immediately we can see duplicated code between Tee and Pen. This can be easily solved with a mixin.

  module Sellable
    def self.included(base)
      base.has_one :product_properties, :as => :sellable, :autosave => true
    end
  end
 
  class Tee < ActiveRecord::Base
    include Sellable
  end
 
  class Pen < ActiveRecord::Base
    include Sellable
  end

Now comes another issue. Every time we want to access price or title attributes (stored in product_properties) we have to call @tee.product_properties.price. This isn’t very convenient, especially considering that product_properties has to be built first in case it doesn’t exist. So let’s ensure it’s always built by updating the module.

  module Sellable
    def self.included(base)
      base.has_one :product_properties, :as => :sellable, :autosave => true
      base.alias_method_chain :product_properties, :autobuild
    end
   
    def product_properties_with_autobuild
      product_properties_without_autobuild || build_product_properties
    end
  end

Awesome, now product_properties is built automatically in case it doesn’t exist. We still have the method accessing issue though. For that I used method_missing.

  module Sellable
    def self.included(base)
      base.has_one :product_properties, :as => :sellable, :autosave => true
      base.alias_method_chain :product_properties, :autobuild
    end
   
    def product_properties_with_autobuild
      product_properties_without_autobuild || build_product_properties
    end
   
    def method_missing(meth, *args, &blk)
      if product_properties.public_methods.include?(meth.to_s)
        product_properties.send(meth, *args, &blk)
      else
        super
      end
    end
  end

Now if a method is missing from Tee or Pen instance it will be delegated to product_properties, which enables us to use @tee.price and @tee.title.

However, what about validations? Let’s say we want all products to always have a title, and we want to see an error appear on a Tee instance when ProductProperties#title is missing. Basically I want to completely remove product_properties from my sight as if it doesn’t exist, make it absolutely transparent. Let’s add the necessary validation in ProductProperties.

  class ProductProperties < ActiveRecord::Base
    belongs_to :sellable, :polymorphic => true, :dependent => :destroy
    validates_presence_of :title
  end

And now let’s make all Sellable models respect the validation as if it’s their own.

  module Sellable
    def self.included(base)
      base.has_one :product_properties, :as => :sellable, :autosave => true
      base.validate :product_properties_must_be_valid
      base.alias_method_chain :product_properties, :autobuild
    end

    def product_properties_with_autobuild
      product_properties_without_autobuild || build_product_properties
    end

    def method_missing(meth, *args, &blk)
      if product_properties.public_methods.include?(meth.to_s)
        product_properties.send(meth, *args, &blk)
      else
        super
      end
    end

  protected
    def product_properties_must_be_valid
      unless product_properties.valid?
        product_properties.errors.each do |attr, message|
          errors.add(attr, message)
        end
      end
    end
  end

Notice that I’m including an additional validator with the Sellable module. The validator collects all the errors on ProductProperties and adds them to parent class as if the errors are on a Tee or Pen itself.

As a nice finishing touch we can put this snippet into a Rails initializer.

  class ActiveRecord::Base
    def self.acts_as_product
      include Sellable
    end
  end
 
  # now we can say
 
  class Tee < ActiveRecord::Base
    acts_as_product
  end

Although that’s a matter of taste.

Fixing method_missing

There is a problem with method_missing. It checks the array of public_methods on product_properties to find out if delegation should occur. This check will fail in cases like @tee.title_changed?. That’s a magic method and therefore will not be part of static method array. Well, this is an easy fix.

  # Replace old method_missing with this one:
  def method_missing(meth, *args, &blk)
    product_properties.send(meth, *args, &blk)
  rescue NoMethodError
    super
  end

As you can see, even magic methods will work this way. Only if a NoMethodError is thrown we withdraw back into super.

Handling attributes hash

In the comments Austin brought up a case where initializing new models like Tee.new(:title => "foo") will throw an unknown attribute error. That’s expected since we rely on method_missing for accessing ProductProperties attributes. Instead we should define accessor methods explicitly in our individual products. Thankfully, it’s not too hard to accomplish with our Sellable mixin. First we need to add a submodule ClassMethods with a class method that uses class_eval to magically generate missing attributes.

  module ClassMethods
    def define_product_properties_accessors
      all_attributes = ProductProperties.content_columns.map(&:name)
      ignored_attributes = ["created_at", "updated_at", "sellable_type"]
      attributes_to_delegate = all_attributes - ignored_attributes
      attributes_to_delegate.each do |attrib|
        class_eval <<-RUBY
          def #{attrib}
            product_properties.#{attrib}
          end
         
          def #{attrib}=(value)
            self.product_properties.#{attrib} = value
          end
         
          def #{attrib}?
            self.product_properties.#{attrib}?
          end
        RUBY
      end
    end
  end

I’ll walk through this code quickly. First we’re extracting only the columns that we want to access. When we call content_columns in the first line of the method, it already excludes a bunch of special columns such as id and type. We then manually subtract more columns we’d like to ignore, such as timestamps, and polymorphic type.

Next we iterate over each remaining attribute and creating instance methods for it, such as title, title= and (for completeness) title?. Having these accessors defined explicitly is enough for ActiveRecord to see them when performing mass assignment, etc. We can now do something like Tee.new(:title => "foo") without any problems. The extra cases such as @tee.title_changed? are still handled by method_missing so we’re good.

One more thing left. We need to run this method on the base class into which we include Sellable. Just need to add a couple of lines to the self.included hook.

  def self.included(base)
    base.has_one :product_properties, :as => :sellable, :autosave => true
    base.validate :product_properties_must_be_valid
    base.alias_method_chain :product_properties, :autobuild
   
    # Add these two lines:
    base.extend ClassMethods
    base.define_product_properties_accessors
  end

And we’re all set.

All together now

Here’s the full picture of everything we just did.

  class ActiveRecord::Base
    def self.acts_as_product
      include Sellable
    end
  end

  class ProductProperties < ActiveRecord::Base
    belongs_to :sellable, :polymorphic => true, :dependent => :destroy
    validates_presence_of :title # for example
  end

  module Sellable
    def self.included(base)
      base.has_one :product_properties, :as => :sellable, :autosave => true
      base.validate :product_properties_must_be_valid
      base.alias_method_chain :product_properties, :autobuild
      base.extend ClassMethods
      base.define_product_properties_accessors
    end

    def product_properties_with_autobuild
      product_properties_without_autobuild || build_product_properties
    end

    def method_missing(meth, *args, &blk)
      product_properties.send(meth, *args, &blk)
    rescue NoMethodError
      super
    end

    module ClassMethods
      def define_product_properties_accessors
        all_attributes = ProductProperties.content_columns.map(&:name)
        ignored_attributes = ["created_at", "updated_at", "sellable_type"]
        attributes_to_delegate = all_attributes - ignored_attributes
        attributes_to_delegate.each do |attrib|
          class_eval <<-RUBY
            def #{attrib}
              product_properties.#{attrib}
            end

            def #{attrib}=(value)
              self.product_properties.#{attrib} = value
            end

            def #{attrib}?
              self.product_properties.#{attrib}?
            end
          RUBY
        end
      end
    end

  protected
    def product_properties_must_be_valid
      unless product_properties.valid?
        product_properties.errors.each do |attr, message|
          errors.add(attr, message)
        end
      end
    end
  end

  class Tee < ActiveRecord::Base
    acts_as_product
  end
 
  class Pen < ActiveRecord::Base
    acts_as_product
  end

This can be easily adapted for any other use case besides products in a store. In fact, with some meta magic or code generation this can easily be made into a plugin which I encourage you to try and send me the link when you’re done. :)

50 responses to “Multiple Table Inheritance with ActiveRecord”

  1. Tweets that mention Multiple Table Inheritance with ActiveRecord | Medium eXposure -- Topsy.com

    [...] This post was mentioned on Twitter by RailsIndia, Maxim. Maxim said: So I finally posted that article on Multiple Table Inheritance with ActiveRecord. http://bit.ly/mti-ar (cc @fowlduck) [...]

  2. Falk

    Nice solution – nice and clear code!

  3. Austin Schneider

    This helped me improve my plugin a lot. Thanks! Great stuff.

  4. Austin Schneider

    I noticed you can’t do this:

    Tee.new :title => ‘Foo’

    You’ll get:

    ActiveRecord::UnknownAttributeError: unknown attribute: title

    Anyway around this?

  5. Simon

    Awesome Post! One question do you think this approach could be made generic? One piece of includable code that could be use to do the same thing for something like Animal -> Mammal/Fish and Vehical -> Car/Boat…etc

  6. Philipp Kursawe

    Thats pretty neat! Just something I was looking for I guess. I have the following problem to solve. I have a Playlist that has_many PlaylistItems. Each PlaylistItem can be either a Track, Album or Video. Track, Album and Video all have different attribs and only share maybe title and length as common attribs. I wonder if that could be designed using the solution you proposed here?

  7. Aurélien

    Hi,

    This look exacly what I need ! But I am a bit confused about how the model is. there is a “product_property_id” field in Tee and Pen table ? the “id” field in Tee orPen is PK-FK ?

    Thanks.

  8. Joey

    Nice job! Thanks a lot.

    However, a small problem. Take your example. Let’s suppose Product has_and_belongs_to_many materials (not sure this is proper but I think you can get it). Then the current solution won’t find the materials attribute if we do Product.create(:materials = [...]). I don’t know what’s the term is for this kind of attribute…

    I made an ugly fix for this by doing:

    
    
      attributes_to_delegate = all_attributes - ignored_attributes + ["materials"]

    Is there any solution more elegant?

  9. Bryan L.

    Thanks a lot for sharing this code! I’ve been interested in MTI in Rails for quite some time.

    Is it possible to modify the code a little so that it supports something like:

    @productproperties = ProductProperties.all @productproperties.each do |p| print p.tee.color end

    This would be extremely useful. Thanks.

  10. Bryan L.

    Bummer, nevermind. Just after posting the previous comment, I figured you can just do “p.sellable.color” and it’ll work fine. How dumb of me.

  11. Bruno Pinto

    @hakunin

    First, thank you so much, this is the best post I have ever seen about MTI on rails.

    However, I had one problem when using :include.

    Example: class Pen < ActiveRecord::Base include Sellable end

    class Pack belongs_to :pen

    named_scope :with_product_properties, :include => { :pen => { product_properties => title } end

    So, if I try to use @pack = Pack.with_product_properties @pack.pen.title it says that title is an unknown property for this ‘aggregation however @pack.pen.product_properties.title is okay.

    If you need more info, I could create a code to reproduce this error.

    Thanks in advance!

  12. Ron

    I’m using a slightly different technique. I think it’s roughly similar, but it goes something like:

    class Product < ActiveRecord::Base self.abstract_class = true has_one :product_properties, :as => :product, :dependent => :destroy delegate :price, :price=, :name, :name=, :description, :description=, :to => :product_properties def product_properties_with_autobuild product_properties_without_autobuild || build_product_properties end alias_method_chain :product_properties, :autobuild end

    Then just inherit your products from Product.

    This mostly works in rails 3, but isn’t perfect. I’ve been waiting for a decent CTI implementation for activerecord for years. I have a project that relies on it very heavily. Anyone know of an ORM that supports it properly? Can DataMapper do it?

    1. Anthony Green

      I’ve been using the same pattern as Ron. I prefer using delegate over method_missing to avoid too much metamagic obviscation.

  13. Eric Coulthard

    Would you be able to add which files the code would go in and their path in the rails directory structure? I am trying to recreate what you have done for my project and I cannot since when I try to ‘rake db:seed’ it tells me that acts_as_product is undefined. If I replace the call with ‘include Sellable’ I get ‘undefined method sellable_type=…’

    I am new at this so any help you could give would be greatly appreciated.

    1. John York

      Eric, I just had the same problem, and it was because my table wasn’t defined properly. the product_properties table must have at least the following columns:

      id sellable_type sellable_id

  14. Evgeniy Dolzhenko

    Something that burned me a few times ago:

    public_methods.include?(meth.to_s)

    won’t work on 1.9 since method names would be returned as array of symbols instead of an array of strings

    public_methods.map(&:to_sym).include?(meth.to_sym)

    should work across different versions I think.

  15. Bryan L

    This has been extremely helpful to me. One issue I’ve got is that I can’t do eager loading with this. I get this error:

    Can not eagerly load the polymorphic association :sellable

    Would anyone know how to modify it so that we can do eager loading? I would like to reduce the N+1 queries. Thanks!

    1. Anonymous

      Actually, you would have to eager load the other table also: :include => {:pen => :product_properties}

      or something like this.

  16. Phillip

    Hi, this is really a nice approach!

    Did you thought about including dynamic finders ? If you just send them to the product_properties class you will get a product_properties object as return and not, as you may want, a pen or a tee object with all the necessary data. Maybe you have something in mind i haven’t thought of yet.

    thanks anyway for your sharing.

  17. method_missing override is not working | The Largest Forum Archive

    [...] I wrote a convenience ActiveRecord extension to delegate methods to a base object (based on multi-table inheritance) [...]

  18. Chris Ivens

    I’m getting confused around this area:

    def product_properties_with_autobuild product_properties_without_autobuild || build_product_properties end

    In my version which has different method names as my ‘product_properties’ class is called ‘node’ I keep getting ‘stack level too deep’ errors.

    It keeps calling node_without_autobuild > node > method_missing > node_without_autobuild… repeat until dead.

    Have I missed something key here? My full code is on forrst here: http://forr.st/~uss

    Any help will be appreciated.

  19. Drew

    Thanks for this entry– there is very little out there on MTI. I’ve adapted this code (actually using the method Ron posted) to try and subclass a generic ‘post’ model as an empty container for various type of user contributed content. There is one little code quirk that I can’t seem to fix. Currently a post record isn’t created when creating a child record (for example from the poll model) unless I specifically call @poll.post_with_autobuild, or @poll.build_post. Even just referring to @poll.post causes the records creation. For example, within the create method:

    This creates a poll and post record

    
    
    @poll = Poll.new(params[:poll]);
    logger.debug @poll.post;
    if @poll.save .......

    This only creates poll record

    
    
    @poll = Poll.new(params[:poll])
    if @poll.save .......

    Anyone know what is going on here?

  20. bob

    How about change ‘:dependent => :destroy’ from ProductProperties into module ‘Sellable’

    class ProductProperties < ActiveRecord::Base belongs_to :sellable, :polymorphic => true validates_presence_of :title end

    module Sellable def self.included(base) base.has_one :product_properties, :as => :sellable, :autosave => true, , :dependent => :destroy base.validate :product_properties_must_be_valid base.alias_method_chain :product_properties, :autobuild base.extend ClassMethods base.define_product_properties_accessors end

    So we can use Tee.destroy(1) or Pen.destroy_all and completely delete related ‘ProductProperties’.

    1. Ro

      I also want to do the same so I can delete pen and the product_properties by ‘Pen.destroy’. I tried adding ‘:dependent => :destroy’ from ProductProperties into module ‘Sellable’ but it gave me grief. Have you worked out a way to do this?

      1. bob

        Yes, it is working for me. Could you show me the error message?

  21. Bodaniel Jeanes

    In your scenario of a product listing, MTI provides no way to list all products or perform actions on products as a whole. This is one of the fundamental benefits of an “is_a” relationship. You’ve created more of a “behaves_as” relationship, if that makes sense. Still useful in some scenarios surely, but none that I can think of. How would you search products or list them? You’d need a different page/section/category to fetch each type individually. Seems to me like CTI would be a better fit, because it still preserves the inheritance and the primary models share a table.

  22. Paul Petrick

    I was so happy when I found this solution. Crystal clear.

    This weekend I upgraded to Rails 3 and everything went kaput. It seems alias_method_chain has been modified or removed in Rails 3. As a result, method_missing gets stuck in a loop – nasty. I’ve been mucking around with possible solutions, but can’t seem to find any that work

    Any suggestions?

  23. João

    Hi!

    I have error in rails 3… SystemStackError: stack level too deep …

    It’s because may be base.alias_method_chain don’t work in rails 3 see in.. http://efreedom.com/Question/1-3689736/Rails-Alias-Method-Chain-Still-Used

    But… I already try put super in this method and don’t work…

    def product_properties_with_autobuild product_properties_without_autobuild || build_product_properties end

    Any help?

    1. João

      Ok.. i think i was doing the things bad…

      With this approach, when we create a Tee we have to create at same time the product_properties like…:

      obj = Tee.new(:product_properties => (:title => “good tee” ) )

      ?

      Because only works for me like that….

  24. Ralph

    On Rails 3.0.3 I had a problem with the method_missing function going into an endless loop when calling tee.product_properties. After some debugging I found out this happens because method_missing is being called for the ‘id’ method, maybe because the new Tee hasn’t been saved yet?

    Only way I could get past this error was modifying the method missing as follows:

    def method_missing(meth, *args, &blk) raise NoMethodError if meth.to_s == ‘id’ ip_resource.send(meth, *args, &blk) rescue NoMethodError super end

    I don’t know if this can cause any other problems, but so far it works for me.

  25. vier's me2day

    영민군의 생각…

    multiple-table-inheritance-active-record/…

  26. Dennis C

    Hi.

    That not works for me :( I’m usgin ruby 1.9.2 and rails 3.0.3 and continous get the error: stack level too deep Any more idea?

    1. Dennis C

      HI.. NOw it’s working.. i made a mistake.

      The problem is destroy method-… only remove record from Tee and not work product_properties if you added a destroy dependent: base.has_one :product_property, :as => :sellable, :autosave => true, :dependent => :destroy you get: stack level too deep and repeat n times SELECT “product_properties”.* FROM “product_properties” WHERE (“product_properties”.sellable_id = 2 AND “product_properties”.sellable_type = ‘Tee’ LIMIT 1

      Dennis C.

      1. Dennis C

        Hi.. NOw all is working!

        class ProductProperties < ActiveRecord::Base belongs_to :sellable, :polymorphic => true validates_presence_of :title # for example end

        and the module:

        module Sellable def self.included(base) base.has_one :product_property, :as => :sellable, :autosave => true, :dependent => :destroy base.validate :product_property_must_be_valid base.alias_method_chain :product_property, :autobuild base.extend ClassMethods base.define_product_property_accessors end

        ….. ….. …

        def method_missing(meth, *args, &blk) raise NoMethodError if meth.to_s == ‘id’ product_property.send(meth, *args, &blk) rescue NoMethodError super end

        …….. …

        end

  27. Kevin

    It is still breaking when you have two levels of inheritance. For instance, I have a class CreditCardPayment < FinancialPayment < Payment

    When I create a new FinancialPayment, it can see the properties of Payment with no problem. CreditCardPayments can see the FinancialPayment properties, but not the properties of Payment. It seems to be breaking when we go two levels deep. Also, the validations fail for the same reason.

    Does anybody know an easy fix to this?

  28. Jim

    Thanks for the great write-up.

    One question. With this implementation, is it possible to find all products across type with one simple line. There are times I might want to see just tees, but other times I may need to iterate through all products and take an action based on product_type.

    Thanks, Jim

  29. Fake Multiple Table Inheritance | Space Babies

    [...] using the “is_a” pattern. There is no clean way to solve this in Rails. I discovered this post though, which outlines a good method to solve this [...]

  30. Gudata

    Very clean an readable code.

    Congratulations!

  31. Pierre

    Hey :)

    Thanks a lot for this article which makes perfect sense in all ways. i have a quick question tho. Let’s take the problem this way: if the base class is named “Character” instead of “Product”, and then in place of “Pen” and “Tie” we respectively replace them by “Archer” and “Swordsman”. A “Character” can be one, the other, both or none of them, and i’d like to be able to be able to get functional information on how you would do that.

    i have posted here regarding this matter: http://stackoverflow.com/questions/5346378/ror-sti-mti-mixin-confusion If you have any idea, i’d be extremely grateful :)

    Thanks a lot, Pierre.

  32. Josh Schramm

    A small addition I made – When using the ActionView form helpers it sometimes calls responds_to? before rending the form element. I added the following to help resolve an issue where respond_to? returned false for custom attributes.

    def respond_to?(method, include_private_methods = false) return true if self.content_item.respond_to?(method, include_private_methods) super end

  33. ryant

    Nice sharing. This is what I look for!

  34. Lasse

    Very nice post, it helps a lot.

  35. Exposure table | Switchtoseeds

    [...] Multiple Table Inheritance with ActiveRecord | Medium eXposureSingle Table Inheritance … The problem here is that all attributes are stored in the same table. It’s likely that soon the number of attributes will grow unmanageable, and most of them will always stay NULL since they’ll … Tweets that mention Multiple Table Inheritance with ActiveRecord | Medium eXposure — Topsy.com… [...]

  36. andre

    Hi,

    I’m getting:

    wrong number of arguments (0 for 1) (ArgumentError)
    org/jruby/RubyKernel.java:2052:in 'send'
    c:0:in 'destroy'

    Anyone has any idea of what I could be missing?

    I’m using -JRuby v 1.6.1 -rails 3.0.7

    This could really help me a lot..

  37. jvb

    Searching the web for some special rails solution I stumbled upon this article. If every post across the web would be of this quality people would populate the whole galaxy by 2020 :-) Awesome! Thanks!

  38. rtb

    You may want to add ‘:class_name => “::ProductProperites”‘ to your has_one definition to avoid name collisions.

    For instance, I was working with a module (Workflow) that had a class named Event, and I had an Activerecord model also named Event in the MTI module. When I included both modules, Activerecord kept looking to the MTI module for the Workflow Event class.

    Hope this helps anyone with the same problem.

Leave a Reply