STI, one controller

后端 未结 5 2021
谎友^
谎友^ 2020-11-27 09:49

I\'m new to rails and I\'m kind of stuck with this design problem, that might be easy to solve, but I don\'t get anywhere: I have two different kinds of advertisements: high

相关标签:
5条回答
  • 2020-11-27 10:25

    [Rewritten with simpler solution that works fully:]

    Iterating on the other answers, I have come up with the following solution for a single controller with Single Table Inheritance that works well with Strong Parameters in Rails 4.1. Just including :type as a permitted parameter caused an ActiveRecord::SubclassNotFound error if an invalid type is entered. Moreover, type is not updated because the SQL query explicitly looks for the old type. Instead, :type needs to be updated separately with update_column if it is different than what is current set and is a valid type. Note also that I've succeeded in DRYing up all lists of types.

    # app/models/company.rb
    class Company < ActiveRecord::Base
      COMPANY_TYPES = %w[Publisher Buyer Printer Agent]
      validates :type, inclusion: { in: COMPANY_TYPES,
        :message => "must be one of: #{COMPANY_TYPES.join(', ')}" }
    end
    
    Company::COMPANY_TYPES.each do |company_type|
      string_to_eval = <<-heredoc
        class #{company_type} < Company
          def self.model_name  # http://stackoverflow.com/a/12762230/1935918
            Company.model_name
          end
        end
      heredoc
      eval(string_to_eval, TOPLEVEL_BINDING)
    end
    

    And in the controller:

      # app/controllers/companies_controller.rb
      def update
        @company = Company.find(params[:id])
    
        # This separate step is required to change Single Table Inheritance types
        new_type = params[:company][:type]
        if new_type != @company.type && Company::COMPANY_TYPES.include?(new_type)
          @company.update_column :type, new_type
        end
    
        @company.update(company_params)
        respond_with(@company)
      end
    

    And routes:

    # config/routes.rb
    Rails.application.routes.draw do
      resources :companies
      Company::COMPANY_TYPES.each do |company_type|
        resources company_type.underscore.to_sym, type: company_type, controller: 'companies', path: 'companies'
      end
      root 'companies#index'
    

    Finally, I recommend using the responders gem and setting scaffolding to use a responders_controller, which is compatible with STI. Config for scaffolding is:

    # config/application.rb
        config.generators do |g|
          g.scaffold_controller "responders_controller"
        end
    
    0 讨论(0)
  • 2020-11-27 10:26

    I know this is an old question by here is a pattern I like which includes the answers from @flOOr and @Alan_Peabody. (Tested in Rails 4.2, probably works in Rails 5)

    In your model, create your whitelist at startup. In dev this must be eager loaded.

    class Ad < ActiveRecord::Base
        Rails.application.eager_load! if Rails.env.development?
        TYPE_NAMES = self.subclasses.map(&:name)
        #You can add validation like the answer by @dankohn
    end
    

    Now we can reference this whitelist in any controller to build the correct scope, as well as in a collection for a :type select on a form, etc.

    class AdsController < ApplicationController
        before_action :set_ad, :only => [:show, :compare, :edit, :update, :destroy]
    
        def new
            @ad = ad_scope.new
        end
    
        def create
            @ad = ad_scope.new(ad_params)
            #the usual stuff comes next...
        end
    
        private
        def set_ad
            #works as normal but we use our scope to ensure subclass
            @ad = ad_scope.find(params[:id])
        end
    
        #return the scope of a Ad STI subclass based on params[:type] or default to Ad
        def ad_scope
            #This could also be done in some kind of syntax that makes it more like a const.
            @ad_scope ||= params[:type].try(:in?, Ad::TYPE_NAMES) ? params[:type].constantize : Ad
        end
    
        #strong params check works as expected
        def ad_params
            params.require(:ad).permit({:foo})
        end
    end
    

    We need to handle our forms because the routing should to be sent to the base class controller, despite the actual :type of the object. To do this we use "becomes" to trick the form builder into correct routing, and the :as directive to force the input names to be the base class as well. This combination allows us to use unmodified routes (resources :ads) as well as the strong params check on the params[:ad] coming back from the form.

    #/views/ads/_form.html.erb
    <%= form_for(@ad.becomes(Ad), :as => :ad) do |f| %>
    
    0 讨论(0)
  • 2020-11-27 10:29

    I just wanted to include this link because there are a number of interesting tricks all related to this topic.

    Alex Reisner - Single Table Inheritance in Rails

    0 讨论(0)
  • 2020-11-27 10:31

    First. Add some new routes:

    resources :highlights, :controller => "ads", :type => "Highlight"
    resources :bargains, :controller => "ads", :type => "Bargain"
    

    And fix some actions in AdsController. For example:

    def new
      @ad = Ad.new()
      @ad.type = params[:type]
    end
    

    For best approach for all this controller job look this comment

    That's all. Now you can go to localhost:3000/highlights/new and new Highlight will be initialized.

    Index action can look like this:

    def index
      @ads = Ad.where(:type => params[:type])
    end
    

    Go to localhost:3000/highlights and list of highlights will appear.
    Same way for bargains: localhost:3000/bargains

    etc

    URLS

    <%= link_to 'index', :highlights %>
    <%= link_to 'new', [:new, :highlight] %>
    <%= link_to 'edit', [:edit, @ad] %>
    <%= link_to 'destroy', @ad, :method => :delete %>
    

    for being polymorphic :)

    <%= link_to 'index', @ad.class %>
    
    0 讨论(0)
  • 2020-11-27 10:40

    fl00r has a good solution, however I would make one adjustment.

    This may or may not be required in your case. It depends on what behavior is changing in your STI models, especially validations & lifecycle hooks.

    Add a private method to your controller to convert your type param to the actual class constant you want to use:

    def ad_type
      params[:type].constantize
    end
    

    The above is insecure, however. Add a whitelist of types:

    def ad_types
      [MyType, MyType2]
    end
    
    def ad_type
      params[:type].constantize if params[:type].in? ad_types
    end
    

    More on the rails constantize method here: http://api.rubyonrails.org/classes/ActiveSupport/Inflector.html#method-i-constantize

    Then in the controller actions you can do:

    def new
      ad_type.new
    end
    
    def create
      ad_type.new(params)
      # ...
    end
    
    def index
      ad_type.all
    end
    

    And now you are using the actual class with the correct behavior instead of the parent class with the attribute type set.

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