181

我的 Rails 视图和控制器充斥着redirect_tolink_toform_for方法调用。有时link_toredirect_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 正在寻找由对象的类名定义的路由,即employee。这些员工路线没有定义,也没有员工控制器,因此也没有定义操作。

之前有人问过这个问题:

  1. StackOverflow,答案是在整个代码库中编辑 link_to 等的每个实例,并明确说明路径
  2. 再次在StackOverflow上,有两个人建议使用routes.rb将子类资源映射到父类(map.resources :employees, :controller => 'people')。同一个 SO 问题中的最佳答案建议使用类型转换代码库中的每个实例对象.becomes
  3. StackOverflow上的另一个问题,最佳答案是 Do Repeat Yourself 阵营,并建议为每个子类创建重复的脚手架。
  4. 在SO再次出现同样的问题,其中最佳答案似乎是错误的(Rails magic Just Works!)
  5. 在网络的其他地方,我发现了这篇博文,F2Andy 建议在代码中各处的路径中进行编辑。
  6. 在 Logical Reality Design的博客文章Single Table Inheritance and RESTful Routes中,建议将子类的资源映射到超类控制器,如上面的 SO 答案 2 中所示。
  7. Alex Reisner 在 Rails 中有一篇文章Single Table Inheritance,其中他主张反对将子类的资源映射到父类routes.rb,因为这只能捕获来自link_toand的路由中断redirect_to,而不是来自form_for. 所以他建议改为向父类添加一个方法,让子类对他们的类撒谎。听起来不错,但他的方法给了我错误undefined local variable or method `child' for #

所以看起来最优雅和最一致的答案(但它不是那么优雅,也不那么一致),就是将资源添加到你的routes.rb. 除非这不适用于form_for. 我需要一些澄清!为了提炼上面的选择,我的选择是

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

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

4

18 回答 18

143

这是我能想出的最简单的解决方案,副作用最小。

class Person < Contact
  def self.model_name
    Contact.model_name
  end
end

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

它是如何工作的: URL 助手依赖于YourModel.model_name反映模型并生成(在许多事情中)单数/复数路由键。这里Person基本上是说我就像Contact老兄,问他

于 2012-02-27T10:31:39.313 回答
47

我有同样的问题。使用 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

于 2011-02-23T07:32:14.780 回答
35

我建议您看一下:https ://stackoverflow.com/a/605172/445908 ,使用此方法将使您能够使用“form_for”。

ActiveRecord::Base#becomes
于 2012-04-17T00:18:09.467 回答
18

在路线中使用类型:

resources :employee, controller: 'person', type: 'Employee' 

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

于 2014-05-15T08:29:36.310 回答
16

遵循@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。

于 2012-10-30T00:59:33.483 回答
16

我也遇到了这个问题,并在与我们类似的问题上得到了这个答案。它对我有用。

form_for @list.becomes(List)

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

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

.becomes信息在这里:http ://apidock.com/rails/ActiveRecord/Base/becomes

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

于 2016-12-31T13:25:11.300 回答
5

好的,我在 Rails 的这个领域有很多挫折,并得出了以下方法,也许这对其他人有帮助。

首先请注意,网络之上和周围的许多解决方案都建议在客户端提供的参数上使用常量化。这是一个已知的 DoS 攻击向量,因为 Ruby 不会垃圾收集符号,因此允许攻击者创建任意符号并消耗可用内存。

我已经实现了下面的方法,它支持模型子类的实例化,并且对于上面的 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 将不会知道所有子类,这会影响为匹配类型列而发出的 SQL。此外,如果没有“require_dependency”,您还可能会同时遇到多个模型类版本的情况!(例如,当您更改基类或中间类时,可能会发生这种情况,子类似乎并不总是重新加载,而是从旧类继承下来)

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

require_dependency 'person'
require_dependency 'organisation'

我也没有按照上面的建议覆盖 model_name,因为我使用 I18n 并且需要为不同子类的属性使用不同的字符串,例如:tax_identifier 变成了 Organization 的“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

总是很高兴听到别人的经验和改进。

于 2013-06-12T02:52:20.530 回答
4

我最近记录了我在 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 方法时遇到的许多其他问题。

于 2012-02-01T09:04:56.027 回答
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 定义的模型名称而导致的任何潜在副作用的风险。

于 2020-03-19T07:52:31.273 回答
2

如果你没有嵌套路由,你可以试试这个:

resources :employee, path: :person, controller: :person

或者您可以采用另一种方式并使用一些 OOP 魔法,如下所述:https ://coderwall.com/p/yijmuq

在第二种方式中,您可以为所有嵌套模型制作类似的助手。

于 2013-11-20T17:33:29.890 回答
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|

希望这可以帮助。

于 2016-06-21T17:16:59.650 回答
2

在@prathan-thananart answer之后,对于多个 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]

于 2019-04-04T15:01:21.407 回答
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
于 2013-05-21T14:57:28.750 回答
1

如果我考虑这样的 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

因此,我什至可以轻松地选择要使用默认模型的模型,甚至无需触及子类定义。非常干燥。

于 2013-04-27T09:35:30.663 回答
1

您可以创建返回虚拟父对象用于路由目的的方法

class Person < ActiveRecord::Base      
  def routing_object
    Person.new(id: id)
  end
end

然后只需调用 form_for @employee.routing_object 没有类型将返回 Person 类对象

于 2016-08-26T00:35:11.287 回答
0

压倒一切model_name似乎很危险。使用.becomes似乎是更安全的选择。

一个问题是您不知道您正在处理的模型(以及基本模型)。

我只是想分享一下,在这种情况下,可以使用:

foo.becomes(foo.class.base_class)

为了便于使用,我已将此方法添加到我的ApplicationRecord

def becomes_base
  becomes(self.class.base_class)
end

添加.becomes_base一些路由辅助方法对我来说似乎没什么大不了的。

于 2020-09-04T08:33:35.490 回答
0

我赞成使用PolymorphicRoutesurl_for基于资源、任何命名空间等动态生成路由。

https://api.rubyonrails.org/classes/ActionDispatch/Routing/PolymorphicRoutes.html

https://api.rubyonrails.org/classes/ActionDispatch/Routing/UrlFor.html

polymorphic_url([:admin, @article, @comment])
# => admin_article_comment_url(@article, @comment)

edit_polymorphic_path(@post) 
# => "/posts/1/edit"

admin命名空间

url_for([:admin, Role])
# => "admin/roles" # index

url_for([:admin, Role, action: :new])
# => "admin/roles/new" # new

url_for([:admin, @role])
# => "admin/roles/1" # show; for destroy, use link "method: :delete"

url_for([:edit, :admin, @role])
# => "admin/roles/1/edit" # edit
于 2020-09-03T03:41:37.267 回答
-6

hackish,但只是解决方案列表中的另一个。

class Parent < ActiveRecord::Base; end

Class Child < Parent
  def class
    Parent
  end
end

适用于 rails 2.x 和 3.x

于 2012-01-23T13:52:53.913 回答