Best practices to handle routes for STI subclasses in rails

后端 未结 18 1168
面向向阳花
面向向阳花 2020-11-30 16:25

My Rails views and controllers are littered with redirect_to, link_to, and form_for method calls. Sometimes link_to and <

相关标签:
18条回答
  • 2020-11-30 16:48

    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.

    0 讨论(0)
  • 2020-11-30 16:49

    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.

    0 讨论(0)
  • 2020-11-30 16:49

    You can try this, if you have no nested routes:

    resources :employee, path: :person, controller: :person
    

    Or you can go another way and use some OOP-magic like described here: https://coderwall.com/p/yijmuq

    In second way you can make similar helpers for all your nested models.

    0 讨论(0)
  • 2020-11-30 16:51

    Overriding model_name seems dangerous. Using .becomes seems like the safer option.

    One issue is in cases where you don't know what model you are dealing with (and thus the base model).

    I just wanted to share that in such cases, one can use:

    foo.becomes(foo.class.base_class)
    

    For ease of use, I've added this method to my ApplicationRecord:

    def becomes_base
      becomes(self.class.base_class)
    end
    

    Adding .becomes_base to a few route helper methods doesn't seem like too big of a deal to me.

    0 讨论(0)
  • 2020-11-30 16:53

    I recently documented my attempts to get a stable STI pattern working in a Rails 3.0 app. Here's the TL;DR version:

    # app/controllers/kase_controller.rb
    class KasesController < ApplicationController
    
      def new
        setup_sti_model
        # ...
      end
    
      def create
        setup_sti_model
        # ...
      end
    
    private
    
      def setup_sti_model
        # This lets us set the "type" attribute from forms and querystrings
        model = nil
        if !params[:kase].blank? and !params[:kase][:type].blank?
          model = params[:kase].delete(:type).constantize.to_s
        end
        @kase = Kase.new(params[:kase])
        @kase.type = model
      end
    end
    
    # app/models/kase.rb
    class Kase < ActiveRecord::Base
      # This solves the `undefined method alpha_kase_path` errors
      def self.inherited(child)
        child.instance_eval do
          def model_name
            Kase.model_name
          end
        end
        super
      end  
    end
    
    # app/models/alpha_kase.rb
    # Splitting out the subclasses into separate files solves
    # the `uninitialize constant AlphaKase` errors
    class AlphaKase < Kase; end
    
    # app/models/beta_kase.rb
    class BetaKase < Kase; end
    
    # config/initializers/preload_sti_models.rb
    if Rails.env.development?
      # This ensures that `Kase.subclasses` is populated correctly
      %w[kase alpha_kase beta_kase].each do |c|
        require_dependency File.join("app","models","#{c}.rb")
      end
    end
    

    This approach gets around the problems that you list as well as a number of other issues that others have had with STI approaches.

    0 讨论(0)
  • 2020-11-30 16:55

    If I consider an STI inheritance like this:

    class AModel < ActiveRecord::Base ; end
    class BModel < AModel ; end
    class CModel < AModel ; end
    class DModel < AModel ; end
    class EModel < AModel ; end
    

    in 'app/models/a_model.rb' I add:

    module ManagedAtAModelLevel
      def model_name
        AModel.model_name
      end
    end
    

    And then in the AModel class:

    class AModel < ActiveRecord::Base
      def self.instanciate_STI
        managed_deps = { 
          :b_model => true,
          :c_model => true,
          :d_model => true,
          :e_model => true
        }
        managed_deps.each do |dep, managed|
          require_dependency dep.to_s
          klass = dep.to_s.camelize.constantize
          # Inject behavior to be managed at AModel level for classes I chose
          klass.send(:extend, ManagedAtAModelLevel) if managed
        end
      end
    
      instanciate_STI
    end
    

    Therefore I can even easily choose which model I want to use the default one, and this without even touching the sub class definition. Very dry.

    0 讨论(0)
提交回复
热议问题