Вы находитесь на странице: 1из 10

Single Table Inheritance in Rails [Alex Reisner]

h p://code.alexreisner.com/ar cles/singletableinheritanceinrails...

home code ar cles other

Single Table Inheritance in Rails

10 Dec 2009 Updated 14 Dec 2010 Because single table inheritance (STI) has been invaluable to me in some recent Rails projects, I thought Id try to dispel a li le of its nega ve reputa on by wri ng about some reasons you might use it, when to avoid it, and some ps for working with it. If you gave up on Railss STI a while ago because it required too many workarounds, Id recommend giving it another look, as some annoyances (eg, regarding valida on and demoduliza on) have recently been xed. For this ar cle Im assuming you know what STI is and how to set it up (add a type column and inherit from a parent class). Im also assuming that youre familiar with polymorphic associa ons and using modules to share code among dierent classes. Just because you know how to use these techniques doesnt mean deciding which one to use is easy.

When To Use It
Suppose you have three classes in your applica on which model similar things. To make this easier to think about Ill be referring to some hypothe cal classes by name: Car, Truck, and Motorcycle. Lets consider three choices for modeling this situa on: Polymorphic Associa ons (separate classes, mul ple tables) Single Table Inheritance (separate classes, one table) Single Class with condi onals (one class, one table) With Polymorphic Associa ons we use modules to share code among classes. A Single Class is not exactly a design pa ern, or anything par cularly interes ng. Im thinking of a model with a typelike a ribute (maybe called kind) and some if statements in methods where you need dierent behavior for dierent kinds of objects. OOP purists hate this, but it works well in the real world when there are only a few slight dierences between object types and separate classes are overkill. When deciding how to design your data models, here are some ques ons to ask yourself:

1. Are the objects, conceptually, children of a single parent?

First and foremost, your design choice needs to be understandable. In the case of a car, a truck, and a motorcycle, it seems perfectly reasonable to think of them all as vehicles. If you add a bicycle and a wheelbarrow it may become confusing because, in our minds, these things are not as vehiclelike as a car. This is to say: dont use single table inheritance just because your classes share some a ributes (eg: num_wheels, color, length), make sure there is actually an OO inheritance rela onship between each of them and an understandable parent class. And be sure to choose class names carefully.

2. Do you need to do database queries on all objects together?

If you want to list the objects together or run aggregate queries on all of the data, youll probably want
1 of 10 2/8/2011 2:39 PM

Single Table Inheritance in Rails [Alex Reisner]

h p://code.alexreisner.com/ar cles/singletableinheritanceinrails...

everything in the same database table for speed and simplicity, especially if there is a lot of data. This points to a Single Table Inheritance or a Single Class design. Remember that while SQL is op mized for doing joins and Ac veRecord provides tools to make them easier, data separa on may not be worth the increase in complexity for database opera ons. In the real world, 100% normalized data is not always the best design.

3. Do the objects have similar data but dierent behavior?

How many database columns are shared by every model? If there are going to be many modelspecic columns, you should consider Polymorphic Associa ons. On the other hand, if a Car, Truck, and Motorcycle all have the same a ributes, eg:
color engine_size price

but dierent method implementa ons, eg:

drivetrain_weight # sums dierent components value_after_depreciation(years) # uses dierent deprecia on rates drivers_license_certifications # references dierent cer ca ons

then Single Table Inheritance is probably a good design choice. If there are only minor dierences in a few methods, you may want to cheat and go with a Single Class.

Single Table Inheritance Tips

Here are some things that can make your STI experience more enjoyable.

Use a Single Controller

This may not always apply, but I have yet to see a case where STI works well with mul ple controllers. If we are using STI, our objects share a set of IDs and a ributes, and therefore should all be accessed in basically the same way (nd by some a ribute, sort by some a ribute, restrict to administrators, etc). If presenta on varies greatly we may want to render dierent modelspecic views from our controller. But if object access varies so much that it suggests separate controllers, then STI may not have been the correct design choice.

Put All Fixtures in the Parents File

If youre using Railss xtures for tes ng your app, remember that there is a onetoone rela onship between xture les and database tables. With STI, everything is in one table, so in our example wed have a single test/fixtures/vehicles.yml le with all our xtures in it (be sure to provide a type a ribute value for every record).

Allow Children To Use Their Parents Routes

Updated Dec 14, 2010: Now more robust, courtesy of Nathan McWilliams. If youve ever tried to add STI to an exis ng Rails applica on you probably know that many of your link_to and form_for methods throw errors when you add a parent class. This is because Ac onPack looks at the class of an object to determine its path and URL, and you havent mapped routes for your new subclasses. You can add the routes like so, though I dont recommend it:
# NOT recommended: map.resources :cars, 2 of 10 :as => :vehicles, :controller => :vehicles 2/8/2011 2:39 PM

Single Table Inheritance in Rails [Alex Reisner] h p://code.alexreisner.com/ar cles/singletableinheritanceinrails... map.resources :trucks, :as => :vehicles, :controller => :vehicles map.resources :motorcycles, :as => :vehicles, :controller => :vehicles

This only alleviates a par cular symptom. If we use form_for, our form elds will s ll not have the names we expect (eg: params[:car][:color] instead of params[:vehicle][:color]). Instead, we should a ack the root of the problem by implemen ng the model_name method in our parent class. I havent seen any documenta on for this technique, so this is very unocial, but it makes sense and it works perfectly for me in Rails 2.3 and 3:
def self.inherited(child) child.instance_eval do def model_name Vehicle.model_name end end super end

This probably looks confusing, so let me explain: When you call a URLgenera ng method (eg: link_to("car", car)), Ac onPack calls model_name on the class of the given object (here car). This returns a special type of string that determines what the object is called in URLs. All were doing here is overriding the model_name method for subclasses of Vehicle so Ac onPack will see Car, Truck, and Motorcycle subclasses as belonging to the parent class (Vehicle), and thus use the parent classs named routes (VehiclesController) wherever URLs are generated. This is all assuming youre using Rails resourcestyle (RESTful) URLs. (If youre not, please do.) To inves gate the model_name invoca on yourself, see the Rails source code for the ActionController::RecordIdentifier#model_name_from_record_or_class method. In Rails 2.3 the special string is an instance of ActiveSupport::ModelName, in Rails 3 its an ActiveModel::Name

Make The Parent Class Aware of Its Children

Lets say we want to provide a single form for crea ng a Vehicle object, so the rst eld is a select menu where the user chooses the object class (Car, Truck, or Motorcycle). How do we know what the op ons are without hardcoding the class names into the select eld? We can use Rubys inherited hook method to keep a running list of children as they inherit from the parent. It turns out that Ac veRecord actually does this already, so we could implement a Vehicle.select_options method like this:
def self.select_options subclasses.map{ |c| c.to_s }.sort end

Internally, Ac veRecord uses a class variable @@subclasses which is accessible only through the protected method subclasses. This is another trick I discovered while reading the Rails source code (unocial as far as I know), so if you dont like the feel of it we can reimplement the behavior directly in our parent class:
@child_classes = [] def self.inherited(child) @child_classes << child super # important! end def self.child_classes @child_classes end

In either case, we s ll have one more problem to solve: child classes are not recognized by the parent un l they are loaded. In typical Rails produc on and test environments this happens right away, but in development, where config.cache_classes == false, classes arent loaded un l you call upon them. So, for this to work consistently in our development environment we need to manually require classes:
3 of 10 2/8/2011 2:39 PM

Single Table Inheritance in Rails [Alex Reisner] h p://code.alexreisner.com/ar cles/singletableinheritanceinrails... # add to config/environments/development.rb: %w[vehicle car truck motorcycle].each do |c| require_dependency File.join("app","models","#{c}.rb") end

Its important to understand that classes are lazy loaded in the development environment so you avoid serious problems. Thats all for now. Good luck.
6 people liked this.

Showing 32 comments
Sort by Subscribe by email Subscribe by RSS

Thanks, this was very helpful!

i am relatively new to rails. i wanted to find out where exactly to put each piece of code. Does: def self.model_name name = "vehicle" name.instance_eval do def plural; pluralize; end def singular; singularize; end end return name end Go in the controller or model? And then for the two snippets you reference in the "Make the Parent Class Aware" section, do those go in the parent model?

@marc: The self.model_name method goes in the model (not the controller) and, yes, the stuff at the bottom goes in the parent model.
1 person liked this.

Brilliant! Was beginning to think STI was a lost cause. One question thoughdoes this method require all gems/plugins used by a model be required dependent in the development environment as well?

4 of 10

2/8/2011 2:39 PM

Single Table Inheritance in Rails [Alex Reisner]

h p://code.alexreisner.com/ar cles/singletableinheritanceinrails...

This has some issues with authlogic. If you add %w[vehicle car truck motorcycle].each do |c| require_dependency File.join("app","models","#{c}.rb") end on development.rb
1 person liked this.

Also with geokit's acts_as_mappable

Alex, thanks for the great article. I posted the following post on Stack Overflow yesterday and haven't really gotten any responses. Any insights or suggestions would be greatly appreciated: Original Post: I am working on a horse racing application and I'm trying to utilize STI to model a horse's connections. A horse's connections is comprised of his owner, trainer and jockey. Over time, connections can change for a variety of reasons: The horse is sold to another owner The owner switches trainers or jockey The horse is claimed by a new owner As it stands now, I have model this with the following tables: horses connections (join table) stakeholders (stakeholder has three sub classes: jockey, trainer & owner) Here are my clases and associations: class Horse < ActiveRecord::Base has_one :connection has_one :owner_stakeholder, :through => :connection has_one :jockey_stakeholder, :through => :connection has_one :trainer_stakeholder, :through => :connection end class Connection < ActiveRecord::Base belongs_to :horse belongs_to :owner_stakeholder belongs_to :jockey_stakeholder belongs_to :trainer_stakeholder end class Stakeholder < ActiveRecord::Base has_many :connections has_many :horses, :through => :connections end class Owner < Stakeholder # Owner specific code goes here. end

5 of 10

2/8/2011 2:39 PM

Single Table Inheritance in Rails [Alex Reisner]

class Jockey < Stakeholder # Jockey specific code goes here. end

h p://code.alexreisner.com/ar cles/singletableinheritanceinrails...

class Trainer < Stakeholder # Trainer specific code goes here. end One the database end, I have inserted a Type column in the connections table. Have I modeled this correctly. Is there a better/more elegant approach. Thanks in advance for you feedback.
1 person liked this.

"Allow Children To Use Their Parents Routes" is what I've been looking for a while! Rails is usually elegant... why do we have to resort to this!!!

There is still a problem when you mass assign from a form with the type you want, it will NOT be assigned and your class will be of the topclass. Here is the workaround (virtual attribute): def type_helper self.type end def type_helper=(type) self.type = type end Source: http://stackoverflow.com/quest...

great post, very helpful. Had been working on trying to fix this problem for a couple hours. Also the select box idea is great. Thanks alot!

Doesn't seem to work as my subclasses only seem to be saved as their parent class and any callbacks or extra validations I have on the subclasses don't get called.

Buddy Beers: I also try to make the select thing to work... but my classes never get loaded or only once and disappear... Fought it with: def self.select_options # fight lazy loading in development mode if Rails.env == 'development' DutyTax.new

6 of 10

2/8/2011 2:39 PM

Single Table Inheritance in Rails [Alex Reisner]

SalesTax.new end subclasses.map{ |c| c.to_s }.sort end

h p://code.alexreisner.com/ar cles/singletableinheritanceinrails...

Regarding "Make The Parent Class Aware of Its Children", This will cause issues when you have additional classes in your app that are relationships with one of these classes. For example, if your "Vehicle" class has a HABTM relationship with "Color", you will get an ActiveRecord::ConnectionNotEstablished method when the vehicle is loaded. You can't manually load Color either because it references Vehicle in the HABTM relationship and Vehicle is not loaded. Ideas?

does anyone know whether the differing behavior of class loading in development and test/production a bug or a feature? that was weird.

Under "Allow Children To Use Their Parents Routes", the custom String#human needs to be amended to: def human(*args); singularize; end in order to avoid problem with remarkable_activemodel, where String#human is called with hash argument. Hope this avoids unnecessary headaches for people who use remarkable.
2 people liked this.

Thanks for this outstanding article. I am always amazed at the helpful information people put out there while solving a problem. I sometimes solve problems, but am not able to get all the information together coherently afterwards. It just seems so obvious when I am done. I need to get an article together which solves a problem!

I'm still on the fence concerning STI in Rails but this is exactly the conversation I want to hear more of. Thanks for a great article.

self.model_name will also need i18n_key as of ActiveModel 3.0.3

Thanks! Updated the article.

7 of 10

2/8/2011 2:39 PM

Single Table Inheritance in Rails [Alex Reisner]

h p://code.alexreisner.com/ar cles/singletableinheritanceinrails...

Thanks a lot for keeping the article so current!

I ran into some problems when adding the following code to development.rb in rails 3.0.x %w[vehicle car truck motorcycle].each do |c| require_dependency File.join("app","models","#{c}.rb") end In my case it was because my vehicle class had a has_and_belongs_to_many relationship with another class. Anyway, the solution was to wrap the code in a config.to_prepare block as follows: config.to_prepare { %w[lesson group_lesson private_lesson preschool_lesson].each do |c| require_dependency File.join("app","models","#{c}.rb") end }

implementing the model name as described didn't work for me. There are actually many more methods than those too, take a look at ActiveModel::Name. The one that bit me was the "element" method, which is apparently used in serializing to json. So instead of changing model_name, I tried hooking in the inherited callback in the parent class like so: def self.inherited(child) child.instance_eval { def model_name; ParentClassName.model_name; end } super end replacing, "ParentClassName" with the actual name of the parent class. I'm still a bit new to Ruby so hopefully this isn't egregious. It seems to work for me without any problems.

1 person liked this.

Not egregious at all. In fact it's a much better solution than mine. Why make a cheap knockoff of an ActiveModel::Name when you can have the real thing? I've updated the article with your muchimproved code. Thanks!

Alex Thank you for keeping the article uptodate. I also really appreciate it! Michael Durrant.

8 of 10

2/8/2011 2:39 PM

Single Table Inheritance in Rails [Alex Reisner]

h p://code.alexreisner.com/ar cles/singletableinheritanceinrails...

You might also want to add 'Updated 12/15/2020' right at the top if you can. It's always hard to know how current these things are and old guides can be bad, but this is great and current (right now!) so might as well put it at the top :)

Good idea. Done. Thanks!

Hi Alex I added the self.model_name method to my STI parent class model definition, and I got the error undefined local variable or method `child' for #<Class:0x000001022332c0>. Added code to child model class, same result. Seems like child is not defined. Am I missing something? Thanks

You're not missing anything, I am. There was a typo in the first line of that example code, which is now fixed. See above, or replace 'model_name' with 'inherited(child)' and you should be good. Thanks for pointing this out.

Alex, it's working for me now, thank you!


The model_name thing seems to me to be still addressing the symptom not the underlying problem, and is likely to mess up a bunch of other things, as other comments here provide workarounds for. After all, you're kind of lying when a child claims it's model name is the parent name, it's not actually true. What's really needed is a patch to Rails so all those helpers recognize a child in single table inheritance, and just use the parent's model_name instead where appropriate (but not where not appropriate). Obviously much trickier to accomplish; probably quite doable in Rails3, although i'm not sure where to start; but I wouldn't even bother trying in Rails2.

9 of 10

2/8/2011 2:39 PM

Single Table Inheritance in Rails [Alex Reisner]

h p://code.alexreisner.com/ar cles/singletableinheritanceinrails...

This is great info, thanks for writing it.

Add New Comment

subscribe rss read code github say hi contact bro ls /etc | more

10 of 10

2/8/2011 2:39 PM