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

zig*_*ism 169 ruby ruby-on-rails single-table-inheritance

我的Rails视图和控制器上到处是redirect_to,link_toform_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正在寻找由对象的类名定义的路由,即雇员.这些员工路线未定义,并且没有员工控制器,因此也未定义操作.

之前已经问过这个问题:

  1. StackOverflow,答案是编辑整个代码库中link_to等的每个实例,并明确说明路径
  2. 再次在StackOverflow上,有两个人建议使用routes.rb将子类资源映射到父类(map.resources :employees, :controller => 'people').在同一个SO问题中的最佳答案建议使用代码库中的每个实例对象进行类型转换.becomes
  3. StackOverflow的另一个问题,最重要的答案是在Do Repeat Yourself阵营中,并建议为每个子类创建重复的脚手架.
  4. 这里是再次同样的问题在SO,其中顶部的答案似乎只是错误的(Rails的魔法可以工作了)
  5. 在网络的其他地方,我发现了这篇博文,其中F2Andy建议在代码中的任何地方编辑路径.
  6. 在Logical Reality Design 上的单表继承和RESTful路由的博客文章中,建议将子类的资源映射到超类控制器,如上面的SO答案2中所示.
  7. 亚历克斯·赖斯纳有一个帖子在Rails的单表继承中,他主张对映射子类在父类的资源routes.rb,因为只有抓住从路由断裂link_toredirect_to,但不从form_for.所以他建议在父类中添加一个方法,让子类对它们的类​​撒谎.听起来不错,但他的方法给了我错误undefined local variable or method `child' for #.

所以这似乎是最优雅,拥有最共识(但不是所有的答案优雅的,也不太大的共识),是资源添加到您的routes.rb.除此之外不起作用form_for.我需要一些清晰度!为了提炼上述选择,我的选择是

  1. 将子类的资源映射到超类的控制器routes.rb(并希望我不需要在任何子类上调用form_for)
  2. 覆盖rails内部方法,使类彼此相互依赖
  3. 编辑代码中的每个实例,其中隐式或显式调用对象操作的路径,更改路径或键入对象.

有了所有这些相互矛盾的答案,我需要一个裁决.在我看来,似乎没有好的答案.这是rails设计中的失败吗?如果是这样,它是否可以修复?或者如果没有,那么我希望有人可以让我直截了当,让我了解每个选项的优点和缺点(或解释为什么不是一个选项),哪一个是正确的答案,以及为什么.或者是否有一个我在网上找不到的正确答案?

Pra*_*art 135

这是我能够提出的最简单的解决方案,副作用最小.

class Person < Contact
  def self.model_name
    Contact.model_name
  end
end
Run Code Online (Sandbox Code Playgroud)

现在url_for @person将按contact_path预期映射.

工作原理: URL助手依赖于YourModel.model_name反映模型并生成(在许多方面)单数/复数路由键.这Person基本上是说我就像个Contact老兄,问他.

  • 如果您尝试根据类别为属性设置不同的人名,则会中断i18n. (8认同)
  • **规格.**此外,我们在生产中使用此代码,我可以证明它不会搞砸:1)模型间关系,2)STI模型实例化(通过`build_x` /`create_x`).另一方面,玩魔法的代价是你永远不会100%确定可能会发生什么变化. (6认同)
  • 我正在考虑做同样的事情,但担心#model_name可能会在Rails的其他地方使用,并且这种改变可能会干扰正常运行.有什么想法吗? (4认同)
  • 您可以直接覆盖所需的位,而不是像这样完全覆盖.请参阅https://gist.github.com/sj26/5843855 (4认同)
  • 我完全同意神秘的陌生人@nkassis.这是一个很酷的黑客,但你怎么知道你没有破坏铁轨的内部? (3认同)
  • 我也可能只是添加,而不是使用`Contact.model_name`显式指定父模型,另一个选项是`base_class.model_name`. (2认同)

Jam*_*mes 46

我有同样的问题.使用STI后,该form_for方法发布到错误的子URL.

NoMethodError (undefined method `building_url' for
Run Code Online (Sandbox Code Playgroud)

我最后添加了子类的额外路由并将它们指向相同的控制器

 resources :structures
 resources :buildings, :controller => 'structures'
 resources :bridges, :controller => 'structures'
Run Code Online (Sandbox Code Playgroud)

另外:

<% form_for(@structure, :as => :structure) do |f| %>
Run Code Online (Sandbox Code Playgroud)

在这种情况下,结构实际上是一个建筑物(子类)

在提交后似乎对我有用form_for.

  • 这有效,但在我们的路线中添加了许多不必要的路径.有没有办法以较少侵入的方式做到这一点? (2认同)

Siw*_*申思维 32

我建议你看一下:https://stackoverflow.com/a/605172/445908,使用这种方法可以让你使用"form_for".

ActiveRecord::Base#becomes
Run Code Online (Sandbox Code Playgroud)

  • @lulalala尝试`<%= form_for @ child.becomes(Parent)` (3认同)
  • 我必须明确设置url才能正确呈现from***和***save.`<%= form_for @child,:as =>:child,url:@ child.becomes(Parent)` (2认同)

job*_*wat 17

在路线中使用类型:

resources :employee, controller: 'person', type: 'Employee' 
Run Code Online (Sandbox Code Playgroud)

http://samurails.com/tutorial/single-table-inheritance-with-rails-4-part-2/


elo*_*esp 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
Run Code Online (Sandbox Code Playgroud)

现在url_for @person将按预期映射到contact_path.


小智 11

我也遇到了这个问题的麻烦,并且在类似于我们的问题上得到了这个答案.它对我有用.

form_for @list.becomes(List)
Run Code Online (Sandbox Code Playgroud)

此处显示的答案:在同一控制器上使用STI路径

.becomes方法被定义为主要用于解决像您这样的STI问题form_for.

.becomesinfo:http://apidock.com/rails/ActiveRecord/Base/becomes

超级迟到的回应,但这是我能找到的最佳答案,对我来说效果很好.希望这对某人有所帮助.干杯!


And*_*ing 5

好吧,我在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)
Run Code Online (Sandbox Code Playgroud)

在为开发问题中的"子类加载"尝试了各种方法之后,我发现唯一可行的方法就是在我的模型类中使用'require_dependency'.这可确保类加载在开发中正常工作,并且不会导致生产中出现问题.在开发中,没有'require_dependency'AR不会知道所有子类,这会影响为类型列匹配而发出的SQL.此外,如果没有'require_dependency',您最终也会遇到具有多个版本的模型类的情况!(例如,当您更改基类或中间类时,可能会发生这种情况,子类似乎并不总是重新加载,而是从旧类继承子类)

# contact.rb
class Contact < ActiveRecord::Base
  acts_as_castable
end

require_dependency 'person'
require_dependency 'organisation'
Run Code Online (Sandbox Code Playgroud)

我也没有按照上面的建议覆盖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 } }
Run Code Online (Sandbox Code Playgroud)

除了路由映射,我正在使用InheritedResources和SimpleForm,我使用以下通用表单包装器来执行新操作:

simple_form_for resource, as: resource_request_name, url: collection_url,
      html: { class: controller_name, multipart: true }
Run Code Online (Sandbox Code Playgroud)

...以及编辑操作:

simple_form_for resource, as: resource_request_name, url: resource_url,
      html: { class: controller_name, multipart: true }
Run Code Online (Sandbox Code Playgroud)

为了使这项工作,在我的基础ResourceContoller中,我将InheritedResource的resource_request_name公开为视图的辅助方法:

helper_method :resource_request_name 
Run Code Online (Sandbox Code Playgroud)

如果您没有使用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
Run Code Online (Sandbox Code Playgroud)

总是乐于听到别人的经历和改进.