Professional Documents
Culture Documents
Single Table Inheritance in Rails (Alex Reisner)
Single Table Inheritance in Rails (Alex Reisner)
Single Table Inheritance in Rails (Alex Reisner)
h p://code.alexreisner.com/ar cles/singletableinheritanceinrails...
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:
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.
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 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
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
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
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.
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
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
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.
7 of 10
2/8/2011 2:39 PM
h p://code.alexreisner.com/ar cles/singletableinheritanceinrails...
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.
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
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 :)
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.
http://stackoverflow.com/quest...
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
h p://code.alexreisner.com/ar cles/singletableinheritanceinrails...
subscribe rss read code github say hi contact bro ls /etc | more
10 of 10
2/8/2011 2:39 PM