首页 文章

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

提问于
浏览
167

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

之前已经问过这个问题:

  • StackOverflow,答案是在整个代码库中编辑link_to等的每个实例,并明确说明路径

  • 再次_1111093_,有两个人建议使用 routes.rb 将子类资源映射到父类( map.resources :employees, :controller => 'people' ) . 在同一个SO问题中的最佳答案建议使用 .becomes 在代码库中对每个实例对象进行类型转换

  • 还有一个在StackOverflow,最重要的答案是在Do Repeat Yourself阵营,并建议为每个子类创建重复的脚手架 .
    在SO处再次提出同样的问题,其中最佳答案似乎是错误的(Rails magic Just Works!)

  • 在网络的其他地方,我找到了this blog post,其中F2Andy建议在代码中的任何地方编辑路径 .

  • 在Logical Reality Design的博客文章Single Table Inheritance and RESTful Routes上,建议将子类的资源映射到超类控制器,如上面的答案编号2所示 .

  • Alex Reisner有一个帖子Single Table Inheritance in Rails,他主张反对将子类的资源映射到 routes.rb 中的父类,因为它只能从 link_toredirect_to 捕获路由断点,而不是从 form_for 捕获 . 所以他建议在父类中添加一个方法,让子类对它们的类撒谎 . 听起来不错,但他的方法给了我错误 undefined local variable or methodchild' for #` .

因此,似乎最优雅且最具共识的答案(但并非所有优雅,也不是那么多共识),是为您的 routes.rb 添加资源 . 除此之外不适用于 form_for . 我需要一些清晰度!为了提炼上述选择,我的选择是

  • 将子类的资源映射到 routes.rb 中超类的控制器(并希望我不需要在任何子类上调用form_for)

  • 覆盖rails内部方法,使类彼此相互依赖

  • 编辑代码中的每个实例,其中隐式或显式调用对象操作的路径,更改路径或键入对象 .

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

14 回答

  • 2

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

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

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

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

  • 30

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

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

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

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

  • 1

    我有同样的问题 . 使用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 提交后,它似乎对我有用 .

  • 1

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

    ActiveRecord::Base#becomes
    
  • 5

    在路线中使用类型:

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

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

  • 1

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

  • 13

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

    form_for @list.becomes(List)
    

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

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

    .becomes info here:http://apidock.com/rails/ActiveRecord/Base/becomes

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

  • 130

    好吧,我在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
    

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

  • 17

    我最近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 :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|
    

    希望这可以帮助 .

  • 45

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

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

  • 11

    这种方式对我有用(在基类中定义此方法):

    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
    
  • 4

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

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

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

  • -6

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

    class Parent < ActiveRecord::Base; end
    
    Class Child < Parent
      def class
        Parent
      end
    end
    

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

相关问题