【问题标题】:STI, one controllerSTI,一个控制器
【发布时间】:2011-07-11 22:22:00
【问题描述】:

我是 Rails 新手,我有点被这个设计问题所困扰,这可能很容易解决,但我没有得到任何结果: 我有两种不同的广告:亮点和特价。它们都具有相同的属性:标题、描述和一张图片(带有回形针)。它们还可以应用相同类型的操作:索引、新建、编辑、创建、更新和销毁。

我设置了这样的 STI:

广告模型:ad.rb

class Ad < ActiveRecord::Base
end

讨价还价模式:deal.rb

class Bargain < Ad
end

高亮模型:highlight.rb

class Highlight < Ad
end

问题是我希望只有一个控制器 (AdsController) 来执行我所说的根据 URL 进行讨价还价或突出显示的操作,例如 www.foo.com/bargains[/...]或 www.foo.com/highlights[/...]。

例如:

  • GET www.foo.com/highlights => 一个所有亮点广告的列表。
  • GET www.foo.com/highlights/new => 表单来创建一个新的亮点 等等……

我该怎么做?

谢谢!

【问题讨论】:

    标签: ruby-on-rails ruby ruby-on-rails-3


    【解决方案1】:

    首先。添加一些新路线:

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

    并修复AdsController 中的一些操作。例如:

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

    要获得所有这些控制器工作的最佳方法,请查看 this comment

    就是这样。现在您可以转到localhost:3000/highlights/new 并初始化新的Highlight

    索引操作可能如下所示:

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

    转到localhost:3000/highlights,将出现亮点列表。
    讨价还价的方式相同:localhost:3000/bargains

    网址

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

    因为是多态的:)

    <%= link_to 'index', @ad.class %>
    

    【讨论】:

    • 是的!谢谢,效果很好!!我知道有一种方法可以以 DRY 和 RESTful 的方式执行此操作。我现在唯一遇到的问题是如何根据广告类型正确设置路径和网址。我想我可以只检查类型值,但我不觉得,非常聪明......有什么想法吗?
    • 好吧,我已经完成了,我已经使用polymorphic_path 设置视图中的路径以及 Alan 的想法(如下所示)根据 URL 使用正确的类(亮点或讨价还价)...我不完全相信,但它的工作原理和看起来不错。
    • 嗯...我不知道...当您有一个使用相同视图的单个控制器时,在这种情况下,ads_controller,您不应该指定:highlights,或:讨价还价。我这样做的方式:link_to "new", new_polymorphic_path(@ad.class)
    • 你可以:)。例如,它会将您发送到higlights_path,即ads_controller。就试一试吧 :)。其实这也是 polymorphic_path
    • 哦,我知道了。你可以试试&lt;%= link_to 'index', @ad.class %&gt; 以及 polymorphic_path 的缩写形式
    【解决方案2】:

    fl00r 有一个很好的解决方案,但是我会做一个调整。

    根据您的情况,这可能需要也可能不需要。这取决于您的 STI 模型中发生了什么行为,尤其是验证和生命周期挂钩。

    为你的控制器添加一个私有方法,将你的类型参数转换为你想要使用的实际类常量:

    def ad_type
      params[:type].constantize
    end
    

    但是,上述内容是不安全的。添加类型的白名单:

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

    更多关于rails constantize方法的信息在这里:http://api.rubyonrails.org/classes/ActiveSupport/Inflector.html#method-i-constantize

    然后在控制器中你可以做的动作:

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

    现在您使用的是具有正确行为的实际类,而不是设置了属性类型的父类。

    【讨论】:

    • FWIW,我必须将创建方法设置为:ad_type.new(params[params[:type].downcase])
    • 我认为你的方法应该更新为params[:type].underscore。示例:如果您的类型是BrandNewType,那么您应该参考params['brand_new_type'] 而不是params['brandnewtype']
    • 不要在生产代码中按原样使用此方法。 一开始可能并不明显,但默认情况下,此 Ad#ad_type 方法隐式信任用户提供的数据,打破了网络应用安全的基本规则。想象一个对/ads?type=User 的POST 请求,其有效负载包括is_admin=1(或任何适用于正在使用的身份验证机制的内容)。现在攻击用户拥有自己的具有管理员权限的用户帐户! (从技术上讲,可以通过更改或删除默认的ads 路由来击败此攻击向量,但稍后的新路由可能会在不知不觉中再次打开漏洞。)
    • 安全问题的解决方案是提供白名单。比如:allowed_types = [MyType, MyType2],然后是params[:type].constantize if params[:type].in? allowed_types
    • 在检查 params[:type] 是否包含之前,必须将有效的 ad_types 数组转换为字符串
    【解决方案3】:

    我只是想包含这个链接,因为有许多有趣的技巧都与这个主题相关。

    Alex Reisner - Single Table Inheritance in Rails

    【讨论】:

    【解决方案4】:

    [用完全有效的更简单的解决方案重写:]

    迭代其他答案,我为具有单表继承的单个控制器提出了以下解决方案,该解决方案适用于 Rails 4.1 中的强参数。如果输入无效类型,仅将 :type 作为允许的参数包含会导致ActiveRecord::SubclassNotFound 错误。此外,类型不会更新,因为 SQL 查询显式查找旧类型。相反,如果 :type 与当前设置的不同并且是有效类型,则需要使用 update_column 单独更新它。另请注意,我已经成功地干燥了所有类型列表。

    # 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
    

    在控制器中:

      # 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
    

    还有路线:

    # 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'
    

    最后,我建议使用responders gem 并将脚手架设置为使用与 STI 兼容的 responders_controller。脚手架的配置是:

    # config/application.rb
        config.generators do |g|
          g.scaffold_controller "responders_controller"
        end
    

    【讨论】:

    • 我喜欢验证器。查看我的变体,它基于子类构建白名单,并调整表单而不是每种类型都需要路由。如果您有大量嵌套路由并且不希望每个子类都存在大量近乎重复的内容,那么这将非常有用。
    【解决方案5】:

    我知道这是一个老问题,这是我喜欢的一种模式,其中包括来自@flOOr 和@Alan_Peabody 的答案。 (在 Rails 4.2 中测试,可能适用于 Rails 5)

    在您的模型中,在启动时创建白名单。在 dev 中,这必须是预先加载的。

    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
    

    现在我们可以在任何控制器中引用这个白名单来构建正确的范围,以及在表单上的 :type 选择的集合中,等等。

    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
    

    我们需要处理我们的表单,因为路由应该被发送到基类控制器,尽管对象的实际 :type 是。为此,我们使用“成为”来欺骗表单构建器进入正确的路由,并使用 :as 指令强制输入名称也成为基类。这种组合允许我们使用未修改的路由(资源:ads)以及对从表单返回的 params[:ad] 进行强大的参数检查。

    #/views/ads/_form.html.erb
    <%= form_for(@ad.becomes(Ad), :as => :ad) do |f| %>
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-09-07
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多