Mes Rails vues et contrôleurs sont jonchés de redirect_to
, link_to
, et form_for
appels de méthode. Parfois link_to
et redirect_to
sont explicites dans les chemins qu'ils lient (par exemple link_to 'New Person', new_person_path
), mais souvent les chemins sont implicites (par exemple link_to 'Show', person
).
J'ajoute un héritage de table unique (STI) à mon modèle (disons Employee < Person
), et toutes ces méthodes s'arrêtent pour une instance de la sous-classe (disons Employee
); lorsque Rails exécute link_to @person
, il contient des erreurs avec undefined method employee_path' for #<#<Class:0x000001022bcd40>:0x0000010226d038>
. Rails recherche un itinéraire défini par le nom de classe de l'objet, qui est employé. Ces itinéraires d'employé ne sont pas définis, et il n'y a pas de contrôleur d'employé, donc les actions ne sont pas définies non plus.
Cette question a déjà été posée:
routes.rb
pour mapper les ressources de la sous-classe à la classe parente (map.resources :employees, :controller => 'people'
). La meilleure réponse dans cette même question SO suggère de transtyper chaque objet d'instance dans la base de code en utilisant .becomes
routes.rb
, car cela n'attrape que les ruptures de routage de link_to
et redirect_to
, mais pas de form_for
. Il recommande donc plutôt d'ajouter une méthode à la classe parente pour que les sous-classes mentent sur leur classe. Sonne bien, mais sa méthode m'a donné l'erreur undefined local variable or method `child' for #
.Donc, la réponse qui semble la plus élégante et qui a le plus de consensus (mais ce n'est pas tout que élégant, ni que beaucoup de consensus), est l'ajout des ressources à votre routes.rb
. Sauf que cela ne fonctionne pas pour form_for
. J'ai besoin de clarté! Pour distiller les choix ci-dessus, mes options sont
routes.rb
(et j'espère que je n'ai pas besoin d'appeler form_for sur les sous-classes)Avec toutes ces réponses contradictoires, j'ai besoin d'une décision. Il me semble qu'il n'y a pas de bonne réponse. Est-ce un échec dans la conception de Rails? Si oui, s'agit-il d'un bug qui pourrait être corrigé? Sinon, j'espère que quelqu'un pourra me mettre au clair, expliquer les avantages et les inconvénients de chaque option (ou expliquer pourquoi ce n'est pas une option), et laquelle est la bonne réponse, et pourquoi. Ou existe-t-il une bonne réponse que je ne trouve pas sur le Web?
C'est la solution la plus simple que j'ai pu proposer avec un effet secondaire minimal.
class Person < Contact
def self.model_name
Contact.model_name
end
end
À présent url_for @person
correspondra à contact_path
comme prévu.
Comment ça marche: Les assistants URL s'appuient sur YourModel.model_name
pour réfléchir sur le modèle et générer (parmi beaucoup de choses) des clés de route singulières/plurielles. Ici, Person
signifie essentiellement Je suis comme Contact
mec, demandez-lui .
J'ai eu le même problème. Après avoir utilisé STI, le form_for
la méthode a été envoyée à la mauvaise URL enfant.
NoMethodError (undefined method `building_url' for
J'ai fini par ajouter les routes supplémentaires pour les classes enfants et les pointer vers les mêmes contrôleurs
resources :structures
resources :buildings, :controller => 'structures'
resources :bridges, :controller => 'structures'
Aditionellement:
<% form_for(@structure, :as => :structure) do |f| %>
dans ce cas, la structure est en fait un bâtiment (classe enfant)
Cela semble fonctionner pour moi après avoir fait une soumission avec form_for
.
Je vous suggère de jeter un œil à: https://stackoverflow.com/a/605172/445908 , l'utilisation de cette méthode vous permettra d'utiliser "form_for".
ActiveRecord::Base#becomes
Utilisez le type dans les itinéraires:
resources :employee, controller: 'person', type: 'Employee'
http://samurails.com/tutorial/single-table-inheritance-with-Rails-4-part-2/
Suivre l'idée de @Prathan Thananart mais en essayant de ne rien détruire. (car il y a tellement de magie impliquée)
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
Maintenant, url_for @person sera mappé sur contact_path comme prévu.
J'avais aussi des problèmes avec ce problème et suis venu par cette réponse sur une question similaire à la nôtre. Ça a marché pour moi.
form_for @list.becomes(List)
Réponse affichée ici: en utilisant le chemin STI avec le même contrôleur
Le .becomes
La méthode est définie comme étant principalement utilisée pour résoudre des problèmes d'IST comme votre form_for
une.
.becomes
info ici: http://apidock.com/Rails/ActiveRecord/Base/becomes
Réponse super tardive, mais c'est la meilleure réponse que j'ai pu trouver et cela a bien fonctionné pour moi. J'espère que cela aide quelqu'un. À votre santé!
Ok, j'ai eu une tonne de frustration dans ce domaine de Rails, et suis arrivé à l'approche suivante, peut-être que cela aidera les autres.
Tout d'abord, sachez qu'un certain nombre de solutions au-dessus et autour du net suggèrent d'utiliser la constante sur les paramètres fournis par le client. Il s'agit d'un vecteur d'attaque DoS connu sous la forme Ruby ne détruit pas les symboles de collecte, permettant ainsi à un attaquant de créer des symboles arbitraires et de consommer la mémoire disponible.
J'ai implémenté l'approche ci-dessous qui prend en charge l'instanciation des sous-classes de modèle et est SÉCURISÉE du problème de contantisation ci-dessus. Il est très similaire à ce que fait Rails 4, mais permet également plusieurs niveaux de sous-classement (contrairement à Rails 4) et fonctionne dans 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)
Après avoir essayé différentes approches pour le `` chargement de sous-classe dans le problème de développement '', similaires à ce qui est suggéré ci-dessus, j'ai trouvé que la seule chose qui fonctionnait de manière fiable était d'utiliser `` require_dependency '' dans mes classes de modèle. Cela garantit que le chargement des classes fonctionne correctement pendant le développement et ne cause aucun problème en production. En développement, sans 'require_dependency' AR ne connaîtra pas toutes les sous-classes, ce qui impacte le SQL émis pour la correspondance sur la colonne type. De plus, sans "require_dependency", vous pouvez également vous retrouver dans une situation avec plusieurs versions des classes de modèle en même temps! (par exemple, cela peut se produire lorsque vous modifiez une classe de base ou intermédiaire, les sous-classes ne semblent pas toujours se recharger et restent sous-classées de l'ancienne classe)
# contact.rb
class Contact < ActiveRecord::Base
acts_as_castable
end
require_dependency 'person'
require_dependency 'organisation'
Je ne remplace pas non plus model_name comme suggéré ci-dessus car j'utilise I18n et j'ai besoin de chaînes différentes pour les attributs de différentes sous-classes, par exemple: tax_identifier devient 'ABN' pour Organisation et 'TFN' pour Personne (en Australie).
J'utilise également le mappage d'itinéraire, comme suggéré ci-dessus, en définissant le type:
resources :person, :controller => 'contacts', :defaults => { 'contact' => { 'type' => Person.sti_name } }
resources :organisation, :controller => 'contacts', :defaults => { 'contact' => { 'type' => Organisation.sti_name } }
En plus du mappage de route, j'utilise InheritedResources et SimpleForm et j'utilise l'encapsuleur de formulaire générique suivant pour de nouvelles actions:
simple_form_for resource, as: resource_request_name, url: collection_url,
html: { class: controller_name, multipart: true }
... et pour les actions d'édition:
simple_form_for resource, as: resource_request_name, url: resource_url,
html: { class: controller_name, multipart: true }
Et pour que cela fonctionne, dans ma base ResourceContoller j'expose resource_request_name de InheritedResource comme méthode d'aide pour la vue:
helper_method :resource_request_name
Si vous n'utilisez pas InheritedResources, utilisez quelque chose comme ce qui suit dans votre "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
Toujours heureux d'entendre les expériences et les améliorations des autres.
J'ai récemment documenté mes tentatives pour obtenir un modèle STI stable fonctionnant dans une application Rails 3.0. Voici la version 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
Cette approche contourne les problèmes que vous énumérez ainsi qu'un certain nombre d'autres problèmes que d'autres ont rencontrés avec les approches STI.
Vous pouvez essayer ceci, si vous n'avez pas de routes imbriquées:
resources :employee, path: :person, controller: :person
Ou vous pouvez aller dans un autre sens et utiliser une magie OOP comme décrit ici: https://coderwall.com/p/yijmuq
Dans un deuxième temps, vous pouvez créer des assistants similaires pour tous vos modèles imbriqués.
Voici un moyen propre et sûr de le faire fonctionner dans les formulaires et tout au long de votre application que nous utilisons.
resources :districts
resources :district_counties, controller: 'districts', type: 'County'
resources :district_cities, controller: 'districts', type: 'City'
Ensuite, j'ai sous ma forme. La pièce ajoutée pour cela est le:: district.
= form_for(@district, as: :district, html: { class: "form-horizontal", role: "form" }) do |f|
J'espère que cela t'aides.
Cela fonctionne pour moi ok (définissez cette méthode dans la classe de 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
Si je considère un héritage IST comme celui-ci:
class AModel < ActiveRecord::Base ; end
class BModel < AModel ; end
class CModel < AModel ; end
class DModel < AModel ; end
class EModel < AModel ; end
dans 'app/models/a_model.rb' j'ajoute:
module ManagedAtAModelLevel
def model_name
AModel.model_name
end
end
Et puis dans la 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
Par conséquent, je peux même facilement choisir le modèle que je veux utiliser par défaut, et ce sans même toucher à la définition de la sous-classe. Très sec.
Vous pouvez créer une méthode qui renvoie un objet parent factice pour le routage purpouse
class Person < ActiveRecord::Base
def routing_object
Person.new(id: id)
end
end
puis appelez simplement form_for @ employee.routing_object qui sans type renverra l'objet de classe Person
En suivant la réponse @ prathan-thananart, et pour les multiples classes STI, vous pouvez ajouter ce qui suit au modèle parent ->
class Contact < ActiveRecord::Base
def self.model_name
ActiveModel::Name.new(self, nil, 'Contact')
end
end
Cela fera de chaque formulaire avec des données de contact pour envoyer des paramètres en tant que params[:contact]
au lieu de params[:contact_person]
, params[:contact_whatever]
.