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

时间:2010-12-22 07:45:33

标签: ruby-on-rails ruby single-table-inheritance

My 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正在寻找由对象的类名定义的路由,即雇员。这些员工路线未定义,并且没有员工控制器,因此也未定义操作。

之前已经问过这个问题:

  1. StackOverflow,答案是编辑整个代码库中link_to等的每个实例,并明确说明路径<​​/ li>
  2. 再次在StackOverflow上,有两个人建议使用routes.rb将子类资源映射到父类(map.resources :employees, :controller => 'people')。在同一个SO问题中的最佳答案建议使用.becomes
  3. 在代码库中对每个实例对象进行类型转换
  4. StackOverflow的另一个,最重要的答案是在Do Repeat Yourself阵营,并建议为每个子类创建重复的脚手架。
  5. Here's在SO处再次提出同样的问题,其中最重要的答案似乎是错误的(Rails magic Just Works!)
  6. 在网络的其他地方,我找到了this blog post,其中F2Andy建议在代码中的所有路径中进行编辑。
  7. 在Logical Reality Design的博客文章Single Table Inheritance and RESTful Routes上,建议将子类的资源映射到超类控制器,如上面的答案编号2所示。
  8. Alex Reisner有一个帖子Single Table Inheritance in Rails,他主张反对将子类的资源映射到routes.rb中的父类,因为它只捕获来自link_to的路由断点和redirect_to,但不是来自form_for。所以他建议在父类中添加一个方法,让子类对它们的类​​撒谎。听起来不错,但他的方法给了我错误undefined local variable or method `child' for #
  9. 所以答案似乎最优雅且最具共识(但并非所有 优雅, 很多共识),是为您添加资源routes.rb。除此之外不适用于form_for。我需要一些清晰度!为了提炼上述选择,我的选择是

    1. 将子类的资源映射到routes.rb中超类的控制器(并希望我不需要在任何子类上调用form_for)
    2. 覆盖rails内部方法以使这些类相互谎言
    3. 编辑代码中的每个实例,其中隐式或显式调用对象操作的路径,更改路径或压缩对象。
    4. 由于所有这些相互矛盾的答案,我需要一个裁决。在我看来,似乎没有好的答案。这是rails设计中的失败吗?如果是这样,它是否可以修复?或者如果没有,那么我希望有人可以让我直截了当,让我了解每个选项的优缺点(或解释为什么这不是一个选项),哪一个是正确的答案,以及为什么。或者是否有正确的答案,我没有在网上找到?

18 个答案:

答案 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
在'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

因此我甚至可以轻松选择我想使用默认模型的模型,而这甚至没有触及子类定义。非常干燥。

答案 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