Práticas recomendadas para lidar com rotas para subclasses de STI em trilhos

175

Meus pontos de vista Rails e controladores estão repletas redirect_to, link_toe form_forchamadas de método. Às vezes link_toe redirect_tosão explícitos nos caminhos que estão vinculando (por exemplo link_to 'New Person', new_person_path), mas muitas vezes os caminhos estão implícitos (por exemplo link_to 'Show', person).

Eu adiciono alguma herança de tabela única (STI) ao meu modelo (por exemplo Employee < Person), e todos esses métodos são interrompidos para uma instância da subclasse (por exemplo Employee); quando o rails é executado link_to @person, ele erro com undefined method employee_path' for #<#<Class:0x000001022bcd40>:0x0000010226d038>. O Rails está procurando uma rota definida pelo nome da classe do objeto, que é empregado. Essas rotas de funcionários não são definidas e não há um controlador de funcionários, portanto, as ações também não são definidas.

Esta pergunta já foi feita antes:

  1. No StackOverflow , a resposta é editar todas as instâncias de link_to etc em toda a sua base de código e indicar o caminho explicitamente
  2. No StackOverflow novamente, duas pessoas sugerem o uso routes.rbpara mapear os recursos da subclasse para a classe pai ( map.resources :employees, :controller => 'people'). A resposta principal nessa mesma pergunta de SO sugere a conversão de tipos de todos os objetos de instância na base de código usando.becomes
  3. Ainda outra no StackOverflow , a resposta principal está no campo Do Repeat Yourself e sugere a criação de andaimes duplicados para cada subclasse.
  4. Aqui está a mesma pergunta novamente na SO, onde a resposta principal parece estar errada (a mágica do Rails simplesmente funciona!)
  5. Em outras partes da web, encontrei este post no qual a F2Andy recomenda editar o caminho em qualquer parte do código.
  6. Na postagem do blog Herança de tabela única e rotas RESTful no Logical Reality Design, é recomendável mapear os recursos da subclasse para o controlador da superclasse, como na resposta SO 2 acima.
  7. Alex Reisner tem um post de Herança de tabela única no Rails , no qual defende o mapeamento dos recursos das classes filho para a classe pai routes.rb, pois isso apenas captura falhas de roteamento de link_toe redirect_to, mas não de form_for. Portanto, ele recomenda adicionar um método à classe pai para fazer com que as subclasses mentam sobre a classe. Parece bom, mas o método dele me deu o erro undefined local variable or method `child' for #.

Portanto, a resposta que parece mais elegante e tem o maior consenso (mas não é tudo o que elegante, nem que muito consenso), é a adicionar os recursos para o seu routes.rb. Exceto que isso não funciona form_for. Eu preciso de alguma clareza! Para destilar as opções acima, minhas opções são

  1. mapeie os recursos da subclasse para o controlador da superclasse em routes.rb(e espero não precisar chamar form_for em nenhuma subclasse)
  2. Substituir métodos internos dos trilhos para fazer com que as classes se encontrem
  3. Edite todas as instâncias no código em que o caminho para a ação de um objeto é invocado implicitamente ou explicitamente, alterando o caminho ou convertendo o objeto em tipo.

Com todas essas respostas conflitantes, preciso de uma decisão. Parece-me que não há uma boa resposta. Isso é uma falha no design dos trilhos? Em caso afirmativo, é um bug que pode ser corrigido? Ou, se não, espero que alguém possa me esclarecer sobre isso, guiar-me pelos prós e contras de cada opção (ou explicar por que isso não é uma opção) e qual é a resposta certa e por quê. Ou existe uma resposta certa que não estou encontrando na web?

zigurates
fonte
1
Houve um erro de digitação no código de Alex Reisner, que ele corrigiu depois que eu comentei em seu blog. Então, esperançosamente, agora a solução de Alex é viável. Minha pergunta ainda permanece: qual é a solução certa?
Ziggurism
1
Embora tenha cerca de três anos, achei este post em rookieonrails.blogspot.com/2008/01/… e a conversa vinculada da lista de discussão sobre trilhos informativa. Um dos respondentes descreve a diferença entre auxiliares polimórficos e auxiliares nomeados.
Ziggurism
2
Uma opção que você não lista é corrigir o Rails para que link_to, form_for e similares sejam agradáveis ​​com a herança de uma única tabela. Pode ser um trabalho difícil, mas é algo que eu adoraria ver corrigido.
Scott Ford

Respostas:

140

Esta é a solução mais simples que consegui encontrar com um efeito colateral mínimo.

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

Agora url_for @personserá mapeado contact_pathconforme o esperado.

Como funciona: os auxiliares de URL dependem YourModel.model_namepara refletir sobre o modelo e gerar (entre muitas coisas) chaves de rota singular / plural. Aqui Personestá basicamente dizendo que eu sou como Contactcara, pergunte a ele .

Prathan Thananart
fonte
4
Eu estava pensando em fazer a mesma coisa, mas estava preocupado que o #model_name pudesse ser usado em outro lugar no Rails e que essa alteração pudesse interferir no funcionamento normal. Alguma ideia?
Nkassis
3
Eu concordo totalmente com o misterioso estranho @nkassis. Este é um truque legal, mas como você sabe que não está invadindo os internos dos trilhos?
Tsherif #
6
Especificações. Além disso, usamos esse código na produção e posso atestar que ele não bagunça: 1) relações entre modelos, 2) instanciação do modelo STI (via build_x/ create_x). Por outro lado, o preço de jogar com magia é que você nunca está 100% certo do que pode mudar.
Prank Thananart
10
Isso interrompe o i18n se você estiver tentando ter nomes humanos diferentes para atributos, dependendo da classe.
Rufo Sanchez
4
Em vez de substituir completamente dessa maneira, você pode substituir os bits necessários. Veja gist.github.com/sj26/5843855
sj26
47

Eu tive o mesmo problema. Depois de usar o STI, o form_formétodo estava sendo lançado no URL filho incorreto.

NoMethodError (undefined method `building_url' for

Acabei adicionando rotas extras para as classes filho e apontando-as para os mesmos controladores

 resources :structures
 resources :buildings, :controller => 'structures'
 resources :bridges, :controller => 'structures'

Além disso:

<% form_for(@structure, :as => :structure) do |f| %>

neste caso, a estrutura é realmente um edifício (classe filho)

Parece funcionar para mim depois de enviar um form_for.

James
fonte
2
Isso funciona, mas adiciona muitos caminhos desnecessários em nossas rotas. Não existe uma maneira de fazer isso de uma maneira menos invasiva?
Anders Kindberg
1
Você pode configurar rotas programaticamente em seu arquivo routes.rb, para fazer uma pequena meta-programação para configurar as rotas filho. No entanto, em ambientes onde as classes não são armazenadas em cache (por exemplo, desenvolvimento), você precisa pré-carregar as classes primeiro. Então, de uma maneira ou de outra, você precisa especificar as subclasses em algum lugar. Veja gist.github.com/1713398 para um exemplo.
31512 Chris Bloom
No meu caso, não é desejável expor o nome do objeto (caminho) ao usuário (e confuso para o usuário).
Laffuste
33

Sugiro que você dê uma olhada em: https://stackoverflow.com/a/605172/445908 , o uso desse método permitirá que você use "form_for".

ActiveRecord::Base#becomes
Siwei Shen 申思维
fonte
2
Eu tive que definir o URL explicitamente para que ele renderize o from e o salve corretamente. <%= form_for @child, :as => :child, url: @child.becomes(Parent)
Lulalala 28/05
4
@lulalala Try<%= form_for @child.becomes(Parent)
Richard Jones
14

Seguindo a ideia de @ Nathan Thananart, mas tentando não destruir nada. (já que há tanta mágica envolvida)

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

Agora, url_for @person será mapeado para contact_path conforme o esperado.

eloyesp
fonte
14

Eu também estava tendo problemas com esse problema e encontrei esta resposta em uma pergunta semelhante à nossa. Funcionou para mim.

form_for @list.becomes(List)

Resposta mostrada aqui: Usando caminho STI com o mesmo controlador

O .becomesmétodo é definido como usado principalmente para resolver problemas de DST, como o seu form_for.

.becomesinformações aqui: http://apidock.com/rails/ActiveRecord/Base/becomes

Resposta super tardia, mas esta é a melhor resposta que pude encontrar e funcionou bem para mim. Espero que isso ajude alguém. Felicidades!

Marcus
fonte
5

Ok, tive muita frustração nesta área do Rails e cheguei à seguinte abordagem, talvez isso ajude outras pessoas.

Em primeiro lugar, lembre-se de que várias soluções acima e ao redor da rede sugerem o uso de constantes nos parâmetros fornecidos pelo cliente. Esse é um vetor de ataque DoS conhecido, pois Ruby não coleta símbolos de lixo, permitindo que um invasor crie símbolos arbitrários e consuma a memória disponível.

Eu implementei a abordagem abaixo, que suporta instanciação de subclasses de modelo e é SAFE do problema de contantização acima. É muito semelhante ao que o Rails 4 faz, mas também permite mais de um nível de subclasse (diferente do Rails 4) e funciona no 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)

Depois de tentar várias abordagens para o 'carregamento de subclasse na questão do desenvolvimento', muito parecido com o sugerido acima, descobri que a única coisa que funcionava de maneira confiável era usar 'require_dependency' nas minhas classes de modelo. Isso garante que o carregamento da classe funcione corretamente no desenvolvimento e não cause problemas na produção. No desenvolvimento, o AR sem 'require_dependency' não saberá sobre todas as subclasses, o que afeta o SQL emitido para correspondência na coluna type. Além disso, sem 'require_dependency', você também pode acabar em uma situação com várias versões das classes de modelo ao mesmo tempo! (por exemplo, isso pode acontecer quando você altera uma classe base ou intermediária, as subclasses nem sempre parecem recarregar e são deixadas em subclasses da classe antiga)

# contact.rb
class Contact < ActiveRecord::Base
  acts_as_castable
end

require_dependency 'person'
require_dependency 'organisation'

Também não substituo model_name como sugerido acima, porque uso I18n e preciso de cadeias diferentes para os atributos de diferentes subclasses, por exemplo: tax_identifier torna-se 'ABN' para Organização e 'TFN' para Pessoa (na Austrália).

Eu também uso o mapeamento de rotas, como sugerido acima, definindo o tipo:

resources :person, :controller => 'contacts', :defaults => { 'contact' => { 'type' => Person.sti_name } }
resources :organisation, :controller => 'contacts', :defaults => { 'contact' => { 'type' => Organisation.sti_name } }

Além do mapeamento de rota, estou usando InheritedResources e SimpleForm e uso o seguinte invólucro de formulário genérico para novas ações:

simple_form_for resource, as: resource_request_name, url: collection_url,
      html: { class: controller_name, multipart: true }

... e para ações de edição:

simple_form_for resource, as: resource_request_name, url: resource_url,
      html: { class: controller_name, multipart: true }

E para fazer isso funcionar, em minha ResourceContoller base, eu exponho resource_request_name de InheritedResource como um método auxiliar para a exibição:

helper_method :resource_request_name 

Se você não estiver usando InheritedResources, use algo como o seguinte em seu '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

Sempre feliz em ouvir outras experiências e melhorias.

Andrew Hacking
fonte
Na minha experiência (pelo menos no Rails 3.0.9), constantize falha se a constante nomeada pela string ainda não existir. Então, como ele pode ser usado para criar novos símbolos arbitrários?
Lex Lindsey
4

Recentemente, documentei minhas tentativas de obter um padrão estável de STI trabalhando em um aplicativo Rails 3.0. Aqui está a versão 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

Essa abordagem contorna os problemas que você lista, bem como vários outros problemas que outras pessoas tiveram com as abordagens de DST.

Chris Bloom
fonte
2

Você pode tentar isso se não tiver rotas aninhadas:

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

Ou você pode seguir outro caminho e usar alguma mágica OOP como descrito aqui: https://coderwall.com/p/yijmuq

Na segunda maneira, você pode criar ajudantes semelhantes para todos os seus modelos aninhados.

everm1nd
fonte
2

Aqui está uma maneira segura e limpa de fazê-lo funcionar em formulários e em todo o aplicativo que usamos.

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

Então eu tenho na minha forma. A peça adicionada para isso é o distrito de::.

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

Espero que isto ajude.

Jake
fonte
2

A solução mais limpa que encontrei é adicionar o seguinte à classe 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

Ele funciona para todas as subclasses e é muito mais seguro do que substituir todo o objeto de nome do modelo. Ao direcionar apenas as chaves de rota, resolvemos os problemas de roteamento sem interromper o I18n ou arriscar qualquer efeito colateral potencial causado pela substituição do nome do modelo, conforme definido pelo Rails.

Alexander Makarenko
fonte
1

Se eu considerar uma herança STI como esta:

class AModel < ActiveRecord::Base ; end
class BModel < AModel ; end
class CModel < AModel ; end
class DModel < AModel ; end
class EModel < AModel ; end

em 'app / models / a_model.rb', adiciono:

module ManagedAtAModelLevel
  def model_name
    AModel.model_name
  end
end

E então na classe 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

Portanto, posso escolher com facilidade o modelo que desejo usar o padrão, e isso sem sequer tocar na definição da subclasse. Muito seco.

Laurent B.
fonte
1

Desta forma, funciona para mim ok (defina este método na classe 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
fonte
1

Você pode criar um método que retorne um objeto pai fictício para o roteador

class Person < ActiveRecord::Base      
  def routing_object
    Person.new(id: id)
  end
end

e então simplesmente chame form_for @ employee.routing_object que sem type retornará o objeto da classe Person

conviver
fonte
1

Após a resposta @ prathan-thananart e para as várias classes STI, você pode adicionar o seguinte ao modelo pai ->

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

Isso fará com que cada formulário com os dados de contato para enviar parâmetros como params[:contact], em vez de params[:contact_person], params[:contact_whatever].

Zlatko Alomerovic
fonte
-6

hackish, mas apenas mais um na lista de soluções.

class Parent < ActiveRecord::Base; end

Class Child < Parent
  def class
    Parent
  end
end

funciona nos trilhos 2.xe 3.x

Ohad Levy
fonte
5
Isso corrige um problema, mas cria outro. Agora, quando você tenta fazer Child.new, retorna uma Parentclasse em vez da subclasse. Isso significa que você não pode criar as subclasses através da atribuição em massa por meio de um controlador (já que typeé um atributo protegido por padrão), a menos que você também defina o atributo type explicitamente.
31512 Chris Bloom