dipth / Heritage

A gem for using Multiple Table Inheritance with rails 3
50 stars 18 forks source link

h1. Heritage

Heritage is a gem that implements Multiple Table Inheritance for ActiveRecord models.

h2. Compatability

Heritage has only been tested with Rails 3

h2. Installation

Simply add Heritage to your Gemfile and bundle it up:

  gem 'heritage'

h2. Usage

Heritage works by assigning one model as your @predecessor@, and one or more other models as it's @heir@. The predecessor is the parent of it's heirs, and thereby implicitly gives it's heirs access to it's columns, and optionally exposing methods to them.

To mark a model as predecessor, simply use the @acts_as_predecessor@ class-method:

  class Post < ActiveRecord::Base
    acts_as_predecessor
  end

To mark a model as heir, simply use the @acts_as_heir_of@ class-method, passing a symbol to the model that is to be the heirs predecessor.

  class BlogPost < ActiveRecord::Base
    acts_as_heir_of :post
  end

This takes care of the model configuration. We however need to add two extra columns to the Posts table. We need a @heir_id@ column of type @integer@ and a @heir_type@ column of type @string@.

  class CreatePosts < ActiveRecord::Migration
    def self.up
      create_table :posts do |t|
        t.integer :heir_id
        t.string :heir_type
        t.string :title
        t.timestamps
      end
    end

    def self.down
      drop_table :posts
    end
  end

  class CreateBlogPosts < ActiveRecord::Migration
    def self.up
      create_table :blog_posts do |t|
        t.text :body
      end
    end

    def self.down
      drop_table :blog_posts
    end
  end

When this is done and the database is migrated, we can begin using the models.

h2. Creating new instances

Now we can simply call the following to create a new @BlogPost@

  blog_post = BlogPost.create(:title => "Wow", :body => "That's a nice blog post!")

Notice that the @title@ attribute belongs to the @Post@ model, and the @body@ attribute belongs to the @BlogPost@ model.

h2. Attributes

We can directly access the @title@ attribute through @BlogPost@ and even change it's value

  blog_post.title # "Wow"
  blog_post.title = "Oh boy!"
  blog_post.save!
  blog_post.title # "Oh boy!"

We can also update attributes like normal through @update_attributes@

  blog_post.update_attributes(:title => "Hubba Hubba", :body => "Nice blog post!")
  blog_post.title # "Hubba Hubba"
  blog_post.body # "Nice blog post!"

h2. Methods

If we want to expose some methods from our predecessor model to it's heirs, we can do so when calling the @acts_as_predecessor@ class-method

  class Post < ActiveRecord::Base

    acts_as_predecessor :exposes => :hello

    def hello
      "Hi there!"
    end

  end

Now all heirs of @Post@ will have a hello-method, which we can call directly on the heir-model:

  blog_post = BlogPost.create(:title => "I am full", :body => "of methods...")
  blog_post.hello # "Hi there!"

If you for some reason need to override the method in one of your heir-models, you can simply implement the method, and it will override the method from the predecessor.

  class BlogPost < ActiveRecord::Base

    acts_as_heir_of :post

    def hello
      "Yo!"
    end

  end

Calling the @hello@ method on BlogPost will now yield another result:

  blog_post = BlogPost.create(:title => "I have", :body => "my own methods...")
  blog_post.hello # "Yo!"

If we need to combine the local method in the heir, with the method in the predecessor, we can do so through the @predecessor@ method of the heir model, kinda like you would use @super@.

  class BlogPost < ActiveRecord::Base

    acts_as_heir_of :post

    def hello
      "Yo! #{predecessor.hello}"
    end

  end

The result would now be a combination of the local method in the heir, and the method in the predecessor:

  blog_post = BlogPost.create(:title => "I have", :body => "my own methods...")
  blog_post.hello # "Yo! Hi there!"

h2. Listing and filtering

To list all your wonderful heir models you do as you normally would in ActiveRecord, with one single exception.

Normally you would call something like this, to show all @BlogPosts@

  @posts = BlogPost.all

This however will result in 1 + the number of returned records SQL calls, which is hardly good. Instead you need to tell ActiveRecord that it should include the predecessors of the heirs, like so:

  @posts = BlogPost.all(:include => :predecessor)

We now only call the database twice; Once for loading the heirs, and once for loading all referenced predecessors.

Another gotcha is when you need to filter the heirs. You can't directly filter by attributes from the predecessor model. So in our example where we have the @title@ attribute in the @Post@ model, we can't do the following:

  @posts = BLogPost.where("title = 'test'")

Instead we need to join the predecessor attributes by its association, like so:

  @posts = BlogPost.joins(:predecessor).where("posts.title = 'test'")

Behind the scenes, heritage works just like a simple ActiveRecord association, so it makes sense.

h2. Timestamps

If all of your heir-models needs timestamps, then you can simply add timestamps to the predecessor model, and omit them from the heir-models. Heritage will make sure, that whenever you update your heir-model, the @updated_at@ timestamp in the predecessor model will be updated.

h2. A note on destruction

Heritage depends on the destroy-method of the models, and as such you should always delete predecessor and heir models by calling the @destroy@ method on either, and NEVER by calling the @delete@ or @delete_all@ methods. If you absolutely need to do a direct delete in the database, then you need to manually remove the counterpart as well.

For instance, if you manually delete a @BlogPost@ that is heir of @Post@, then you need to first find the right @Post@, then delete the heir and finally delete the predecessor.

h2. Advanced usage

It is always possible to traverse between a predecessor and it's associated heir, through the @predecessor@ method of an heir, and the @heir@ method of a predecessor.

h2. Questions, Feedback

Feel free to message me on Github (murui)

h2. Contributing to Heritage

Fork, fix, then send me a pull request.

h2. Credits

Credits goes out to Gerry from TechSpry.com for the idea for this implementation: http://techspry.com/ruby_and_rails/multiple-table-inheritance-in-rails-3/

h2. License

Creative Commons License
Heritage by Thomas Dippel @ Benjamin Media A/S is licensed under a Creative Commons Attribution-ShareAlike 3.0 Unported License.
Based on a work at techspry.com