在轨道中处理 STI 子类路由的最佳实践

我的 Rails 视图和控制器充斥着 redirect_tolink_toform_for方法调用。有时 link_toredirect_to在它们链接的路径中是显式的(例如 link_to 'New Person', new_person_path) ,但很多时候路径是隐式的(例如 link_to 'Show', person)。

我将一些单表继承(STI)添加到我的模型中(比如 Employee < Person) ,所有这些方法都会因为子类的一个实例而中断(比如 Employee) ; 当 ails 执行 link_to @person时,它会因为 undefined method employee_path' for #<#<Class:0x000001022bcd40>:0x0000010226d038>而出错。Rails 正在寻找一个由对象的类名定义的路由,这个对象是 employee。没有定义这些雇员路由,也没有雇员控制器,因此也没有定义操作。

以前也有人问过这个问题:

  1. StackOverflow 堆栈溢出中,答案是编辑整个代码库中 link _ to etc 的每个实例,并显式地声明路径
  2. StackOverflow 堆栈溢出上,有两个人建议使用 routes.rb将子类资源映射到父类(map.resources :employees, :controller => 'people')。同一个 SO 问题的最佳答案建议使用 .becomes对代码库中的每个实例对象进行类型强制转换
  3. 还有一个在 StackOverflow 堆栈溢出,顶部的答案是方式重复自己的阵营,并建议创建每个子类重复脚手架。
  4. 这里又出现了同样的问题,顶部的答案似乎是错误的(Rails Magic Just Works!)
  5. 在网络的其他地方,我找到了 这篇博文,F2Andy 建议在代码的任何地方编辑路径。
  6. 在逻辑现实设计的博客文章 单表继承和 RESTful 路由中,建议将子类的资源映射到超类控制器,如上面第2个答案所示。
  7. Alex Reisner 有一篇文章 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. 重写 Rail 内部方法,使类相互撒谎
  3. 编辑代码中隐式或显式调用对象操作路径的每个实例,更改路径或对对象进行类型强制转换。

这么多互相矛盾的答案,我需要一个裁决。在我看来,似乎没有好的答案。这是轨道设计的失败吗?如果是这样的话,这是一个可以修复的 bug 吗?或者如果没有,那么我希望有人可以直接告诉我这个问题,告诉我每个选项的利弊(或者解释为什么这不是一个选项) ,哪一个是正确的答案,以及为什么。还是有一个我在网上找不到的正确答案?

30454 次浏览

我也有同样的问题。在使用 STI 之后,form_for方法发送到了错误的子网址。

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进行提交之后,它似乎对我有效。

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

class Parent < ActiveRecord::Base; end


Class Child < Parent
def class
Parent
end
end

在 Rails2.x 和3.x 上工作

我最近 记录在案我的尝试得到一个稳定的 STI 模式在 Rails 3.0应用程序工作。下面是 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 方法中遇到的许多其他问题。

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

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

现在,url_for @person将如期映射到 contact_path

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

我建议您看一下: https://stackoverflow.com/a/605172/445908,使用这种方法将使您能够使用“ form _ for”。

ActiveRecord::Base#becomes

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

如果我考虑像这样的性传播疾病遗传:

class AModel < ActiveRecord::Base ; end
class BModel < AModel ; end
class CModel < AModel ; end
class DModel < AModel ; end
class EModel < AModel ; end

在‘ app/model/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

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

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

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

好的,我在 Rails 的这个领域有很多挫折,并且已经达到了下面的方法,也许这会对其他人有所帮助。

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

我已经实现了下面的方法,该方法支持模型子类的实例化,并且可以避免上面的内容化问题。它与 Rails4的功能非常相似,但是也允许多个子类化级别(与 Rails4不同) ,并且可以在 Rails3中工作。

# 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)

在尝试了“开发问题中的子类加载”的各种方法之后,我发现唯一可靠的方法就是在我的模型类中使用“ need _ Depency”。这可以确保类加载在开发中正常工作,并且在生产中不会导致任何问题。在开发过程中,如果没有‘ need _ Depency’AR 就不会知道所有的子类,这会影响为了在类型列上进行匹配而发出的 SQL。除此之外,如果没有“ need _ ”,您也可能同时处于模型类有多个版本的情况中!(例如,当您更改基类或中间类时,子类似乎并不总是重新加载,而是从旧类继承子类)

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


require_dependency 'person'
require_dependency 'organisation'

我也不会像上面建议的那样覆盖 model _ name,因为我使用的是 I18n,不同子类的属性需要不同的字符串,例如: tax _ Identity 变成了 Organisation 的‘ ABN’,而 Person 的‘ TFN’(在澳大利亚)。

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

resources :person, :controller => 'contacts', :defaults => { 'contact' => { 'type' => Person.sti_name } }
resources :organisation, :controller => 'contacts', :defaults => { 'contact' => { 'type' => Organisation.sti_name } }

除了路由映射之外,我还使用 Heritage itedResources 和 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 基础中,我将 Heritage itedResource 的 resource _ request _ name 作为视图的助手方法公开:

helper_method :resource_request_name

如果您没有使用 Heritage itedResources,那么可以在“ 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

总是乐于听到别人的经验和进步。

如果您没有嵌套的路由,您可以尝试这样做:

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

或者您可以走另一条路,使用一些像这里描述的面向对象的魔法: https://coderwall.com/p/yijmuq

第二种方法是为所有嵌套模型制作类似的帮助程序。

在路线中使用 类型:

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

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

这里有一个安全干净的方法,让它在表单和整个您的应用程序,我们使用的工作。

resources :districts
resources :district_counties, controller: 'districts', type: 'County'
resources :district_cities, controller: 'districts', type: 'City'

然后我有在我的形式。这个增加的一块是作为: : 区。

= form_for(@district, as: :district, html: { class: "form-horizontal",         role: "form" }) do |f|

希望这个能帮上忙。

您可以创建返回虚拟 Parent 对象的方法,以便进行路由

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

然后简单地调用 form _ for@employee. routing _ object 它不带类型将返回 Person 类对象

我在这个问题上也遇到了麻烦,在一个类似于我们的问题上得到了这个答案。这招对我很管用。

form_for @list.becomes(List)

答案显示在这里: 使用同一控制器的 STI 路径

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

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

超级迟的反应,但这是我能找到的最好的答案,它工作得很好。希望这对某人有帮助。干杯!

在@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]

我发现的最干净的解决方案是向基类添加以下内容:

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 定义的模型名而产生任何潜在的副作用。

我赞成使用 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

覆盖 model_name看起来很危险。使用 .becomes看起来是更安全的选择。

其中一个问题是,您不知道正在处理的是什么模型(因此也不知道基本模型)。

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

foo.becomes(foo.class.base_class)

为了方便使用,我将这个方法添加到我的 ApplicationRecord:

def becomes_base
becomes(self.class.base_class)
end

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