Mejores prácticas para manejar rutas para subclases de ITS en rieles

175

Mis Rieles vistas y controladores están llenas redirect_to, link_toy form_forlas llamadas a métodos. A veces link_toy redirect_toson explícitos en las rutas que están vinculando (por ejemplo link_to 'New Person', new_person_path), pero muchas veces las rutas son implícitas (por ejemplo link_to 'Show', person).

Agrego algo de herencia de tabla única (STI) a mi modelo (por ejemplo Employee < Person), y todos estos métodos se rompen para una instancia de la subclase (por ejemplo Employee); cuando se ejecuta rails link_to @person, se equivoca con undefined method employee_path' for #<#<Class:0x000001022bcd40>:0x0000010226d038>. Rails está buscando una ruta definida por el nombre de la clase del objeto, que es empleado. Estas rutas de empleados no están definidas y no hay un controlador de empleados, 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 establecer explícitamente la ruta
  2. En StackOverflow nuevamente, dos personas sugieren usar routes.rbpara asignar los recursos de la subclase a la clase padre ( map.resources :employees, :controller => 'people'). La respuesta principal en esa misma pregunta SO sugiere la conversión de tipos de cada objeto de instancia en la base de código usando.becomes
  3. Otro más en StackOverflow , la respuesta principal es en el campo Do Repeat Yourself, y sugiere crear andamios duplicados para cada subclase.
  4. Aquí está la misma pregunta nuevamente en SO, donde la respuesta principal parece estar mal (¡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 Herencia de tabla única y rutas RESTful en Logical Reality Design, se recomienda asignar los recursos para la subclase al controlador de la superclase, como en la respuesta SO número 2 anterior.
  7. Alex Reisner tiene una publicación de herencia de tabla única en Rails , en la que defiende contra el mapeo de los recursos de las clases secundarias a la clase principal routes.rb, ya que eso solo detecta roturas de enrutamiento desde link_toy redirect_to, pero no desde form_for. Por lo tanto, recomienda 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 undefined local variable or method `child' for #.

Así que la respuesta que parece más elegante y tiene la mayor parte de consenso (pero no es todo lo que elegante, ni que tanto consenso), es el añadir los recursos a su routes.rb. Excepto que esto no funciona form_for. Necesito algo de claridad! Para destilar las opciones anteriores, mis opciones son

  1. asigne 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. Anular métodos internos de rieles para hacer que las clases se mientan entre sí
  3. Edite cada instancia en el código donde se invoca implícita o explícitamente la ruta a la acción de un objeto, ya sea cambiando la ruta o escribiendo el objeto.

Con todas estas respuestas en conflicto, necesito una decisión. Me parece que no hay una buena respuesta. ¿Es esto una falla en el diseño de los rieles? Si es así, ¿es un error que puede solucionarse? O si no, entonces espero que alguien pueda aclararme esto, guiarme a través de los pros y los contras de cada opción (o explicar por qué esa 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?

ziggurismo
fuente
1
Hubo un error tipográfico en el código de Alex Reisner, que se corrigió después de que comenté en su blog. Esperemos que ahora la solución de Alex sea viable. Mi pregunta sigue en pie: ¿cuál es la solución correcta?
ziggurismo
1
Aunque tiene aproximadamente tres años, encontré esta publicación de blog en rookieonrails.blogspot.com/2008/01/… y la conversación vinculada de la lista de correo de rieles es informativa. Uno de los respondedores describe la diferencia entre los ayudantes polimórficos y los ayudantes nombrados.
ziggurismo
2
Una opción que no enumera es parchear Rails para que link_to, form_for y el lugar similar sean agradables con herencia de tabla única. Ese puede ser un trabajo difícil, pero es algo que me encantaría ver arreglado.
M. Scott Ford

Respuestas:

140

Esta es la solución más simple que pude encontrar con un efecto secundario mínimo.

class Person < Contact
  def self.model_name
    Contact.model_name
  end
end

Ahora se url_for @personasignará a contact_pathlo esperado.

Cómo funciona: los ayudantes de URL dependen YourModel.model_namepara reflexionar sobre el modelo y generar (entre muchas cosas) claves de ruta en singular / plural. Aquí Personbásicamente está diciendo que soy como Contactamigo, pregúntale .

Prathan Thananart
fuente
44
Estaba pensando en hacer lo mismo, pero me preocupaba que #model_name pudiera usarse en otros lugares de Rails, y que este cambio pudiera interferir con el funcionamiento normal. ¿Alguna idea?
nkassis
3
Estoy totalmente de acuerdo con el misterioso desconocido @nkassis. Este es un truco genial, pero ¿cómo sabes que no estás destruyendo los elementos internos de los rieles?
tsherif
66
Especificaciones. Además, utilizamos este código en producción, y puedo dar fe de que no estropea: 1) relaciones entre modelos, 2) creación de instancias de modelo de ITS (a través de build_x/ create_x). Por otro lado, el precio de jugar con magia es que nunca estás 100% seguro de lo que puede cambiar.
Prathan Thananart
10
Esto rompe i18n si está intentando tener diferentes nombres humanos para los atributos según la clase.
Rufo Sanchez
44
En lugar de anular por completo de esta manera, puede anular los bits que necesita. Ver gist.github.com/sj26/5843855
sj26
47

Yo tuve el mismo problema. Después de usar STI, el form_formétodo se estaba publicando en la url secundaria incorrecta.

NoMethodError (undefined method `building_url' for

Terminé agregando las rutas adicionales para las clases secundarias y señalá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 secundaria)

Parece funcionar para mí después de hacer un envío con form_for.

James
fuente
2
Esto funciona, pero agrega muchas rutas innecesarias en nuestras rutas. ¿No hay una manera de hacer esto de una manera menos intrusiva?
Anders Kindberg
1
Puede configurar rutas programáticamente en su archivo routes.rb, por lo que podría hacer una pequeña meta programación para configurar las rutas secundarias. Sin embargo, en entornos donde las clases no se almacenan en caché (por ejemplo, desarrollo), primero debe cargar las clases previamente. Entonces, de una forma u otra, debe especificar las subclases en algún lugar. Consulte gist.github.com/1713398 para ver un ejemplo.
Chris Bloom
En mi caso, exponer el nombre del objeto (ruta) al usuario no es deseable (y confuso para el usuario).
laffuste
33

Le sugiero que eche un vistazo a: https://stackoverflow.com/a/605172/445908 , el uso de este método le permitirá usar "form_for".

ActiveRecord::Base#becomes
Siwei Shen 申思维
fuente
2
Tuve que configurar la URL explícitamente para que renderice y guarde correctamente. <%= form_for @child, :as => :child, url: @child.becomes(Parent)
lulalala
44
@lulalala Probar<%= form_for @child.becomes(Parent)
Richard Jones
14

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 asignará a contact_path como se esperaba.

eloyesp
fuente
14

También estaba teniendo problemas con este problema y recibí esta respuesta en una pregunta similar a la nuestra. Funcionó para mi.

form_for @list.becomes(List)

La respuesta se muestra aquí: Uso de la ruta STI con el mismo controlador

El .becomesmétodo se define como el utilizado principalmente para resolver problemas de ITS como el suyo form_for.

.becomesinformación aquí: http://apidock.com/rails/ActiveRecord/Base/becomes

Respuesta súper tardía, pero esta es la mejor respuesta que pude encontrar y funcionó bien para mí. Espero que esto ayude a alguien. ¡Salud!

Marcus
fuente
5

Ok, he tenido un montón de frustración en esta área de Rails, y he llegado al siguiente enfoque, quizás esto ayude a otros.

En primer lugar, tenga en cuenta que varias 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 vector de ataque DoS conocido ya que Ruby no recolecta símbolos de recolección de basura, lo que permite a un atacante crear símbolos arbitrarios y consumir memoria disponible.

He implementado el siguiente enfoque que admite la creación de instancias de subclases de modelos, y es SEGURO del problema de contacto anterior. Es muy similar a lo que hace Rails 4, pero también permite más de un nivel de subclasificación (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 el 'problema de carga de subclase en desarrollo', muchos similares a los sugeridos anteriormente, descubrí que lo único que funcionaba de manera confiable era usar 'require_dependency' en mis clases de modelo. Esto garantiza que la carga de clases funcione correctamente en el desarrollo y no cause problemas en la producción. En desarrollo, sin AR '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 varias versiones de las clases de modelo al mismo tiempo. (por ejemplo, esto puede suceder cuando cambia una clase base o intermedia, las subclases no siempre parecen recargarse y quedan 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 sugirió anteriormente 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 rutas, como se sugirió anteriormente, configurando 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 rutas, estoy usando InheritedResources y SimpleForm y uso el siguiente contenedor 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 editar acciones:

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 resource_request_name de InheritedResource como un método auxiliar para la vista:

helper_method :resource_request_name 

Si no está utilizando InheritedResources, 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 contento de escuchar otras experiencias y mejoras.

Andrew Hacking
fuente
En mi experiencia (al menos en Rails 3.0.9), la constante falla si la constante nombrada por la cadena no existe. Entonces, ¿cómo se puede usar para crear nuevos símbolos arbitrarios?
Lex Lindsey
4

Recientemente documenté mis intentos de lograr que un patrón STI estable funcione 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 otros problemas que otros han tenido con los enfoques de ITS.

Chris Bloom
fuente
2

Puedes probar esto, si no tienes rutas anidadas:

resources :employee, path: :person, controller: :person

O puede ir por otro lado y usar algo de magia OOP como se describe aquí: https://coderwall.com/p/yijmuq

De la segunda manera, puede crear ayudantes similares para todos sus modelos anidados.

siempre
fuente
2

Aquí hay una manera limpia y segura de que funcione en los formularios y en toda la aplicación que utilizamos.

resources :districts
resources :district_counties, controller: 'districts', type: 'County'
resources :district_cities, controller: 'districts', type: 'City'

Entonces tengo en mi forma. La pieza adicional para esto es el: distrito.

= form_for(@district, as: :district, html: { class: "form-horizontal",         role: "form" }) do |f|

Espero que esto ayude.

Jake
fuente
2

La solución más limpia que encontré es agregar lo siguiente a la clase base:

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

Funciona para todas las subclases y es mucho más seguro que anular todo el objeto del nombre del modelo. Al apuntar solo a las teclas de ruta, resolvemos los problemas de enrutamiento sin romper I18n o arriesgar cualquier efecto secundario potencial causado por anular el nombre del modelo como lo define Rails.

Alexander Makarenko
fuente
1

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 el predeterminado, y esto sin siquiera tocar la definición de subclase. Muy seco.

Laurent B.
fuente
1

Esta manera me funciona bien (defina 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
ka8725
fuente
1

Puede crear un método que devuelva un objeto padre ficticio para el propósito de 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 clase Person

mezclarse
fuente
1

Siguiendo la respuesta @ prathan-thananart, y para las múltiples clases de ITS, puede agregar lo siguiente al modelo principal ->

class Contact < ActiveRecord::Base
  def self.model_name
    ActiveModel::Name.new(self, nil, 'Contact')
  end
end

Eso hará que cada formulario con los datos de contacto para enviar params como params[:contact]lugar de params[:contact_person], params[:contact_whatever].

Zlatko Alomerovic
fuente
-6

hackear, pero solo otro más en la lista de soluciones.

class Parent < ActiveRecord::Base; end

Class Child < Parent
  def class
    Parent
  end
end

funciona en rieles 2.xy 3.x

Ohad Levy
fuente
55
Esto soluciona un problema, pero crea otro. Ahora, cuando intentas hacerlo Child.new, devuelve una Parentclase en lugar de la subclase. Esto significa que no puede crear las subclases a través de la asignación masiva a través de un controlador (ya que typees un atributo protegido de forma predeterminada) a menos que también establezca el atributo type explícitamente.
Chris Bloom