CTI
CTI is a gem that implements Multiple Table Inheritance for ActiveRecord models.
Compatability
CTI has only been tested with Rails 4
Installation
Simply add CTI to your Gemfile and bundle it up:
gem 'cti'
Usage
CTI 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.
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.
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!"
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!"
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, CTI works just like a simple ActiveRecord association, so it makes sense.
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.
CTI will make sure, that whenever you update your heir-model, the updated_at
timestamp in the predecessor model will be updated.
A note on destruction
CTI 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.
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.
Credits
Credits goes out to Thomas Dippel @ Benjamin Media A/S for the predecessor Heritage and Gerry from TechSpry.com for the idea for this implementation:
http://techspry.com/ruby_and_rails/multiple-table-inheritance-in-rails-3/
Contributing to cti
- Check out the latest master to make sure the feature hasn’t been implemented or the bug hasn’t been fixed yet.
- Check out the issue tracker to make sure someone already hasn’t requested it and/or contributed it.
- Fork the project.
- Start a feature/bugfix branch.
- Commit and push until you are happy with your contribution.
- Make sure to add tests for it. This is important so I don’t break it in a future version unintentionally.
- Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
License
CTI by Seyed Razavi @ Education Apps is licensed under a Creative Commons Attribution-ShareAlike 3.0 Unported License.
Based on a work at Heritage