在rails中处理STI子类的路由的最佳实践

我的Rails视图和控制器乱七八糟的是redirect_tolink_toform_for方法调用。 有时link_toredirect_to在它们链接的path中是明确的(例如link_to 'New Person', new_person_path ),但是path隐含很多次(例如link_to 'Show', person )。

我添加了一些单表inheritance(STI)到我的模型(称为Employee < Person ),并且所有这些方法都会为子类的一个实例(称为Employee )分解。 当rails执行link_to @personundefined method employee_path' for #<#<Class:0x000001022bcd40>:0x0000010226d038>undefined method employee_path' for #<#<Class:0x000001022bcd40>:0x0000010226d038>发生错误。 Rails正在寻找一个由对象的类名定义的路由,这是员工。 这些员工路线没有定义,也没有员工控制器,所以这些操作也没有定义。

这个问题之前已经被问到:

  1. 在StackOverflow中 ,答案是在整个代码库中编辑link_to等的每个实例,并显式声明path
  2. 在StackOverflow上 ,两个人build议使用routes.rb将子类资源映射到父类( map.resources :employees, :controller => 'people' )。 在同样的SO问题中最好的答案build议使用.becomestypes转换代码库中的每个实例对象
  3. 另一个在StackOverflow中 ,最重要的答案是在Do Repeat Yourself阵营中,并build议为每个子类创build重复的脚手架。
  4. 在这里 ,同样的问题再次出现在最上面的答案似乎是错的(Rails magic Just Works!)
  5. 在networking上的其他地方,我发现F2Andybuild议在代码中的任何地方编辑path。
  6. 在逻辑现实devise的单表inheritance和RESTful路由的博客文章中,build议将子类的资源映射到超类控制器,如上面的第二个回答中所述。
  7. Alex Reisner 在Rails中发表了单表inheritance(Single Table Inheritance) ,他主张反对将子类的资源映射到routes.rb的父类,因为它只捕获来自link_toredirect_to路由routes.rb ,而不是从form_for 。 所以他build议在父类中添加一个方法来让子类对他们的类进行说谎。 听起来不错,但他的方法给了我错误undefined local variable or method `child' for #

所以看起来最优雅和最有共识的答案(但并不是那么优雅,也没有那么多的共识),就是把资源添加到你的routes.rb 。 除了这个不适用于form_for 。 我需要一些清晰的! 提炼上面的select,我的select是

  1. 将这个子类的资源映射到routes.rb类的控制器(并且希望我不需要在任何子类上调用form_for)
  2. 覆盖导轨的内部方法,使类彼此躺在
  3. 编辑代码中隐藏或显式调用对象操作的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上工作