Best practice per la gestione dei percorsi per le sottoclassi STI nelle rotaie


175

I miei Rails viste e controllori sono disseminati di redirect_to, link_toe form_forchiamate di metodo. A volte link_toe redirect_tosono espliciti nei percorsi che collegano (ad es. link_to 'New Person', new_person_path), Ma molte volte i percorsi sono impliciti (ad es link_to 'Show', person.).

Aggiungo un po 'di ereditarietà a tabella singola (STI) al mio modello (ad esempio Employee < Person) e tutti questi metodi si interrompono per un'istanza della sottoclasse (ad esempio Employee); quando viene eseguita la rota link_to @person, si verifica un errore undefined method employee_path' for #<#<Class:0x000001022bcd40>:0x0000010226d038>. Rails sta cercando una rotta definita dal nome della classe dell'oggetto, che è dipendente. Queste route dei dipendenti non sono definite e non esiste un controller dei dipendenti, pertanto non sono definite nemmeno le azioni.

Questa domanda è stata posta prima:

  1. Su StackOverflow , la risposta è modificare ogni istanza di link_to etc nell'intera base di codice e indicare esplicitamente il percorso
  2. Su StackOverflow di nuovo, due persone suggeriscono di utilizzare routes.rbper mappare le risorse della sottoclasse sulla classe genitore ( map.resources :employees, :controller => 'people'). La risposta migliore in quella stessa domanda SO suggerisce di usare il cast di ogni oggetto di istanza nella base di codice.becomes
  3. Ancora un altro in StackOverflow , la risposta migliore è nel campo Do Repeat Yourself, e suggerisce di creare impalcature duplicate per ogni sottoclasse.
  4. Ecco di nuovo la stessa domanda in SO, dove la risposta migliore sembra essere sbagliata (Rails Magic funziona solo!)
  5. Altrove sul Web, ho trovato questo post sul blog in cui F2Andy consiglia di modificare il percorso ovunque nel codice.
  6. Nel post del blog Ereditarietà a tabella singola e percorsi RESTful in Logical Reality Design, si consiglia di mappare le risorse per la sottoclasse al controller della superclasse, come nella risposta SO numero 2 sopra.
  7. Alex Reisner ha un post Single Table Inheritance in Rails , in cui si propone di non mappare le risorse delle classi secondarie con la classe genitore routes.rb, poiché ciò rileva solo le interruzioni del routing da link_toe redirect_to, ma non da form_for. Quindi raccomanda invece di aggiungere un metodo alla classe genitore per far mentire le sottoclassi sulla loro classe. Sembra buono, ma il suo metodo mi ha dato l'errore undefined local variable or method `child' for #.

Quindi la risposta che sembra più elegante e ha la maggior parte del consenso (ma non è tutto quello elegante, né che tanto consenso), è l'aggiungere le risorse per il vostro routes.rb. Solo che non funziona form_for. Ho bisogno di un po 'di chiarezza! Per distillare le scelte sopra, le mie opzioni sono

  1. mappare le risorse della sottoclasse al controller della superclasse in routes.rb(e spero di non aver bisogno di chiamare form_for su qualsiasi sottoclasse)
  2. Sovrascrivi i metodi interni delle rotaie per far sì che le classi si trovino reciprocamente
  3. Modifica ogni istanza nel codice in cui viene invocato il percorso dell'azione di un oggetto in modo implicito o esplicito, modificando il percorso o eseguendo il cast del tipo dell'oggetto.

Con tutte queste risposte contrastanti, ho bisogno di una sentenza. Mi sembra che non ci sia una buona risposta. È un fallimento nel design delle rotaie? In tal caso, è un bug che può essere corretto? Oppure, in caso contrario, spero che qualcuno possa darmi una dritta su questo, guidarmi attraverso i pro ei contro di ogni opzione (o spiegare perché non è un'opzione), e quale è la risposta giusta e perché. O c'è una risposta giusta che non sto trovando sul web?


1
C'era un refuso nel codice di Alex Reisner, che è stato corretto dopo che ho commentato il suo blog. Quindi speriamo che ora la soluzione di Alex sia praticabile. La mia domanda è ancora valida: qual è la soluzione giusta?
ziggurismo il

1
Anche se ha circa tre anni, ho trovato questo post sul blog su rookieonrails.blogspot.com/2008/01/… e la conversazione collegata dalla mailing list delle rotaie informativa. Uno dei soccorritori descrive la differenza tra aiutanti polimorfici e aiutanti nominati.
ziggurismo il

2
Un'opzione che non elenchi è quella di patchare Rails in modo che link_to, form_for e simili si adattino bene con l'ereditarietà di una singola tabella. Potrebbe essere un lavoro duro, ma è qualcosa che mi piacerebbe vedere risolto.
M. Scott Ford,

Risposte:


140

Questa è la soluzione più semplice che sono riuscito a trovare con un effetto collaterale minimo.

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

Ora url_for @personverrà mappato contact_pathcome previsto.

Come funziona: gli helper URL si affidano YourModel.model_nameper riflettere sul modello e generare (tra le altre cose) chiavi di route singolari / plurali. Qui Personsta sostanzialmente dicendo che sono proprio come Contactamico, chiediglielo .


4
Stavo pensando di fare la stessa cosa, ma ero preoccupato che #model_name potesse essere usato altrove in Rails e che questa modifica potesse interferire con il normale funzionamento. qualche idea?
nkassis,

3
Sono totalmente d'accordo con il misterioso sconosciuto @nkassis. Questo è un bel trucco, ma come fai a sapere che non stai rompendo gli interni delle rotaie?
tsherif,

6
Specifiche. Inoltre, utilizziamo questo codice in produzione e posso attestare che non crea confusione: 1) relazioni tra modelli, 2) istanza del modello STI (via build_x/ create_x). D'altra parte, il prezzo di giocare con la magia è che non sei mai sicuro al 100% di ciò che potrebbe cambiare.
Prathan Thananart,

10
Questo interrompe i18n se stai cercando di avere nomi umani diversi per gli attributi a seconda della classe.
Rufo Sanchez,

4
Invece di ignorare completamente in questo modo, puoi semplicemente ignorare i bit di cui hai bisogno. Vedi gist.github.com/sj26/5843855
sj26

47

Ho avuto lo stesso problema. Dopo aver usato la STI, il form_formetodo stava postando nell'URL figlio sbagliato.

NoMethodError (undefined method `building_url' for

Ho finito per aggiungere i percorsi extra per le classi secondarie e indirizzarle agli stessi controller

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

Inoltre:

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

in questo caso la struttura è in realtà un edificio (classe figlio)

Sembra funzionare per me dopo aver fatto un invio con form_for.


2
Funziona, ma aggiunge molti percorsi inutili nei nostri percorsi. Non c'è un modo per farlo in un modo meno invadente?
Anders Kindberg,

1
È possibile impostare le rotte programmaticamente nel file route.rb, in modo da poter fare una piccola meta programmazione per configurare le rotte figlio. Tuttavia, in ambienti in cui le classi non sono memorizzate nella cache (ad es. Sviluppo) è necessario precaricare prima le classi. Quindi, in un modo o nell'altro, devi specificare le sottoclassi da qualche parte. Vedere gist.github.com/1713398 per un esempio.
Chris Bloom,

Nel mio caso, esporre il nome dell'oggetto (percorso) all'utente non è desiderabile (e confuso per l'utente).
Laffuste,



14

Seguendo l'idea di @Prathan Thananart ma cercando di non distruggere nulla. (dal momento che c'è così tanta magia coinvolta)

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

Ora url_for @person eseguirà il mapping su contact_path come previsto.


14

Anch'io ho avuto problemi con questo problema e sono arrivato da questa risposta su una domanda simile alla nostra. Ha funzionato per me.

form_for @list.becomes(List)

Risposta mostrata qui: utilizzo del percorso STI con lo stesso controller

Il .becomesmetodo è definito come utilizzato principalmente per risolvere problemi STI come il tuo form_for.

.becomesinformazioni qui: http://apidock.com/rails/ActiveRecord/Base/becomes

Risposta super in ritardo, ma questa è la risposta migliore che ho trovato e ha funzionato bene per me. Spero che questo aiuti qualcuno. Saluti!


5

Ok, ho avuto molta frustrazione in quest'area di Rails e sono arrivato al seguente approccio, forse questo aiuterà gli altri.

Innanzitutto, tieni presente che una serie di soluzioni sopra e intorno alla rete suggerisce l'utilizzo di parametri di costante su client. Questo è un vettore di attacco DoS noto poiché Ruby non raccoglie immondizia simboli, consentendo così a un utente malintenzionato di creare simboli arbitrari e consumare memoria disponibile.

Ho implementato l'approccio seguente che supporta l'istanza delle sottoclassi dei modelli ed è SICURO dal problema di contante sopra. È molto simile a quello di Rails 4, ma consente anche più di un livello di sottoclasse (a differenza di Rails 4) e funziona in 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)

Dopo aver provato vari approcci per il "caricamento della sottoclasse nella questione dello sviluppo" molti simili a ciò che è stato suggerito sopra, ho scoperto che l'unica cosa che ha funzionato in modo affidabile è stata l'uso di "demand_dependency" nelle mie classi di modello. Ciò garantisce che il caricamento delle classi funzioni correttamente nello sviluppo e non causi problemi nella produzione. In fase di sviluppo, senza AR "request_dependency" non si conoscono tutte le sottoclassi, il che influisce sull'SQL emesso per la corrispondenza sulla colonna del tipo. Inoltre, senza "request_dependency" puoi anche finire in una situazione con più versioni delle classi del modello contemporaneamente! (ad es. questo può accadere quando si cambia una classe base o intermedia, le sottoclassi non sembrano sempre ricaricare e rimangono sottoclassi dalla vecchia classe)

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

require_dependency 'person'
require_dependency 'organisation'

Inoltre non sovrascrivo nome_modello come suggerito sopra perché utilizzo I18n e ho bisogno di stringhe diverse per gli attributi di diverse sottoclassi, ad esempio: tax_identifier diventa 'ABN' per Organizzazione e 'TFN' per Persona (in Australia).

Uso anche la mappatura del percorso, come suggerito sopra, impostando il tipo:

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

Oltre alla mappatura del percorso, sto usando InheritedResources e SimpleForm e utilizzo il seguente wrapper di modulo generico per nuove azioni:

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

... e per le azioni di modifica:

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

E per far funzionare tutto questo, nella mia base ResourceContoller espongo InheritedResource nome_rischio_risorse come metodo di supporto per la vista:

helper_method :resource_request_name 

Se non stai usando InheritedResources, usa qualcosa come il seguente in '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 felice di ascoltare altre esperienze e miglioramenti.


Nella mia esperienza (almeno in Rails 3.0.9), la costante non riesce se la costante indicata dalla stringa non esiste già. Quindi, come può essere usato per creare nuovi simboli arbitrari?
Lex Lindsey,

4

Di recente ho documentato i miei tentativi di far funzionare un modello STI stabile in un'app Rails 3.0. Ecco la versione 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

Questo approccio aggira i problemi che elenchi e una serie di altri problemi che altri hanno avuto con gli approcci STI.


2

Puoi provare questo, se non hai percorsi nidificati:

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

Oppure puoi andare in un altro modo e usare un po 'di magia OOP come descritto qui: https://coderwall.com/p/yijmuq

In un secondo modo puoi creare helper simili per tutti i tuoi modelli nidificati.


2

Ecco un modo sicuro e sicuro per farlo funzionare nei moduli e in tutta l'applicazione che utilizziamo.

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

Quindi ho nella mia forma. Il pezzo aggiunto per questo è il distretto di as::.

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

Spero che questo ti aiuti.


2

La soluzione più pulita che ho trovato è quella di aggiungere quanto segue alla 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

Funziona con tutte le sottoclassi ed è molto più sicuro che sovrascrivere l'intero oggetto nome modello. Mirando solo alle chiavi del percorso, risolviamo i problemi di routing senza rompere I18n o rischiare eventuali effetti collaterali causati dalla sostituzione del nome del modello come definito da Rails.


1

Se considero un'eredità STI come questa:

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

in 'app / models / a_model.rb' aggiungo:

module ManagedAtAModelLevel
  def model_name
    AModel.model_name
  end
end

E poi nella 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

Pertanto, posso anche facilmente scegliere quale modello desidero utilizzare quello predefinito e questo senza nemmeno toccare la definizione della sottoclasse. Molto secco.


1

In questo modo funziona per me ok (definire questo metodo nella 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

1

È possibile creare un metodo che restituisca un oggetto Parent fittizio per il routing di Purpouse

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

e quindi chiama semplicemente form_for @ employee.routing_object che senza tipo restituirà l'oggetto classe Person


1

Dopo la risposta di @ prathan-thananart e per le diverse classi STI, è possibile aggiungere quanto segue al modello padre ->

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

Che renderà ogni modulo con i dati di contatto per inviare params come params[:contact]invece params[:contact_person], params[:contact_whatever].


-6

hackish, ma solo un altro per l'elenco delle soluzioni.

class Parent < ActiveRecord::Base; end

Class Child < Parent
  def class
    Parent
  end
end

funziona su binari 2.xe 3.x


5
Questo risolve un problema, ma ne crea un altro. Ora quando provi a farlo Child.newrestituisce una Parentclasse anziché la sottoclasse. Ciò significa che non è possibile creare le sottoclassi tramite assegnazione di massa tramite un controller (poiché typeper impostazione predefinita è un attributo protetto) a meno che non si imposti anche l'attributo type in modo esplicito.
Chris Bloom,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.