Ruby on rails rails中处理STI子类路由的最佳实践
我的Rails视图和控制器中充斥着用于方法调用的Ruby on rails rails中处理STI子类路由的最佳实践,ruby-on-rails,ruby,single-table-inheritance,Ruby On Rails,Ruby,Single Table Inheritance,我的Rails视图和控制器中充斥着用于方法调用的重定向到,链接到,以及表单。有时link\u to和redirect\u to在它们链接的路径中是显式的(例如link\u to'New Person',New Person\u path),但很多时候路径是隐式的(例如link\u to'Show',Person) 我在模型中添加了一些单表继承(STI)(比如Employee 在本例中,结构实际上是一个建筑(子类) 在使用表单提交hackish之后,它似乎对我有效,但只是解决方案列表中的另一个
重定向到
,链接到
,以及表单。有时link\u to
和redirect\u to
在它们链接的路径中是显式的(例如link\u to'New Person',New Person\u path
),但很多时候路径是隐式的(例如link\u to'Show',Person
)
我在模型中添加了一些单表继承(STI)(比如Employee
),所有这些方法都会中断子类的实例(比如Employee
);当rails执行link_to@person
时,它会在#
的未定义的方法employee_path'中出错。Rails正在查找由对象的类名(employee)定义的路由。这些员工路线没有定义,并且没有员工控制器,因此操作也没有定义
这个问题以前有人问过:
首先,答案是在整个代码库中编辑link_to etc的每个实例,并显式地声明路径
再次,两个人建议使用routes.rb
将子类资源映射到父类(map.resources:employees,:controller=>“people”
)。同一个SO问题中的顶部答案建议使用转换代码库中的每个实例对象
在另一个例子中,最重要的答案是DoRepeatYourself阵营,建议为每个子类创建重复的脚手架
同样的问题再次出现在SO,最上面的答案似乎是错误的(Rails magic正好有效!)
在web上的其他地方,我找到了F2Andy建议在代码中任何地方的路径中进行编辑的地方
在Logical Reality Design的博客文章中,建议将子类的资源映射到超类控制器,如上面答案2所示
Alex Reisner有一篇文章,他在文章中反对将子类的资源映射到routes.rb
中的父类,因为这只捕获从链接到和重定向到,而不是从表单到的路由中断。因此,他建议改为向父类添加一个方法,以使子类对其类撒谎。听起来不错,但是他的方法给了我一个错误未定义的局部变量或方法'child'。
因此,看起来最优雅、最有共识的答案(但不是那么优雅,也不是那么有共识)是将资源添加到您的routes.rb
。除了这不适用于表单_for
。我需要澄清!要提取上述选项,我的选项如下
将子类的资源映射到routes.rb
中的超类控制器(希望我不需要调用任何子类的表单)
重写rails内部方法以使类彼此独立
编辑代码中隐式或显式调用对象操作路径的每个实例,更改路径或类型转换对象
所有这些相互矛盾的答案,我需要一个裁决。在我看来,似乎没有好的答案。这是rails设计中的一个失败吗?如果是这样的话,这是一个可以修复的bug吗?如果不是,我希望有人能让我明白这一点,让我了解每个选项的优缺点(或者解释为什么这不是一个选项),哪一个是正确的答案,为什么。还是有我在网上找不到的正确答案?我也有同样的问题。使用STI后,
方法的表单_发布到错误的子url
NoMethodError (undefined method `building_url' for
最后,我为子类添加了额外的路由,并将它们指向相同的控制器
resources :structures
resources :buildings, :controller => 'structures'
resources :bridges, :controller => 'structures'
此外:
<% form_for(@structure, :as => :structure) do |f| %>
:结构)do | f |%>
在本例中,结构实际上是一个建筑(子类)
在使用表单提交hackish之后,它似乎对我有效,但只是解决方案列表中的另一个
class Parent < ActiveRecord::Base; end
Class Child < Parent
def class
Parent
end
end
class父类
在rails 2.x和3.x上工作最近,我尝试在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
#app/controllers/kase_controller.rb
类KaseController<应用程序控制器
def新
设置\u sti\u模型
# ...
结束
def创建
设置\u sti\u模型
# ...
结束
私有的
def设置_sti_模型
#这使我们可以从表单和查询字符串中设置“type”属性
模型=零
如果!参数[:kase]。空白?还有!参数[:kase][:type]。是否为空?
model=params[:kase].delete(:type).constantize.tos
结束
@kase=kase.new(参数[:kase])
@kase.type=模型
结束
结束
#app/models/kase.rb
类Kase
这种方法绕过了您列出的问题以及其他人在STI方法中遇到的许多其他问题。这是我能够想到的最简单的解决方案,副作用最小
class Person < Contact
def self.model_name
Contact.model_name
end
end
class-Person
现在@person的url\u将映射到contact\u路径
ActiveRecord::Base#becomes
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
class AModel < ActiveRecord::Base ; end
class BModel < AModel ; end
class CModel < AModel ; end
class DModel < AModel ; end
class EModel < AModel ; end
module ManagedAtAModelLevel
def model_name
AModel.model_name
end
end
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
# 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)
# contact.rb
class Contact < ActiveRecord::Base
acts_as_castable
end
require_dependency 'person'
require_dependency 'organisation'
resources :person, :controller => 'contacts', :defaults => { 'contact' => { 'type' => Person.sti_name } }
resources :organisation, :controller => 'contacts', :defaults => { 'contact' => { 'type' => Organisation.sti_name } }
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 }
helper_method :resource_request_name
# 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
resources :employee, controller: 'person', type: 'Employee'
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|
form_for @list.becomes(List)
class Contact < ActiveRecord::Base
def self.model_name
ActiveModel::Name.new(self, nil, 'Contact')
end
end
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
polymorphic_url([:admin, @article, @comment])
# => admin_article_comment_url(@article, @comment)
edit_polymorphic_path(@post)
# => "/posts/1/edit"
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
foo.becomes(foo.class.base_class)
def becomes_base
becomes(self.class.base_class)
end