Best practices to handle routes for STI subclasses in rails
My Rails views and controllers are littered with redirect_to
, link_to
, and form_for
method calls. Sometimes link_to
and redirect_to
are explicit in the paths they're linking (e.g. link_to 'New Person', new_person_path
), but many times the paths are implicit (e.g. link_to 'Show', person
).
I add some single table inheritance (STI) to my model (say Employee < Person
), and all of these methods break for an instance of the subclass (say Employee
); when rails executes link_to @person
, it errors with undefined method employee_path' for #<#<Class:0x000001022bcd40>:0x0000010226d038>
. Rails is looking for a route defined by the class name of the object, which is employee. These employee routes are not defined, and there is no employee controller so the actions aren't defined either.
This question has been asked before:
- At StackOverflow, the answer is to edit every instance of link_to etc in your entire codebase, and state the path explicitly
- On StackOverflow again, two people suggest using
routes.rb
to map the subclass resources to the parent class (map.resources :employees, :controller => 'people'
). The top answer in that same SO question suggests type-casting every instance object in the codebase using.becomes
- Yet another one at StackOverflow, the top answer is way in the Do Repeat Yourself camp, and suggests creating duplicate scaffolding for every subclass.
- Here's the same question again at SO, where the top answer seems to just be wrong (Rails magic Just Works!)
- Elsewhere on the web, I found this blog post where F2Andy recommends editing in the path everywhere in the code.
- On the blog post Single Table Inheritance and RESTful Routes at Logical Reality Design, it is recommended to map the resources for the subclass to the superclass controller, as in SO answer number 2 above.
- Alex Reisner has a post Single Table Inheritance in Rails, in which he advocates against mapping the resources of the child classes to the parent class in
routes.rb
, since that only catches routing breakages fromlink_to
andredirect_to
, but not fromform_for
. So he recommends instead adding a method to the parent class to get the subclasses to lie about their class. Sounds good, but his method gave me the errorundefined local variable or method `child' for #
.
So the answer that seems most elegant and has the most consensus (but it's not all that elegant, nor that much consensus), is the add the resources to your routes.rb
. Except this doesn't work for form_for
. I need some clarity! To distill the choices above, my options are
- map the resources of the subclass to the controller of the superclass in
routes.rb
(and hope I don't need to call form_for on any subclasses) - Override rails internal methods to make the classes lie to each other
- Edit every instance in the code where the path to an object's action is invoked implicitly or explicitly, either changing the path or type-casting the object.
With all these conflicting answers, I need a ruling. It seems to me like there is no good answer. Is this a failing in rails' design? If so, is it a bug that may get fixed? Or if not, then I'm hoping someone can set me straight on this, walk me through the pros and cons of each option (or explain why that's not an option), and which one is the right answer, and why. Or is there a right answer that I'm not finding on the web?
Solution 1:
This is the simplest solution I was able to come up with with minimal side effect.
class Person < Contact
def self.model_name
Contact.model_name
end
end
Now url_for @person
will map to contact_path
as expected.
How it works: URL helpers rely on YourModel.model_name
to reflect upon the model and generate (amongst many things) singular/plural route keys. Here Person
is basically saying I'm just like Contact
dude, ask him.
Solution 2:
I had the same problem. After using STI, the form_for
method was posting to the wrong child url.
NoMethodError (undefined method `building_url' for
I ended up adding in the extra routes for the child classes and pointing them to the same controllers
resources :structures
resources :buildings, :controller => 'structures'
resources :bridges, :controller => 'structures'
Additionally:
<% form_for(@structure, :as => :structure) do |f| %>
in this case structure is actually a building (child class)
It seems to work for me after doing a submit with form_for
.