在rails中处理STI子类的路由的最佳实践
我的Rails视图和控制器乱七八糟的是redirect_to
, link_to
和form_for
方法调用。 有时link_to
和redirect_to
在它们链接的path中是明确的(例如link_to 'New Person', new_person_path
),但是path隐含很多次(例如link_to 'Show', person
)。
我添加了一些单表inheritance(STI)到我的模型(称为Employee < Person
),并且所有这些方法都会为子类的一个实例(称为Employee
)分解。 当rails执行link_to @person
, undefined method employee_path' for #<#<Class:0x000001022bcd40>:0x0000010226d038>
, undefined method employee_path' for #<#<Class:0x000001022bcd40>:0x0000010226d038>
发生错误。 Rails正在寻找一个由对象的类名定义的路由,这是员工。 这些员工路线没有定义,也没有员工控制器,所以这些操作也没有定义。
这个问题之前已经被问到:
- 在StackOverflow中 ,答案是在整个代码库中编辑link_to等的每个实例,并显式声明path
- 在StackOverflow上 ,两个人build议使用
routes.rb
将子类资源映射到父类(map.resources :employees, :controller => 'people'
)。 在同样的SO问题中最好的答案build议使用.becomes
types转换代码库中的每个实例对象 - 另一个在StackOverflow中 ,最重要的答案是在Do Repeat Yourself阵营中,并build议为每个子类创build重复的脚手架。
- 在这里 ,同样的问题再次出现在最上面的答案似乎是错的(Rails magic Just Works!)
- 在networking上的其他地方,我发现F2Andybuild议在代码中的任何地方编辑path。
- 在逻辑现实devise的单表inheritance和RESTful路由的博客文章中,build议将子类的资源映射到超类控制器,如上面的第二个回答中所述。
- Alex Reisner 在Rails中发表了单表inheritance(Single Table Inheritance) ,他主张反对将子类的资源映射到
routes.rb
的父类,因为它只捕获来自link_to
和redirect_to
路由routes.rb
,而不是从form_for
。 所以他build议在父类中添加一个方法来让子类对他们的类进行说谎。 听起来不错,但他的方法给了我错误undefined local variable or method `child' for #
。
所以看起来最优雅和最有共识的答案(但并不是那么优雅,也没有那么多的共识),就是把资源添加到你的routes.rb
。 除了这个不适用于form_for
。 我需要一些清晰的! 提炼上面的select,我的select是
- 将这个子类的资源映射到
routes.rb
类的控制器(并且希望我不需要在任何子类上调用form_for) - 覆盖导轨的内部方法,使类彼此躺在
- 编辑代码中隐藏或显式调用对象操作的path的每个实例,可以更改path或types转换对象。
所有这些相互矛盾的答案,我需要一个裁决。 在我看来,似乎没有好的答案。 这是轨道devise失败吗? 如果是这样,这是一个可能得到修复的错误? 如果没有,那么我希望有人能够直接对我做出解释,通过每个选项的利弊(或解释为什么这不是一个选项),哪一个是正确的答案,以及为什么。 还是有没有一个正确的答案,我没有在网上find?
这是我能够以最小的副作用提出的最简单的解决scheme。
class Person < Contact def self.model_name Contact.model_name end end
现在url_for @person
将按照预期映射到contact_path
。
工作原理: URL助手依靠YourModel.model_name
来反思模型并生成(在许多事情中)单数/复数路由键。 这里的Person
基本上是说我就像Contact
伙计,问他 。
我有同样的问题。 使用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| %>
在这种情况下结构实际上是一个build筑物(小孩class)
在用form_for
提交之后,它似乎适用于我。
我build议你看看: https : //stackoverflow.com/a/605172/445908 ,使用这种方法将使您可以使用“form_for”。
ActiveRecord::Base#becomes
在路线中使用types :
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。
好吧,我在Rails的这个领域里有很多的挫折,并且已经到了下面的方法,也许这会帮助别人。
首先要注意的是,networking上方和周围的一些解决schemebuild议在客户端提供的参数上使用constantize。 这是一个已知的DoS攻击媒介,因为Ruby不会垃圾收集符号,从而允许攻击者创build任意符号并消耗可用内存。
我已经实现了下面的方法支持模型子类的实例化,并从上面的contantize问题是安全的。 它和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不会知道所有的子类,这会影响在types列上匹配的SQL。 另外,如果没有“require_dependency”,你也可以同时在多个版本的模型类的情况下结束! (例如,当您更改基本类或中间类时,可能会发生这种情况,子类似乎不会总是重新加载,而是从旧类inheritance而来)
# contact.rb class Contact < ActiveRecord::Base acts_as_castable end require_dependency 'person' require_dependency 'organisation'
我也没有按照上面的build议重写model_name,因为我使用了I18n,并且不同子类的属性需要不同的string,例如:tax_identifier变成了组织的“ABN”,Person变成了“TFN”(在澳大利亚)。
我也使用路由映射,如上所示,设置types:
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
总是乐于听到别人的经验和改进。
我也遇到了这个问题,并在类似于我们的问题上得到了这个答案。 它为我工作。
form_for @ list.becomes(List)
此处显示的答案: 使用STIpath和相同的控制器
.becomes声明被定义为主要用于解决您的表单等问题。
.becomes info here: http ://apidock.com/rails/ActiveRecord/Base/becomes
超晚的反应,但这是我能find的最好的答案,它对我来说效果很好。 希望这有助于一个人。 干杯!
我最近logging了我在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
在第二种方法中,您可以为所有嵌套模型制作类似的助手。
这是一个安全的清洁的方式,让它在表单中以及我们使用的整个应用程序中工作。
resources :districts resources :district_counties, controller: 'districts', type: 'County' resources :district_cities, controller: 'districts', type: 'City'
然后,我有我的forms。 为此添加的片段是as::区域。
= form_for(@district, as: :district, html: { class: "form-horizontal", role: "form" }) do |f|
希望这可以帮助。
如果我考虑这样的STIinheritance:
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
因此,我甚至可以轻松地select使用默认模型的哪个模型,甚至不需要触及子类定义。 非常干燥。
这种方式对我很好(在基类中定义此方法):
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
您可以创build返回虚拟父对象路由purpouse的方法
class Person < ActiveRecord::Base def routing_object Person.new(id: id) end end
然后只需调用form_for @ employee.routing_object而不用types将返回Person类对象
骇人听闻,但只是另一个解决scheme的名单。
class Parent < ActiveRecord::Base; end Class Child < Parent def class Parent end end
在rails 2.x和3.x上工作