Ruby-on-rails 处理 rails 中 STI 子类的路由的最佳实践
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/4507149/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
Best practices to handle routes for STI subclasses in rails
提问by ziggurism
My Rails views and controllers are littered with redirect_to, link_to, and form_formethod calls. Sometimes link_toand redirect_toare explicit in the paths they're linking (e.g. link_to 'New Person', new_person_path), but many times the paths are implicit (e.g. link_to 'Show', person).
我的Rails视图和控制器上到处是redirect_to,link_to和form_for方法调用。有时link_to并且redirect_to在它们链接的路径中是显式的(例如link_to 'New Person', new_person_path),但很多时候路径是隐式的(例如link_to 'Show', person)。
I add some single table inheritance (STI) to my model (say Employee < Person), and all of these methods break for an instance of the subclass (say Employee); when rails executes link_to @person, it errors with undefined method employee_path' for #<#<Class:0x000001022bcd40>:0x0000010226d038>. Rails is looking for a route defined by the class name of the object, which is employee. These employee routes are not defined, and there is no employee controller so the actions aren't defined either.
我将一些单表继承 (STI) 添加到我的模型中(例如Employee < Person),并且所有这些方法都因子类的实例(例如)而中断Employee;当 rails 执行时link_to @person,它会出错undefined method employee_path' for #<#<Class:0x000001022bcd40>:0x0000010226d038>。Rails 正在寻找由对象的类名定义的路由,即员工。这些员工路线未定义,也没有员工控制器,因此也未定义操作。
This question has been asked before:
这个问题以前有人问过:
- At StackOverflow, the answer is to edit every instance of link_to etc in your entire codebase, and state the path explicitly
- On StackOverflowagain, two people suggest using
routes.rbto map the subclass resources to the parent class (map.resources :employees, :controller => 'people'). The top answer in that same SO question suggests type-casting every instance object in the codebase using.becomes - Yet another one at StackOverflow, the top answer is way in the Do Repeat Yourself camp, and suggests creating duplicate scaffolding for every subclass.
- Here'sthe same question again at SO, where the top answer seems to just be wrong (Rails magic Just Works!)
- Elsewhere on the web, I found this blog postwhere F2Andy recommends editing in the path everywhere in the code.
- On the blog post Single Table Inheritance and RESTful Routesat Logical Reality Design, it is recommended to map the resources for the subclass to the superclass controller, as in SO answer number 2 above.
- Alex Reisner has a post Single Table Inheritance in Rails, in which he advocates against mapping the resources of the child classes to the parent class in
routes.rb, since that only catches routing breakages fromlink_toandredirect_to, but not fromform_for. So he recommends instead adding a method to the parent class to get the subclasses to lie about their class. Sounds good, but his method gave me the errorundefined local variable or method `child' for #.
- 在StackOverflow,答案是在整个代码库中编辑 link_to 等的每个实例,并明确说明路径
- 再次在StackOverflow上,有两个人建议使用
routes.rb将子类资源映射到父类(map.resources :employees, :controller => 'people')。同一个 SO 问题中的最佳答案建议使用类型转换代码库中的每个实例对象.becomes - StackOverflow 上的另一个,最佳答案是 Do Repeat Yourself 阵营中的方式,并建议为每个子类创建重复的脚手架。
- 这里又是同样的问题,上面的答案似乎是错误的(Rails magic Just Works!)
- 在网络的其他地方,我发现了这篇博客文章,其中 F2Andy 建议在代码中的所有路径中进行编辑。
- 在 Logical Reality Design的博客文章Single Table Inheritance and RESTful Routes中,建议将子类的资源映射到超类控制器,如上面的 SO 答案 2 所示。
- Alex Reisner在 Rails 中发表了一篇文章Single Table Inheritance,其中他主张不要将子类的资源映射到父类 in
routes.rb,因为这只会捕获来自link_toand 的路由中断redirect_to,而不是来自 from 的路由中断form_for。因此,他建议改为向父类添加一个方法,以使子类对其类撒谎。听起来不错,但他的方法给了我错误undefined local variable or method `child' for #。
So the answer that seems most elegant and has the most consensus (but it's not all thatelegant, nor thatmuch consensus), is the add the resources to your routes.rb. Except this doesn't work for form_for. I need some clarity! To distill the choices above, my options are
因此,看起来最优雅且最有共识的答案(但并不是那么优雅,也没有那么多共识),就是将资源添加到您的routes.rb. 除了这不适用于form_for. 我需要一些清晰度!为了提炼上面的选择,我的选择是
- map the resources of the subclass to the controller of the superclass in
routes.rb(and hope I don't need to call form_for on any subclasses) - Override rails internal methods to make the classes lie to each other
- Edit every instance in the code where the path to an object's action is invoked implicitly or explicitly, either changing the path or type-casting the object.
- 将子类的资源映射到超类的控制器中
routes.rb(希望我不需要在任何子类上调用 form_for ) - 覆盖 rails 内部方法使类相互欺骗
- 编辑代码中隐式或显式调用对象操作路径的每个实例,更改路径或类型转换对象。
With all these conflicting answers, I need a ruling. It seems to me like there is no good answer. Is this a failing in rails' design? If so, is it a bug that may get fixed? Or if not, then I'm hoping someone can set me straight on this, walk me through the pros and cons of each option (or explain why that's not an option), and which one is the right answer, and why. Or is there a right answer that I'm not finding on the web?
有了所有这些相互矛盾的答案,我需要一个裁决。在我看来,没有好的答案。这是导轨设计的失败吗?如果是这样,它是一个可以修复的错误吗?或者,如果不是,那么我希望有人可以让我直截了当,引导我了解每个选项的优缺点(或解释为什么这不是一个选项),哪个是正确的答案,以及为什么。或者有没有我在网上找不到的正确答案?
回答by Prathan Thananart
This is the simplest solution I was able to come up with with minimal side effect.
这是我能想出的最简单的解决方案,而且副作用最小。
class Person < Contact
def self.model_name
Contact.model_name
end
end
Now url_for @personwill map to contact_pathas expected.
现在url_for @person将按contact_path预期映射到。
How it works:URL helpers rely on YourModel.model_nameto reflect upon the model and generate (amongst many things) singular/plural route keys. Here Personis basically saying I'm just like Contactdude, ask him.
它是如何工作的:URL 助手依赖于YourModel.model_name反映模型并生成(在许多事情中)单数/复数路由键。这里Person基本上是说我就像Contact老兄,问他。
回答by James
I had the same problem. After using STI, the form_formethod was posting to the wrong child url.
我有同样的问题。使用 STI 后,该form_for方法发布到错误的子网址。
NoMethodError (undefined method `building_url' for
I ended up adding in the extra routes for the child classes and pointing them to the same controllers
我最终为子类添加了额外的路由并将它们指向相同的控制器
resources :structures
resources :buildings, :controller => 'structures'
resources :bridges, :controller => 'structures'
Additionally:
此外:
<% form_for(@structure, :as => :structure) do |f| %>
in this case structure is actually a building (child class)
在这种情况下,结构实际上是一座建筑物(子类)
It seems to work for me after doing a submit with form_for.
使用form_for.
回答by u445908
I suggest you take a look at : https://stackoverflow.com/a/605172/445908, using this method will enable you to use "form_for".
我建议你看看:https: //stackoverflow.com/a/605172/445908,使用这种方法将使你能够使用“form_for”。
ActiveRecord::Base#becomes
回答by jobwat
Use typein the routes:
在路由中使用类型:
resources :employee, controller: 'person', type: 'Employee'
http://samurails.com/tutorial/single-table-inheritance-with-rails-4-part-2/
http://samurails.com/tutorial/single-table-inheritance-with-rails-4-part-2/
回答by Marcus
I was having trouble with this problem too and came by this answer on a question similar to ours. It worked for me.
我也遇到了这个问题,并在一个与我们类似的问题上得到了这个答案。它对我有用。
form_for @list.becomes(List)
Answer shown here: Using STI path with same controller
此处显示的答案:使用具有相同控制器的 STI 路径
The .becomesmethod is defined as mainly used for solving STI problems like your form_forone.
该.becomes方法被定义为主要用于解决像您这样的 STI 问题form_for。
.becomesinfo here: http://apidock.com/rails/ActiveRecord/Base/becomes
.becomes信息在这里:http: //apidock.com/rails/ActiveRecord/Base/becomes
Super late response, but this is the best answer I could find and it worked well for me. Hope this helps some one. Cheers!
超级迟到的回复,但这是我能找到的最好的答案,对我来说效果很好。希望这对某人有所帮助。干杯!
回答by eloyesp
Following the idea of @Prathan Thananart but trying to not destroy nothing. (since there is so much magic involved)
遵循@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
Now url_for @person will map to contact_path as expected.
现在 url_for @person 将按预期映射到 contact_path。
回答by Andrew Hacking
Ok, Ive had a ton of frustration in this area of Rails, and have arrived at the following approach, perhaps this will help others.
好吧,我在 Rails 的这方面遇到了很多挫折,并得出了以下方法,也许这会对其他人有所帮助。
Firstly be aware that a number of solutions above and around the net suggest using constantize on client provided parameters. This is a known DoS attack vector as Ruby does not garbage collect symbols, thus allowing an attacker to create arbitrary symbols and consume available memory.
首先请注意,网络上和网络上的许多解决方案都建议在客户端提供的参数上使用 constantize。这是一个已知的 DoS 攻击向量,因为 Ruby 不会垃圾收集符号,因此允许攻击者创建任意符号并消耗可用内存。
I've implemented the approach below which supports instantiation of model subclasses, and is SAFE from the contantize problem above. It is very similar to what rails 4 does, but also allows more than one level of subclassing (unlike Rails 4) and works in Rails 3.
我已经实现了下面的方法,它支持模型子类的实例化,并且从上面的 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)
After trying various approaches for the 'sublclass loading in devlopment issue' many similar to whats suggested above, I found the only thing that worked reliably was to use 'require_dependency' in my model classes. This ensures that class loading works properly in development and causes no issues in production. In development, without 'require_dependency' AR wont know about all subclasses, which impacts the SQL emitted for matching on the type column. In addition without 'require_dependency' you can also end up in a situation with multiple versions of the model classes at the same time! (eg. this can happen when you change a base or intermediate class, the sub-classes don't always seem to reload and are left subclassing from the old class)
在尝试了许多类似于上面建议的“开发问题中的子类加载”的各种方法后,我发现唯一可靠的方法是在我的模型类中使用“require_dependency”。这可确保类加载在开发中正常工作,并且不会在生产中引起任何问题。在开发中,如果没有 'require_dependency' AR 不会知道所有子类,这会影响为匹配类型列而发出的 SQL。此外,如果没有“require_dependency”,您还可能会同时遇到多个版本的模型类!(例如,当您更改基类或中间类时可能会发生这种情况,子类似乎并不总是重新加载并从旧类中继承子类)
# contact.rb
class Contact < ActiveRecord::Base
acts_as_castable
end
require_dependency 'person'
require_dependency 'organisation'
I also don't override model_name as suggested above because I use I18n and need different strings for the attributes of different subclasses, eg :tax_identifier becomes 'ABN' for Organisation, and 'TFN' for Person (in Australia).
我也没有像上面建议的那样覆盖 model_name,因为我使用 I18n 并且需要不同的字符串来表示不同子类的属性,例如:tax_identifier 成为组织的“ABN”,而人的“TFN”(在澳大利亚)。
I also use route mapping, as suggested above, setting the type:
我也使用路由映射,如上所述,设置类型:
resources :person, :controller => 'contacts', :defaults => { 'contact' => { 'type' => Person.sti_name } }
resources :organisation, :controller => 'contacts', :defaults => { 'contact' => { 'type' => Organisation.sti_name } }
In addition to the route mapping, I'm using InheritedResources and SimpleForm and I use the following generic form wrapper for new actions:
除了路由映射之外,我还使用了 InheritedResources 和 SimpleForm,并且我将以下通用表单包装器用于新操作:
simple_form_for resource, as: resource_request_name, url: collection_url,
html: { class: controller_name, multipart: true }
... and for edit actions:
...对于编辑操作:
simple_form_for resource, as: resource_request_name, url: resource_url,
html: { class: controller_name, multipart: true }
And to make this work, in my base ResourceContoller I expose InheritedResource's resource_request_name as a helper method for the view:
为了完成这项工作,在我的基础 ResourceContoller 中,我将 InheritedResource 的 resource_request_name 作为视图的辅助方法公开:
helper_method :resource_request_name
If you're not using InheritedResources, then use something like the following in your 'ResourceController':
如果您没有使用 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
Always happy to hear others experiences and improvements.
总是很高兴听到别人的经验和改进。
回答by Chris Bloom
I recently documentedmy attempts to get a stable STI pattern working in a Rails 3.0 app. Here's the TL;DR version:
我最近记录了我在 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
This approach gets around the problems that you list as well as a number of other issues that others have had with STI approaches.
这种方法解决了您列出的问题以及其他人使用 STI 方法遇到的许多其他问题。
回答by Alexander Makarenko
The cleanest solution I found is to add the following to the base class:
我找到的最干净的解决方案是将以下内容添加到基类中:
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
It works for all subclasses and is much safer than overriding the entire model name object. By targeting only the route keys, we solve the routing problems without breaking I18n or risking any potential side effects caused by overriding the model name as defined by Rails.
它适用于所有子类,并且比覆盖整个模型名称对象安全得多。通过仅针对路由键,我们解决了路由问题,而不会破坏 I18n 或冒因覆盖 Rails 定义的模型名称而导致的任何潜在副作用的风险。
回答by Jake
Here is a safe clean way to have it work in forms and throughout your application that we use.
这是一种安全干净的方法,可以让它在我们使用的表单和整个应用程序中工作。
resources :districts
resources :district_counties, controller: 'districts', type: 'County'
resources :district_cities, controller: 'districts', type: 'City'
Then I have in my form. The added piece for this is the as: :district.
然后我有我的形式。为此添加的部分是 as::district。
= form_for(@district, as: :district, html: { class: "form-horizontal", role: "form" }) do |f|
Hope this helps.
希望这可以帮助。

