我是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 [/...].
例如:
我该怎么做?
谢谢!
答案 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)
我只想包含此链接,因为有许多与此主题相关的有趣技巧。
答案 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| %>