STI,一个控制器

时间:2011-03-09 13:54:07

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

我是rails的新手,而且我很容易遇到这个设计问题,这可能很容易解决,但我无处可去: 我有两种不同的广告:亮点和便宜货。它们都具有相同的属性:标题,描述和一个图像(使用回形针)。它们也有相同的动作可以应用于它们:索引,新建,编辑,创建,更新和销毁。

我设置了这样的STI:

广告模型:ad.rb

class Ad < ActiveRecord::Base
end

讨价还价模型:bargain.rb

class Bargain < Ad
end

突出显示模型:highlight.rb

class Highlight < Ad
end

问题是我想只有一个控制器(AdsController)执行我在议价或亮点上所说的动作,具体取决于URL,比如www.foo.com/bargains[/ .. 。]或www.foo.com/highlights [/...].

例如:

  • 获取www.foo.com/highlights =&gt;所有重点广告的列表。
  • 获取www.foo.com/highlights/new =&gt;用于创建新亮点的表单 等...

我该怎么做?

谢谢!

5 个答案:

答案 0 :(得分:99)

首先。添加一些新路线:

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

<强> URLS

<%= 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 %>

答案 1 :(得分:63)

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

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

答案 2 :(得分:12)

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

Alex Reisner - Single Table Inheritance in Rails

答案 3 :(得分:0)

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

对其他答案进行迭代,我为单表控制器提供了以下解决方案,该控制器具有单表继承,适用于Rails 4.1中的强参数。仅包括:如果输入的类型无效,则键入允许的参数会导致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并设置scaffolding以使用与STI兼容的responders_controller。脚手架的配置是:

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

答案 4 :(得分:0)

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

在您的模型中,在启动时创建白名单。在开发中,这必须加载。

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

现在我们可以在任何控制器中引用此白名单来构建正确的范围,以及在表单中选择:类型选择

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

我们需要处理表单,因为路由应该发送到基类控制器,尽管实际的:对象的类型。为此,我们使用&#34;成为&#34;将表单构建器引入正确的路由,并使用:as指令强制输入名称作为基类。这种组合允许我们使用未经修改的路线(资源:广告)以及从表格中返回的参数[:ad]的强制参数检查。

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