ruby-on-rails ruby single-table-inheritance

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:

  1. 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
  2. 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
  3. 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.
  4. Here''s la misma pregunta nuevamente en SO, donde la respuesta principal parece estar equivocada (Rails magic Just Works!)
  5. 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.
  6. 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.
  7. 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 de link_to y redirect_to , pero no de form_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 de undefined 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

  1. 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)
  2. Anula los métodos internos de los rieles para hacer que las clases se encuentren entre sí
  3. 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!



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