Рекомендации по обработке маршрутов для подклассов STI в рельсах
Представления и контроллеры My Rails заполняются вызовами методов redirect_to
, link_to
и form_for
. Иногда link_to
и redirect_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 ищет маршрут, определяемый именем класса объекта, который является сотрудником. Эти маршруты сотрудников не определены, и нет контроллера сотрудника, поэтому действия также не определены.
Этот вопрос задан раньше:
- В qaru.site/info/39531/... ответ должен отредактировать каждый экземпляр link_to и т.д. во всей вашей кодовой базе и указать путь явно
- В qaru.site/info/39531/... два человека предлагают использовать
routes.rb
для сопоставления ресурсов подкласса родительскому классу (map.resources :employees, :controller => 'people'
). Верхний ответ в том же самом вопросе SO предполагает, что тип экземпляра каждого экземпляра в базе кода использует тип .becomes
- Еще один из qaru.site/info/39531/..., лучший ответ - в лагере Do Repeat Yourself и предлагает создать дублирующие леса для каждого подкласса.
- Вот и тот же вопрос снова в SO, где верхний ответ кажется просто неправильным (Rails magic Just Works!)
- В другом месте в Интернете я нашел это сообщение в блоге, где F2Andy рекомендует редактировать в пути всюду в коде.
- В сообщении в блоге Одиночные таблицы и RESTful Routes в Logical Reality Design рекомендуется сопоставить ресурсы для подкласса с суперклассом контроллер, как и в ответе "Ответ №2" выше.
- У Alex Reisner есть сообщение Одиночное наследование таблиц в Rails, в котором он выступает против сопоставления ресурсов дочерних классов родительскому классу в
routes.rb
, так как это только ловит разрывы маршрутизации от link_to
и redirect_to
, но не от form_for
. Поэтому он рекомендует вместо этого добавить метод к родительскому классу, чтобы заставить подклассы лгать об их классе. Звучит неплохо, но его метод дал мне ошибку undefined local variable or method `child' for #
.
Таким образом, ответ, который кажется самым изящным и имеет наибольший консенсус (но это не все, что изящно и не так консенсус), добавляет ресурсы к вашему routes.rb
. Кроме того, это не работает для form_for
. Мне нужна ясность! Чтобы отменить выбор выше, мои параметры
- сопоставить ресурсы подкласса с контроллером суперкласса в
routes.rb
(и, надеюсь, мне не нужно вызывать form_for для любых подклассов)
- Переопределить внутренние методы рельсов, чтобы классы лежали друг с другом.
- Редактировать каждый экземпляр кода, в котором путь к действию объекта вызывается неявно или явно, либо изменяя путь, либо тип-литье объекта.
Со всеми этими противоречивыми ответами мне нужно решение. Мне кажется, что нет хорошего ответа. Это неудача в дизайне рельсов? Если да, это ошибка, которая может быть исправлена? Или, если нет, то я надеюсь, что кто-то может направить меня прямо на это, проведет меня через плюсы и минусы каждого варианта (или объясните, почему это не вариант), и какой из них правильный ответ и почему. Или есть правильный ответ, который я не нахожу в Интернете?
Ответы
Ответ 1
Это самое простое решение, с которым я смог придумать с минимальным побочным эффектом.
class Person < Contact
def self.model_name
Contact.model_name
end
end
Теперь url_for @person
будет отображаться на contact_path
, как ожидалось.
Как это работает: Помощники URL-адреса полагаются на YourModel.model_name
, чтобы отражать модель и генерировать (среди многих) уникальные/множественные ключи маршрута. Здесь Person
в основном говорит, что я просто как Contact
чувак, спросите его.
Ответ 2
У меня была та же проблема. После использования 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
.
Ответ 3
Я предлагаю вам взглянуть на: fooobar.com/questions/39526/..., используя этот метод, вы сможете использовать "form_for".
ActiveRecord::Base#becomes
Ответ 4
Использовать тип маршрута:
resources :employee, controller: 'person', type: 'Employee'
http://samurails.com/tutorial/single-table-inheritance-with-rails-4-part-2/
Ответ 5
Следуя идее @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 будет сопоставлять контакт_path, как ожидалось.
Ответ 6
У меня тоже были проблемы с этой проблемой, и я пришел к этому ответу по вопросу, подобному нашему. Это сработало для меня.
form_for @list.becomes(List)
Ответ показан ниже: Использование пути STI с одним и тем же контроллером
Метод .becomes
определяется как в основном используемый для решения проблем STI, таких как ваш form_for
один.
.becomes
info здесь: http://apidock.com/rails/ActiveRecord/Base/becomes
Супер поздний ответ, но это лучший ответ, который я смог найти, и это сработало для меня. Надеюсь, это поможет кому-то. Ура!
Ответ 7
Хорошо, у меня было много разочарований в этой области Rails, и они пришли к следующему подходу, возможно, это поможет другим.
Во-первых, имейте в виду, что ряд решений выше и вокруг сети предполагают использование константы на клиентских параметрах. Это известный вектор атаки DoS, поскольку Ruby не отображает символы сбора мусора, что позволяет злоумышленнику создавать произвольные символы и потреблять доступную память.
Я реализовал подход, который поддерживает создание экземпляров подклассов модели и является SAFE из вышеперечисленной проблемы. Он очень похож на то, что делает рельсы 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)
После того, как вы попытались использовать различные подходы к загрузке sublclass в проблеме devlopment, многие из которых были похожи на предложенные выше, я нашел единственное, что надежно работало в том, чтобы использовать "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" для организации и "TFN" для Person (в Австралии).
Я также использую сопоставление маршрутов, как было предложено выше, для установки типа:
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
Всегда рады услышать о других впечатлениях и улучшениях.
Ответ 8
Недавно я документировал мои попытки получить стабильный шаблон 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.
Ответ 9
Вы можете попробовать это, если у вас нет вложенных маршрутов:
resources :employee, path: :person, controller: :person
Или вы можете пойти другим путем и использовать некоторую ООП-магию, как описано здесь: https://coderwall.com/p/yijmuq
Во-вторых, вы можете создавать похожие помощники для всех ваших вложенных моделей.
Ответ 10
Вот безопасный чистый способ заставить его работать в формах и во всем используемом нами приложении.
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|
Надеюсь, что это поможет.
Ответ 11
Если я считаю наследование 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
Поэтому я могу даже легко выбрать, какую модель я хочу использовать по умолчанию, и это, даже не касаясь определения подкласса. Очень сухой.
Ответ 12
Этот способ работает для меня нормально (определите этот метод в базовом классе):
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
Ответ 13
Вы можете создать метод, который возвращает фиктивный родительский объект для маршрутизации purpouse
class Person < ActiveRecord::Base
def routing_object
Person.new(id: id)
end
end
а затем просто вызовите form_for @employee.routing_object
который без типа возвращает объект класса Person
Ответ 14
взломать, но еще один в список решений.
class Parent < ActiveRecord::Base; end
Class Child < Parent
def class
Parent
end
end
работает с рельсами 2.x и 3.x