Fun With Rails Single Table Inheritance
This is a blog post about my experiences with Rails Single Table Inheritance and how the greatest of intentions produced the biggest of headaches.
One of my projects involves the collation and management of “Resources”. A Resource is a video, news article, podcast etc - Something that can be used to supplement learning.
Here’s the original
Resource model with some simple validations:
Combine the above with a regular CRUD (Create, Read, Update & Destroy) controller and you’ve got a system to view, edit and delete resources. Job done.
Now, fast forward a few months and imagine the following additional requirement: To supplement our offering of “free” resources, we’d like to introduce “member-only” resources that can only be accessed by users that are registered with the website.
How would you model the difference between and public and member-only resource? I wonder how many people jumped straight to adding a boolean to
Resource.rb to denote whether something is public? or private?
Yeah, I probably should have done that…
Keen to learn something new, I thought I’d dabble with a bit of Single Table Inheritance. My problem seemed to fit the solution perfectly - Two objects that are used in a subtly different way but are modelled identically at the database layer. That’s the golden use-case, right?
Say hello to our
PublicResource models. Both of these just inherit the attributes and validations from
Resource.rb - It doesn’t get much simpler than this!
As an aside, to leverage STI in Rails, you’ll need to add a
type column to the Resources DB table.
And if you’re using strong params, you’ll need to add the
:type symbol to your list of whitelisted attributes.
So now we have 2 new models,
PublicResource, both of which inherit all their attributes and validations from
Next, we need to wire things together at the controller level. Since the data-layer is identical, so too are the CRUD actions in the controller, right?
I’ve already got all the CRUD logic defined (and tested) in
Let’s declare 2 new controllers that simply extend
ResourcesController to inherit it’s functionality:
Finally, let’s update our routes file so that the new sub-controllers are exposed to the outside world and our old super-controller can just hide in the background.
Now that I’ve made my changes and described the motivation behind them, let’s take a look at the problems they introduced.
#####Problem 1: Which model should be created?
ResourcesController.rb, we have a hard-coded constant to build, update and destroy instances of
Resource.rb. For example, take a look at the #new action, below:
We need to change this so that a
PublicResource or a
MemberResource is instantiated/updated, as required.
This rather ugly hack solves the problem by instantiating the correct object based on the controller handling the request:
Problem 2: Which parameter should be whitelisted?
Similar to the problem outlined above, our Rails 4 Strong Parameters are hard-coded to require the
Instead, we need to intelligently permit the
:public_resource key, depending on the controller being hit.
We can re-use the
klass method from Problem 1, adding some ugly string manipulation to convert the klass to a symbol:
Problem 3: RSpec Missing Routes
Here’s what happens when you try to run the spec for the old
We’re seeing this error because I’ve removed the routes for
ResourcesController (remember that I commented out the line in
routes.rb ?). RSpec can no longer reach the CRUD actions defined on
ResourcesController and is therefore throwing an error.
Rather than re-introducing the routes across the entire app, let’s just inject them at run-time, for the purpose of the spec:
Problem 4: Missing Views & DoubleRender errors
Here comes another error:
RSpec is throwing a MissingTemplate error because, after processing the “create” action, it’s trying to render the
resources#create view, rather than
public_resources#create view or
member_resources#create view, as expected.
To fix this, let’s tell
resources_controller#create to render nothing and define appropriate redirects in the sub-controllers.
OK, we’re back to a passing test-suite! Let’s verify the app still works in the browser:
Argh! Now we’re getting an error because we’re rendering “nothing” in the super-controller and then redirecting in the sub-controller. That’s 2 renders for a single request - not allowed!
It seems that Single Table Inheritance is viewed, by many, to be more hassle than it’s worth. I think I agree. By mapping multiple models to a single database table, you’re asserting that the attributes and requirements of your models are identical today and will always be so in the future. That’s quite a gamble and it’s a lot of hassle if, further down the line, the models diverge and you need to split them out into their own tables.
If I’ve learnt one thing from this experience, however, it’s that STI at the database layer is one thing, but inheritance at the controller layer is something very different.
Trying to define shared CRUD behaviour in one controller, whilst seeming very DRY (Don’t Repeat Yourself), lead to horrible controller hacks, strange routing and bizarre tests.
Perhaps moving the CRUD logic to a shared “factory” or “service” model, used by all controllers is a better idea. At this point, though, it’s not worth the additional complexity and I think the following commit says it all: