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

问题描述 投票:168回答:15

我的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等的每个实例,并明确说明路径
  2. 再次在StackOverflow上,有两个人建议使用routes.rb将子类资源映射到父类(map.resources :employees, :controller => 'people')。在同一个SO问题中的最佳答案建议使用.becomes在代码库中输入每个实例对象
  3. 还有一个在StackOverflow,最重要的答案是在Do Repeat Yourself阵营,并建议为每个子类创建重复的脚手架。
  4. Here's在SO处再次提出同样的问题,其中最重要的答案似乎是错误的(Rails magic Just Works!)
  5. 在网络的其他地方,我找到了this blog post,其中F2Andy建议在代码中的任何地方编辑路径。
  6. 在Logical Reality Design的博客文章Single Table Inheritance and RESTful Routes上,建议将子类的资源映射到超类控制器,如上面的SO答案2中所示。
  7. Alex Reisner有一个帖子Single Table Inheritance in 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设计中的失败吗?如果是这样,它是否可以修复?或者如果没有,那么我希望有人可以让我直截了当,让我了解每个选项的优点和缺点(或解释为什么不是一个选项),哪一个是正确的答案,以及为什么。或者是否有一个我在网上找不到的正确答案?

ruby-on-rails ruby single-table-inheritance
15个回答
130
投票

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

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

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

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


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|

希望这可以帮助。


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

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


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

1
投票

您可以创建返回虚拟父对象的方法以进行灌浆

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

然后简单地调用form_for @ employee.routing_object,其中没有类型将返回Person类对象


0
投票

关注@ prathan-thananart回答,对于多个STI类,您可以将以下内容添加到父模型中 - >

class Contact < ActiveRecord::Base
  def self.model_name
    ActiveModel::Name.new(self, nil, 'Contact')
  end
end

这将使每个表格与联系人数据发送params为params[:contact]而不是params[:contact_person]params[:contact_whatever]


-6
投票

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

class Parent < ActiveRecord::Base; end

Class Child < Parent
  def class
    Parent
  end
end

适用于rails 2.x和3.x.


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提交后,它似乎对我有用。


31
投票

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

ActiveRecord::Base#becomes

17
投票

在路线中使用类型:

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

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


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。


11
投票

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

form_for @list.becomes(List)

答案显示在这里:Using STI path with same controller

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

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

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


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)

在为开发问题中的“子类加载”尝试了各种方法之后,我发现唯一可行的方法就是在我的模型类中使用'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'(在澳大利亚)。

我也使用路由映射,如上所述,设置类型:

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

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


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方法中遇到的其他问题。


2
投票

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

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

或者你可以采用另一种方式并使用如下所述的OOP魔术:https://coderwall.com/p/yijmuq

在第二种方式中,您可以为所有嵌套模型创建类似的帮助程序。

© www.soinside.com 2019 - 2024. All rights reserved.