2010-11-04 6 views
1

Disons que j'ai une classe comme:Instancier dynamiquement la sous-classe STI imbriquée Rails?

class Basket < ActiveRecord::Base 
    has_many :fruits 

Où "fruits" est une classe de base STI ayant comme sous-classes "pommes", "oranges", etc ...

Je voudrais être en mesure d'avoir une méthode setter dans le panier comme:

def fruits=(params) 
    unless params.nil? 
    params.each_pair do |fruit_type, fruit_data| 
     fruit_type.build(fruit_data) 
    end 
    end 
end 

Mais, évidemment, je reçois une exception comme:

NoMethodError (undefined method `build' for "apples":String) 

Une solution que je pensais fonctionne comme ceci:

def fruits=(params) 
    unless params.nil? 
    params.each_pair do |fruit_type, fruit_data| 
     "#{fruit_type}".create(fruit_data.merge({:basket_id => self.id})) 
    end 
    end 
end 

Mais qui cause l'objet STI fruits à instancier avant la classe de panier, et donc la clé de basket_id est jamais enregistré dans la sous-classe Fruit (parce que basket_id n » t existe encore).

Je suis totalement perplexe. Quelqu'un a des idées?

Répondre

1

Au lieu d'ajouter une méthode setter dans le panier, l'ajouter dans Fruit:

class Fruit < ActiveRecord::Base 
    def type_setter=(type_name) 
    self[:type]=type_name 
    end 
end 

Vous pouvez maintenant transmettre le type lorsque vous construisez l'objet à travers une association:

b = Basket.new 
b.fruits.build(:type_setter=>"Apple") 

Notez que vous ne pouvez pas affecter :type de cette façon, car il est protégé contre l'affectation de masse.

EDIT

Oh, vous vouliez exécuter différents callbacks en fonction de la sous-classe? Droite.

Vous pouvez le faire:

fruit_type = "apples" 
b = Basket.new 
new_fruit = b.fruits << fruit_type.titleize.singularize.constantize.new 
new_fruit.class # Apple 

ou définir une association has_many pour chaque type:

require_dependency 'fruit' # assuming Apple is defined in app/models/fruit.rb 

class Basket 
    has_many :apples 
end 

puis

fruit_type = "apples" 
b = Basket.new 
new_fruit = b.send(fruit_type).build 
new_fruit.class # Apple 
+0

Cela fonctionne pour mes fins. Mais si je comprends bien, cela signifie que les rappels que je pourrais avoir sur mes sous-classes STI ne fonctionneront pas, correct? –

+0

Malheureusement, oui. Voir ma réponse éditée si. – zetetic

+0

Génial! Merci beaucoup! –

-1

En termes de Ruby, "#{x}" est simplement équivalent à x.to_s qui, pour les valeurs String, est exactement identique à la chaîne elle-même. Dans d'autres langages, comme PHP, vous pouvez dé-référencer une chaîne et la traiter comme une classe, mais ce n'est pas le cas ici. Qu'est-ce que vous voulez dire sans doute est la suivante:

fruit_class = fruit_type.titleize.singularize.constantize 
fruit_class.create(...) 

La méthode constantize convertit une chaîne à la classe équivalente, mais il est sensible à la casse. Gardez à l'esprit que vous vous exposez à la possibilité que quelqu'un crée quelque chose avec fruit_type réglé sur "users", puis créez un compte administrateur. Ce qui est peut-être plus responsable, c'est de faire un contrôle additionnel que ce que vous faites est en fait de la bonne classe.

fruit_class = fruit_type.titleize.singularize.constantize 
if (fruit_class.superclass == Fruit) 
    fruit_class.create(...) 
else 
    render(:text => "What you're doing is fruitless.") 
end 

Une chose à surveiller lorsque les cours du chargement de cette façon est que constantize ne sera pas des cours de chargement automatique comme ils sont énoncés dans votre application ne. En mode développement, vous pouvez être incapable de créer des sous-classes qui n'ont pas été explicitement référencées. Vous pouvez éviter cela en utilisant une table de correspondance qui permet de résoudre le problème de sécurité potentiel et préchargement à la fois:

fruit_class = Fruit::SUBCLASS_FOR[fruit_type] 

Vous pouvez définir cette constante comme ceci:

class Fruit < ActiveRecord::Base 
    SUBCLASS_FOR = { 
    'apples' => Apple, 
    'bananas' => Banana, 
    # ... 
    'zuchini' => Zuchini 
    } 
end 

Utilisation de la classe littérale constante dans votre modèle aura l'effet de les charger immédiatement.

+0

OK, c'est une bonne idée, mais je suis toujours coincé. Le panier ne semble pas être à la recherche d'une classe, parce que nous créons une sorte de Fruit à travers l'association has_many: fruits. Donc cela fonctionne: Basket.fruits.create (params) ou Basket.apples.create (params) mais pas Basket.fruit_class.create (params). –

+0

Vous ne créez pas ceci à travers l'association puisque l'association elle-même est tapée. Ce que vous pouvez faire est de créer l'objet enfant avec la propriété 'basket' ​​fusionnée, comme vous avez dans votre question. – tadman

+0

Mais vous ne pouvez pas faire cela, car Basket.id n'est pas encore connu - Basket n'a pas encore été enregistré. –

Questions connexes