我的Rails视图和控制器散落着redirect_to
,link_to
和form_for
方法调用。有时link_to
和redirect_to
在它们连接的路径中是明确的(例如link_to 'New Person', new_person_path
),但很多时候路径是隐含的(例如link_to 'Show', person
)。
我将一些单表继承(STI)添加到我的模型(比如Employee < Person
),并且所有这些方法都打破了子类的一个实例(比如Employee
);当rails执行link_to @person
时,它与undefined method employee_path' for #<#<Class:0x000001022bcd40>:0x0000010226d038>
错误。 Rails正在寻找由对象的类名定义的路由,即雇员。这些员工路线未定义,并且没有员工控制器,因此也未定义操作。
之前已经问过这个问题:
routes.rb
将子类资源映射到父类(map.resources :employees, :controller => 'people'
)。在同一个SO问题中的最佳答案建议使用.becomes
在代码库中输入每个实例对象routes.rb
中的父类,因为它只捕获link_to
和redirect_to
的路由断点,但不是来自form_for
。所以他建议在父类中添加一个方法,让子类对它们的类撒谎。听起来不错,但他的方法给了我错误undefined local variable or method `child' for #
。因此,似乎最优雅且最具共识的答案(但并非所有优雅,也不是那么多共识),是为您的routes.rb
添加资源。除此之外不适用于form_for
。我需要一些清晰度!为了提炼上述选择,我的选择是
routes.rb
中超类的控制器(并希望我不需要在任何子类上调用form_for)有了所有这些相互矛盾的答案,我需要一个裁决。在我看来,似乎没有好的答案。这是rails设计中的失败吗?如果是这样,它是否可以修复?或者如果没有,那么我希望有人可以让我直截了当,让我了解每个选项的优点和缺点(或解释为什么不是一个选项),哪一个是正确的答案,以及为什么。或者是否有一个我在网上找不到的正确答案?
这是我能够提出的最简单的解决方案,副作用最小。
class Person < Contact
def self.model_name
Contact.model_name
end
end
现在,url_for @person
将按照预期映射到contact_path
。
工作原理:URL助手依靠YourModel.model_name
来反映模型并生成(在许多方面)单数/复数路由键。在这里Person
基本上说我就像Contact
老兄,问他。
这是一种安全清洁的方式,使其在表单和我们使用的整个应用程序中工作。
resources :districts
resources :district_counties, controller: 'districts', type: 'County'
resources :district_cities, controller: 'districts', type: 'City'
然后我有我的形式。增加的部分是as :: district。
= form_for(@district, as: :district, html: { class: "form-horizontal", role: "form" }) do |f|
希望这可以帮助。
如果我考虑这样的STI继承:
class AModel < ActiveRecord::Base ; end
class BModel < AModel ; end
class CModel < AModel ; end
class DModel < AModel ; end
class EModel < AModel ; end
在'app / models / a_model.rb'中我添加:
module ManagedAtAModelLevel
def model_name
AModel.model_name
end
end
然后在AModel类中:
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
因此,我甚至可以轻松地选择我想使用默认模型的模型,而这甚至没有触及子类定义。非常干燥。
这种方式对我有用(在基类中定义此方法):
def self.inherited(child)
child.instance_eval do
alias :original_model_name :model_name
def model_name
Task::Base.model_name
end
end
super
end
您可以创建返回虚拟父对象的方法以进行灌浆
class Person < ActiveRecord::Base
def routing_object
Person.new(id: id)
end
end
然后简单地调用form_for @ employee.routing_object,其中没有类型将返回Person类对象
关注@ prathan-thananart回答,对于多个STI类,您可以将以下内容添加到父模型中 - >
class Contact < ActiveRecord::Base
def self.model_name
ActiveModel::Name.new(self, nil, 'Contact')
end
end
这将使每个表格与联系人数据发送params为params[:contact]
而不是params[:contact_person]
,params[:contact_whatever]
。
hackish,但只是解决方案列表中的另一个。
class Parent < ActiveRecord::Base; end
Class Child < Parent
def class
Parent
end
end
适用于rails 2.x和3.x.
我有同样的问题。使用STI后,form_for
方法发布到错误的子URL。
NoMethodError (undefined method `building_url' for
我最后添加了子类的额外路由并将它们指向相同的控制器
resources :structures
resources :buildings, :controller => 'structures'
resources :bridges, :controller => 'structures'
另外:
<% form_for(@structure, :as => :structure) do |f| %>
在这种情况下,结构实际上是一个建筑物(子类)
在用form_for
提交后,它似乎对我有用。
我建议你看看:https://stackoverflow.com/a/605172/445908,使用这种方法可以让你使用“form_for”。
ActiveRecord::Base#becomes
在路线中使用类型:
resources :employee, controller: 'person', type: 'Employee'
http://samurails.com/tutorial/single-table-inheritance-with-rails-4-part-2/
遵循@Prathan Thananart的想法,但试图不破坏任何东西。 (因为涉及太多魔法)
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
现在url_for @person将按预期映射到contact_path。
我也遇到了这个问题的困难,并且在类似于我们的问题上得到了这个答案。它对我有用。
form_for @list.becomes(List)
答案显示在这里:Using STI path with same controller
.becomes
方法被定义为主要用于解决像你的form_for
一样的STI问题。
.becomes
信息在这里:http://apidock.com/rails/ActiveRecord/Base/becomes
超级迟到的回应,但这是我能找到的最佳答案,对我来说效果很好。希望这对某人有所帮助。干杯!
好吧,我在Rails的这个领域遇到了很多挫折,并且已经达到了以下方法,也许这将有助于其他人。
首先要注意的是,网络上方和周围的许多解决方案都建议在客户端提供的参数上使用constantize。这是一个已知的DoS攻击向量,因为Ruby不会垃圾收集符号,从而允许攻击者创建任意符号并消耗可用内存。
我已经实现了下面的方法,它支持模型子类的实例化,并且从上面的contantize问题是SAFE。它与rails 4的功能非常相似,但也允许不止一个级别的子类(与Rails 4不同),并且可以在Rails 3中运行。
# initializers/acts_as_castable.rb
module ActsAsCastable
extend ActiveSupport::Concern
module ClassMethods
def new_with_cast(*args, &block)
if (attrs = args.first).is_a?(Hash)
if klass = descendant_class_from_attrs(attrs)
return klass.new(*args, &block)
end
end
new_without_cast(*args, &block)
end
def descendant_class_from_attrs(attrs)
subclass_name = attrs.with_indifferent_access[inheritance_column]
return nil if subclass_name.blank? || subclass_name == self.name
unless subclass = descendants.detect { |sub| sub.name == subclass_name }
raise ActiveRecord::SubclassNotFound.new("Invalid single-table inheritance type: #{subclass_name} is not a subclass of #{name}")
end
subclass
end
def acts_as_castable
class << self
alias_method_chain :new, :cast
end
end
end
end
ActiveRecord::Base.send(:include, ActsAsCastable)
在为开发问题中的“子类加载”尝试了各种方法之后,我发现唯一可行的方法就是在我的模型类中使用'require_dependency'。这可确保类加载在开发中正常工作,并且不会导致生产中出现问题。在开发中,没有'require_dependency'AR不会知道所有子类,这会影响为类型列匹配而发出的SQL。此外,如果没有'require_dependency',您最终也会遇到具有多个版本的模型类的情况! (例如,当您更改基类或中间类时,可能会发生这种情况,子类似乎并不总是重新加载,而是从旧类继承子类)
# contact.rb
class Contact < ActiveRecord::Base
acts_as_castable
end
require_dependency 'person'
require_dependency 'organisation'
我也没有按照上面的建议覆盖model_name,因为我使用I18n并且需要不同的字符串用于不同子类的属性,例如:tax_identifier变为组织的'ABN',以及Person的'TFN'(在澳大利亚)。
我也使用路由映射,如上所述,设置类型:
resources :person, :controller => 'contacts', :defaults => { 'contact' => { 'type' => Person.sti_name } }
resources :organisation, :controller => 'contacts', :defaults => { 'contact' => { 'type' => Organisation.sti_name } }
除了路由映射,我正在使用InheritedResources和SimpleForm,我使用以下通用表单包装器来执行新操作:
simple_form_for resource, as: resource_request_name, url: collection_url,
html: { class: controller_name, multipart: true }
...以及编辑操作:
simple_form_for resource, as: resource_request_name, url: resource_url,
html: { class: controller_name, multipart: true }
为了使这项工作,在我的基础ResourceContoller中,我将InheritedResource的resource_request_name公开为视图的辅助方法:
helper_method :resource_request_name
如果您没有使用InheritedResources,请在“ResourceController”中使用以下内容:
# controllers/resource_controller.rb
class ResourceController < ApplicationController
protected
helper_method :resource
helper_method :resource_url
helper_method :collection_url
helper_method :resource_request_name
def resource
@model
end
def resource_url
polymorphic_path(@model)
end
def collection_url
polymorphic_path(Model)
end
def resource_request_name
ActiveModel::Naming.param_key(Model)
end
end
总是乐于听到别人的经历和改进。
我最近documented我尝试在Rails 3.0应用程序中获得稳定的STI模式。这是TL; DR版本:
# 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
这种方法可以解决您列出的问题以及其他人在STI方法中遇到的其他问题。
如果没有嵌套路由,可以试试这个:
resources :employee, path: :person, controller: :person
或者你可以采用另一种方式并使用如下所述的OOP魔术:https://coderwall.com/p/yijmuq
在第二种方式中,您可以为所有嵌套模型创建类似的帮助程序。