When Single-Table Inheritance Attacks
Rails provides a very quick and easy way to implement single-table inheritance (STI). The implementation of STI in Rails is so trivial as to make gotchas nearly unavoidable. I’ve spent a couple weeks ignoring, and later dealing with, one of these issues.
As described previously, I’ve been monkeying around with has_many_polymorphs (HMP). Finally I had HMP configured just right and I started seeing strange errors when hitting pages referencing the parents side of my HMP relationship:
LoadError (Expected script/../config/../app/models/ticker.rb to define Ticker)
Hmmm…let me see. Do I have a app/models/ticker.rb
file? Check. Does that file have a class Ticker
defined? Check. Am I free of typos? Check.
One more strange behavior is that, upon server startup, the first load of said page works beautifully. The above error only occurs on subsequent loads.
Oh, and another thing. If you cache your classes in development.rb
, your pages should load fine.
config.cache_classes = true
What the hell!
A model LoadError for every controller
Surfing around, I found the LoadError to happen in each controller action referencing either of my STI subclasses. This looked to be an STI problem, not a HMP problem. Time to expose my basic STI modeling:
# app/models/announcer.rb
class Announcer < ActiveRecord::Base
# Various useful methods defined
end
# app/models/ticker.rb
class Ticker < Announcer
# Various additional useful methods defined
end
# app/models/news_module.rb
class NewsModule < Announcer
# Various additional useful methods defined
end
Pretty straightforward, I think.
Resolving the STI LoadError problem
Through some Google searching, blog reading and experimentation, I found that I must do something special to make a controller aware of these STI subclasses. I don’t know if this can rightly be called a bug, or if it is more of a non-ideal situation in a framework that is frequently ideal, but I can say the solution suffers from near-non-existent documentation.
In all controllers that reference the STI subclasses, I had to do the following:
class TickerController < ApplicationController
# Rails does not pull out single-table inheritance subclasses properly on its own.
# Must require the STI superclass explicitly in controllers.
require_dependency 'announcer'
# More handy methods
end
This gets to be a little smelly, as I now have require_dependency
lines in several controllers. While the quantity of controllers is still small relative to my total number of controllers, I’m considering moving the require_dependency
line up to ApplicationController
so developers on this app don’t run into the same problem in the future.
The problem of limited STI visibility in controllers is described much more succinctly at the bottom of the Rails wiki STI page:
Gang, this’ll save you some hours of frustration. Your controllers may not see your STI subclasses unless you include the following in your controller files:
require_dependency 'model'
…where ‘model’ is the name of the parent class.