My 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设计中的失败吗?如果是这样,它是否可以修复?或者如果没有,那么我希望有人可以让我直截了当,让我了解每个选项的优缺点(或解释为什么这不是一个选项),哪一个是正确的答案,以及为什么。或者是否有正确的答案,我没有在网上找到?
答案 0 :(得分:130)
这是我能够提出的最简单的解决方案,副作用很小。
class Person < Contact
def self.model_name
Contact.model_name
end
end
现在url_for @person
将按预期映射到contact_path
。
工作原理:URL帮助程序依赖YourModel.model_name
来反映模型并生成(在许多方面)单数/复数路径键。这里Person
基本上是在说我就像Contact
老兄,问他。
答案 1 :(得分:46)
我遇到了同样的问题。使用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
提交后,似乎对我有用。
答案 2 :(得分:31)
我建议您查看:https://stackoverflow.com/a/605172/445908,使用此方法可以使用“form_for”。
ActiveRecord::Base#becomes
答案 3 :(得分:17)
在路线中使用类型:
resources :employee, controller: 'person', type: 'Employee'
http://samurails.com/tutorial/single-table-inheritance-with-rails-4-part-2/
答案 4 :(得分:13)
遵循@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。
答案 5 :(得分:11)
我也遇到了这个问题的困难,并且在类似于我们的问题上得出了这个答案。它对我有用。
food(indian) :-
write('Do you want spicy? (spicy/not_spicy)'),
read(Preference),
food(indian,Preference).
food(chinese) :-
write('Do you want fry food? (fry/not_fry)'),
read(Preference),
food(chinese,Preference).
food(malay) :-
write('Do you want chili? (chili/not_chili)'),
read(Preference),
food(malay,Preference).
food(indian,spicy) :- recommend("Curry").
food(indian,not_spicy) :- recommend("Kurma").
food(chinese,fry) :- recommend("StirFry").
food(chinese,not_fry) :- recommend("Chicken").
food(malay,chili) :- recommend("Sambal").
food(malay,not_chili) :- recommend("Singgang").
recommend(Food) :- write('We recommend you '), write(Food).
go :-
write('Which food type do you prefer? (indian, chinese, malay): '),
read(FoodType),
food(FoodType).
此处显示的答案:Using STI path with same controller
form_for @list.becomes(List)
方法被定义为主要用于解决像.becomes
一样的STI问题。
form_for
信息:http://apidock.com/rails/ActiveRecord/Base/becomes
超级迟到的反应,但这是我能找到的最佳答案,对我来说效果很好。希望这对某人有所帮助。干杯!
答案 6 :(得分:5)
好的,我在Rails的这个领域遇到了很多挫折,并且采用了以下方法,也许这会对其他人有所帮助。
首先请注意,网络上方和周围的许多解决方案建议在客户端提供的参数上使用constantize。这是一个已知的DoS攻击向量,因为Ruby不会垃圾收集符号,从而允许攻击者创建任意符号并消耗可用内存。
我已经实现了下面的方法,它支持模型子类的实例化,并且从上面的兼容问题是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'(在澳大利亚)。 / p>
我也使用路由映射,如上所述,设置类型:
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
很高兴听到别人的经历和改进。
答案 7 :(得分:4)
我最近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方法中遇到的其他问题。
答案 8 :(得分:3)
我找到的最干净的解决方案是将以下内容添加到基类中:
def self.inherited(subclass)
super
def subclass.model_name
super.tap do |name|
route_key = base_class.name.underscore
name.instance_variable_set(:@singular_route_key, route_key)
name.instance_variable_set(:@route_key, route_key.pluralize)
end
end
end
它适用于所有子类,并且比覆盖整个模型名称对象安全得多。通过仅定位路由键,我们解决了路由问题,而不会破坏I18n或冒着因覆盖Rails定义的模型名称而引起的任何潜在副作用的风险。
答案 9 :(得分:2)
如果您没有嵌套路线,可以试试这个:
resources :employee, path: :person, controller: :person
或者你可以采用另一种方式并使用如此处所述的OOP魔法:https://coderwall.com/p/yijmuq
在第二种方式中,您可以为所有嵌套模型制作类似的帮助器。
答案 10 :(得分:2)
这是一种安全清洁的方式,可以在我们使用的表单和整个应用程序中使用它。
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|
希望这有帮助。
答案 11 :(得分:1)
如果我考虑这样的STI继承:
class AModel < ActiveRecord::Base ; end
class BModel < AModel ; end
class CModel < AModel ; end
class DModel < AModel ; end
class EModel < AModel ; end
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
因此我甚至可以轻松选择我想使用默认模型的模型,而这甚至没有触及子类定义。非常干燥。
答案 12 :(得分:1)
这种方法对我有用(在基类中定义此方法):
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
答案 13 :(得分:1)
您可以创建返回虚拟Parent对象的方法,以便路由purpouse
class Person < ActiveRecord::Base
def routing_object
Person.new(id: id)
end
end
然后只需调用form_for @ employee.routing_object 没有类型将返回Person类对象
答案 14 :(得分:0)
覆盖model_name
似乎很危险。使用.becomes
似乎是更安全的选择。
一个问题是在您不知道要处理的是哪种模型(进而是基本模型)的情况下。
我只想分享在这种情况下可以使用的信息:
foo.becomes(foo.class.base_class)
为便于使用,我将此方法添加到了ApplicationRecord
:
def becomes_base
becomes(self.class.base_class)
end
在一些路由助手方法中添加.becomes_base
对我来说似乎并不重要。
答案 15 :(得分:0)
我赞成使用 a b
0 [1, 2] 2
1 [2, 4] 3
或 a b
0 [1, None] 1
1 [None] 4
2 [1, None, 2] 5
根据资源,任何命名空间等动态生成路由。
https://api.rubyonrails.org/classes/ActionDispatch/Routing/PolymorphicRoutes.html
https://api.rubyonrails.org/classes/ActionDispatch/Routing/UrlFor.html
PolymorphicRoutes
具有url_for
名称空间
polymorphic_url([:admin, @article, @comment])
# => admin_article_comment_url(@article, @comment)
edit_polymorphic_path(@post)
# => "/posts/1/edit"
答案 16 :(得分:0)
在@ prathan-thananart答案之后,对于多个STI类,您可以将以下内容添加到父模型中->
class Contact < ActiveRecord::Base
def self.model_name
ActiveModel::Name.new(self, nil, 'Contact')
end
end
这将使每个包含联系人数据的表单以params[:contact]
而不是params[:contact_person]
,params[:contact_whatever]
的形式发送参数。
答案 17 :(得分:-6)
hackish,但只是解决方案列表中的另一个。
class Parent < ActiveRecord::Base; end
Class Child < Parent
def class
Parent
end
end
适用于rails 2.x和3.x