Best practices to handle routes for STI subclasses in rails

30,054

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.

Solution 3

I suggest you take a look at : https://stackoverflow.com/a/605172/445908, using this method will enable you to use "form_for".

ActiveRecord::Base#becomes

Solution 4

Use type in the routes:

resources :employee, controller: 'person', type: 'Employee' 

http://samurails.com/tutorial/single-table-inheritance-with-rails-4-part-2/

Solution 5

Following the idea of @Prathan Thananart but trying to not destroy nothing. (since there is so much magic involved)

class Person < Contact
  model_name.class_eval do
    def route_key
     "contacts"
    end
    def singular_route_key
      superclass.model_name.singular_route_key
    end
  end
end

Now url_for @person will map to contact_path as expected.

Share:
30,054

Related videos on Youtube

ziggurism
Author by

ziggurism

Updated on July 08, 2022

Comments

  • ziggurism
    ziggurism almost 2 years

    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:

    1. At StackOverflow, the answer is to edit every instance of link_to etc in your entire codebase, and state the path explicitly
    2. 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
    3. Yet another one at StackOverflow, the top answer is way in the Do Repeat Yourself camp, and suggests creating duplicate scaffolding for every subclass.
    4. Here's the same question again at SO, where the top answer seems to just be wrong (Rails magic Just Works!)
    5. Elsewhere on the web, I found this blog post where F2Andy recommends editing in the path everywhere in the code.
    6. 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.
    7. 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 from link_to and redirect_to, but not from form_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 error undefined 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

    1. 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)
    2. Override rails internal methods to make the classes lie to each other
    3. 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?

    • ziggurism
      ziggurism over 13 years
      There was a typo in Alex Reisner's code, which he's fixed after I commented on his blog. So hopefully now Alex's solution is viable. My question still stands: which is the right solution?
    • ziggurism
      ziggurism over 13 years
      Although it's about three years old, I found this blog post at rookieonrails.blogspot.com/2008/01/… and the linked conversation from the rails mailing list informative. One of the responders describes the difference between polymorphic helpers and named helpers.
    • M. Scott Ford
      M. Scott Ford over 12 years
      An option that you don't list is to patch Rails so that link_to, form_for and the like place nice with single table inheritance. That may be a tough job, but it is something that I would love to see fixed.
    • Felixyz
      Felixyz over 9 years
  • Anders Kindberg
    Anders Kindberg over 12 years
    This works, but adds a lot of unneccessary paths in our routes. Isn't there a way to do this in a less intrusive way?
  • Chris Bloom
    Chris Bloom about 12 years
    This fixes one problem, but creates another. Now when you try to do Child.new it returns a Parent class rather than the subclass. This means that you can't create the subclasses via mass assignment through a controller (since type is a protected attribute by default) unless you also set the type attribute explicitly.
  • Chris Bloom
    Chris Bloom about 12 years
    You can setup routes programatically in your routes.rb file, so you could do a little meta programming to setup the child routes. However, in environments where classes aren't cached (e.g. development) you need to pre-load the classes first. So one way or the other you need to specify the subclasses somewhere. See gist.github.com/1713398 for an example.
  • Mario Uher
    Mario Uher about 12 years
    Wow, why isn't this answer the most up voted one? Definitely the best solution for this problem!
  • nkassis
    nkassis about 12 years
    I was thinking of doing the same thing, but was worried that #model_name might be used elsewhere in Rails, and that this change might interfere with normal functioning. Any thoughts?
  • tsherif
    tsherif about 12 years
    I totally agree with mysterious stranger @nkassis. This is a cool hack, but how do you know you're not busting up rails' internals?
  • Prathan Thananart
    Prathan Thananart about 12 years
    Specs. Also, we use this code in production, and I can attest that it doesn't mess up: 1) inter-model relationships, 2) STI model instantiation (via build_x/create_x). On the other hand, the price of playing with magic is you're never 100% sure what may change.
  • lulalala
    lulalala almost 12 years
    I had to set url explicitly in order for it to both render the from and saves properly. <%= form_for @child, :as => :child, url: @child.becomes(Parent)
  • Rufo Sanchez
    Rufo Sanchez almost 12 years
    This breaks i18n if you're trying to have different human names for attributes depending on the class.
  • sj26
    sj26 almost 11 years
    Rather than completely overriding like this, you can just override the bits you need. See gist.github.com/sj26/5843855
  • Richard Jones
    Richard Jones over 10 years
    @lulalala Try <%= form_for @child.becomes(Parent)
  • laffuste
    laffuste almost 10 years
    In my case, exposing the object name (path) to the user is not desirable (and confusing for the user).
  • StackNG
    StackNG over 9 years
    This solution will break i18n usage. "@model.model_name.human" is frequently used in an i18n app. "@model.becomes" seems better in this situation.
  • Lex Lindsey
    Lex Lindsey over 9 years
    In my experience (at least in Rails 3.0.9), constantize fails if the constant named by the string doesn't already exist. So how can it be used to create arbitrary new symbols?
  • T_Dnzt
    T_Dnzt almost 9 years
    Domain changed, the mentioned article is now available there: samurails.com/tutorial/…
  • isthmuses
    isthmuses over 8 years
    I might also just add that instead of explicitly specifying the parent model with Contact.model_name, another option is base_class.model_name.
  • Finn
    Finn over 6 years
    Check out eloysp's answer below. They propose an approach that changes the model name only for routing concerns and should not mess up anything.
  • rtrojanowski
    rtrojanowski almost 5 years
    I like your solution as it's a very simple approach but unfortunately, it breaks things if you use ActiveAdmin. And who knows what else.
  • saurabh
    saurabh almost 4 years
    The links in post and comment are dead
  • buncis
    buncis over 2 years
    @sj26 lol that is way more overriding than the answer
  • buncis
    buncis over 2 years
    I'm on rails 6.1.4 it got undefined local variable or method 'superclass' my quick fix is add the class name liek this Person.superclass.model_name.singular_route_key
  • UsAndRufus
    UsAndRufus almost 2 years
    This should be the accepted answer, it only overrides URL helpers which is what OP asked for, and doesn't break other things such as i18n