ruby on rails - Mejores prácticas para manejar rutas de subclases de ITS en rieles
ruby-on-rails single-table-inheritance (14)
Aquí hay una forma segura y limpia de que funcione en los formularios y en toda la aplicación que usemos.
resources :districts
resources :district_counties, controller: ''districts'', type: ''County''
resources :district_cities, controller: ''districts'', type: ''City''
Entonces tengo en mi forma. La pieza añadida para esto es el as:: distrito.
= form_for(@district, as: :district, html: { class: "form-horizontal", role: "form" }) do |f|
Espero que esto ayude.
Las vistas y los controladores de My Rails están llenos de redirect_to
, link_to
y form_for
llamadas a métodos. A veces link_to
y redirect_to
son explícitos en las rutas que están vinculando (por ejemplo, link_to ''New Person'', new_person_path
), pero muchas veces las rutas están implícitas (por ejemplo, link_to ''Show'', person
).
Agrego alguna herencia de tabla única (STI) a mi modelo (digamos Employee < Person
), y todos estos métodos se rompen para una instancia de la subclase (digamos Employee
); cuando Rails ejecuta link_to @person
, se link_to @person
un error con el undefined method employee_path'' for #<#<Class:0x000001022bcd40>:0x0000010226d038>
. Rails está buscando una ruta definida por el nombre de clase del objeto, que es empleado. Estas rutas de empleados no están definidas, y no hay un controlador de empleado, por lo que las acciones tampoco están definidas.
Esta pregunta se ha hecho antes:
- En StackOverflow , la respuesta es editar cada instancia de link_to, etc. en toda su base de código, y declarar explícitamente la ruta
- En StackOverflow nuevamente, dos personas sugieren usar
routes.rb
para asignar los recursos de la subclase a la clase padre (map.resources :employees, :controller => ''people''
). La respuesta principal en la misma pregunta de SO sugiere que se.becomes
en cada objeto de instancia en la base de código con.becomes
- Otro más en StackOverflow , la respuesta más importante está en el campo Do Repetir usted mismo, y sugiere crear andamios duplicados para cada subclase.
- Here''s la misma pregunta nuevamente en SO, donde la respuesta principal parece estar equivocada (Rails magic Just Works!)
- En otra parte de la web, encontré esta publicación de blog donde F2Andy recomienda editar en la ruta en todas partes del código.
- En la publicación de blog Single Table Herencia y Rutas RESTful en Logical Reality Design, se recomienda asignar los recursos para la subclase al controlador de superclase, como en la respuesta SO número 2 anterior.
- Alex Reisner tiene una herencia de tabla única en Rails , en la que defiende el mapeo de los recursos de las clases secundarias a la clase padre en
routes.rb
, ya que solo atrapa las roturas de enrutamiento delink_to
yredirect_to
, pero no deform_for
. Por lo tanto, recomienda en su lugar agregar un método a la clase principal para que las subclases mientan sobre su clase. Suena bien, pero su método me dio el error deundefined local variable or method `child'' for #
.
Entonces, la respuesta que parece más elegante y tiene más consenso (pero no es tan elegante, ni mucho consenso), es agregar los recursos a sus routes.rb
. Excepto que esto no funciona para form_for
. Necesito algo de claridad! Para destilar las opciones anteriores, mis opciones son
- mapear los recursos de la subclase al controlador de la superclase en
routes.rb
(y espero no tener que llamar a form_for en ninguna subclase) - Anula los métodos internos de los rieles para hacer que las clases se encuentren entre sí
- Edite cada instancia en el código donde la ruta a la acción de un objeto se invoca implícita o explícitamente, ya sea cambiando la ruta o lanzando el objeto.
Con todas estas respuestas contradictorias, necesito un fallo. Me parece que no hay una buena respuesta. ¿Esto está fallando en el diseño de los rieles? Si es así, ¿es un error que puede solucionarse? O si no, entonces espero que alguien me pueda aclarar esto, guiarme a través de los pros y los contras de cada opción (o explicar por qué no es una opción), y cuál es la respuesta correcta, y por qué. ¿O hay una respuesta correcta que no estoy encontrando en la web?
De esta manera me funciona bien (define este método en la clase base):
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
Esta es la solución más simple que pude obtener con un mínimo efecto secundario.
class Person < Contact
def self.model_name
Contact.model_name
end
end
Ahora url_for @person
se correlacionará con contact_path
como se esperaba.
Cómo funciona: los helpers de URL confían en YourModel.model_name
para reflejar el modelo y generar (entre muchas cosas) claves de ruta singular / plural. Aquí la Person
básicamente dice que soy como Contact
amigo, pregúntale .
Le sugiero que eche un vistazo a: https://.com/a/605172/445908 , utilizando este método le permitirá usar "form_for".
ActiveRecord::Base#becomes
Ok, he tenido mucha frustración en esta área de Rails, y he llegado al siguiente enfoque, quizás esto ayudará a otros.
En primer lugar, tenga en cuenta que una serie de soluciones por encima y alrededor de la red sugieren el uso de la constante en los parámetros proporcionados por el cliente. Este es un conocido vector de ataque DoS, ya que Ruby no recolecta símbolos, lo que permite a un atacante crear símbolos arbitrarios y consumir memoria disponible.
Implementé el siguiente enfoque que admite la creación de instancias de subclases modelo, y es SEGURO desde el problema de contantizar anterior. Es muy similar a lo que hace rails 4, pero también permite más de un nivel de subclases (a diferencia de Rails 4) y funciona en 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)
Después de probar varios enfoques para la ''carga subclase en el problema de desarrollo'', muchos similares a los sugeridos anteriormente, descubrí que lo único que funcionaba de manera confiable era usar ''require_dependency'' en mis clases modelo. Esto asegura que la carga de clases funciona correctamente en el desarrollo y no causa problemas en la producción. En desarrollo, sin ''require_dependency'', AR no conocerá todas las subclases, lo que afecta el SQL emitido para la coincidencia en la columna de tipo. ¡Además, sin ''require_dependency'', también puede terminar en una situación con múltiples versiones de las clases modelo al mismo tiempo! (por ejemplo, esto puede suceder cuando se cambia una clase base o intermedia, las subclases no siempre parecen recargarse y se dejan subclases de la clase anterior)
# contact.rb
class Contact < ActiveRecord::Base
acts_as_castable
end
require_dependency ''person''
require_dependency ''organisation''
Tampoco anulo model_name como se sugiere arriba porque uso I18n y necesito diferentes cadenas para los atributos de diferentes subclases, por ejemplo: tax_identifier se convierte en ''ABN'' para Organización y ''TFN'' para Persona (en Australia).
También uso el mapeo de ruta, como se sugirió anteriormente, estableciendo el tipo:
resources :person, :controller => ''contacts'', :defaults => { ''contact'' => { ''type'' => Person.sti_name } }
resources :organisation, :controller => ''contacts'', :defaults => { ''contact'' => { ''type'' => Organisation.sti_name } }
Además del mapeo de ruta, estoy usando InheritedResources y SimpleForm y utilizo el siguiente wrapper de formulario genérico para nuevas acciones:
simple_form_for resource, as: resource_request_name, url: collection_url,
html: { class: controller_name, multipart: true }
... y para las acciones de edición:
simple_form_for resource, as: resource_request_name, url: resource_url,
html: { class: controller_name, multipart: true }
Y para que esto funcione, en mi ResourceContoller base expongo el resource_request_name de InheritedResource como un método auxiliar para la vista:
helper_method :resource_request_name
Si no está utilizando InheritedResources, entonces use algo como lo siguiente en su ''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
Siempre feliz de escuchar otras experiencias y mejoras.
Puede crear un método que devuelva un objeto principal ficticio para el enrutamiento.
class Person < ActiveRecord::Base
def routing_object
Person.new(id: id)
end
end
y luego simplemente llame a form_for @ employee.routing_object que sin tipo devolverá el objeto de la clase Person
Puedes intentar esto, si no tienes rutas anidadas:
resources :employee, path: :person, controller: :person
O puede ir de otra manera y usar algo de OOP-magic como se describe aquí: https://coderwall.com/p/yijmuq
En una segunda forma, puedes hacer ayudantes similares para todos tus modelos anidados.
Recientemente documented mis intentos de obtener un patrón STI estable trabajando en una aplicación Rails 3.0. Aquí está la versión 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
Este enfoque evita los problemas que enumera, así como una serie de otros problemas que otros han tenido con los enfoques de ITS.
Si considero una herencia de ITS como esta:
class AModel < ActiveRecord::Base ; end
class BModel < AModel ; end
class CModel < AModel ; end
class DModel < AModel ; end
class EModel < AModel ; end
en ''app / models / a_model.rb'' agrego:
module ManagedAtAModelLevel
def model_name
AModel.model_name
end
end
Y luego en la clase 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
Por lo tanto, incluso puedo elegir fácilmente qué modelo quiero usar por defecto, y esto sin siquiera tocar la definición de la clase secundaria. Muy seco.
Siguiendo la idea de @Prathan Thananart, pero tratando de no destruir nada. (ya que hay tanta magia involucrada)
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
Ahora url_for @person se correlacionará con contact_path como se esperaba.
También estaba teniendo problemas con este problema y obtuve esta respuesta en una pregunta similar a la nuestra. Funcionó para mí
form_for @list.becomes(List)
Respuesta que se muestra aquí: Uso de la ruta STI con el mismo controlador
El método .becomes
se define como el más utilizado para resolver problemas de ITS como su form_for
uno.
.becomes
clic aquí: http://apidock.com/rails/ActiveRecord/Base/becomes
Súper tardía respuesta, pero esta es la mejor respuesta que pude encontrar y funcionó bien para mí. Espero que esto ayude a alguien. ¡Aclamaciones!
Use tipo en las rutas:
resources :employee, controller: ''person'', type: ''Employee''
http://samurails.com/tutorial/single-table-inheritance-with-rails-4-part-2/
Yo tuve el mismo problema. Después de usar STI, el método form_for
se estaba publicando en la url secundaria incorrecta.
NoMethodError (undefined method `building_url'' for
Terminé añadiendo las rutas adicionales para las clases secundarias y apuntándolas a los mismos controladores
resources :structures
resources :buildings, :controller => ''structures''
resources :bridges, :controller => ''structures''
Adicionalmente:
<% form_for(@structure, :as => :structure) do |f| %>
en este caso, la estructura es en realidad un edificio (clase infantil)
Parece que me funciona después de enviar un formulario con form_for
.
hackish, pero solo otro más a la lista de soluciones.
class Parent < ActiveRecord::Base; end
Class Child < Parent
def class
Parent
end
end
funciona en los carriles 2.xy 3.x