Динамически создавать вложенные в Rails вложенные STI-подклассы? - PullRequest
1 голос
/ 04 ноября 2010

Допустим, у меня есть такой класс:

class Basket < ActiveRecord::Base
  has_many :fruits

Где "фрукты" - это базовый класс ИППП, имеющий подклассы, такие как "яблоки", "апельсины" и т. Д. *

Я бы хотел иметь возможность метода установки в корзине, как:

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

Но, очевидно, я получаю исключение вроде:

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

Обходной путь, о котором я думал, работает так:

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

Но это вызывает создание экземпляра объекта Fruit STI перед классом Basket, и поэтому ключ basket_id никогда не сохраняется в подклассе Fruit (поскольку basket_id еще не существует).

Я полностью в замешательстве. У кого-нибудь есть идеи?

Ответы [ 2 ]

1 голос
/ 04 ноября 2010

Вместо того, чтобы добавлять метод установки в Basket, добавьте его во Fruit:

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

Теперь вы можете передавать тип при построении объекта через ассоциацию:

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

Обратите внимание, что вы не можете назначить :type таким образом, поскольку он защищен от массового назначения.

EDIT

О, вы хотели запускать различные обратные вызовы в зависимости от подкласса? Верно.

Вы можете сделать это:

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

или определите has_many связь для каждого типа:

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

class Basket
  has_many :apples
end

тогда

fruit_type = "apples"
b = Basket.new
new_fruit = b.send(fruit_type).build
new_fruit.class # Apple
0 голосов
/ 04 ноября 2010

С точки зрения Ruby, "#{x}" просто эквивалентно x.to_s, что для значений String точно такое же, как и сама строка. В других языках, таких как PHP, вы можете отменить ссылку на строку и рассматривать ее как класс, но здесь это не так. Что вы, вероятно, имеете в виду, это:

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

Метод constantize преобразует строку в эквивалентный класс, но он чувствителен к регистру.

Имейте в виду, что вы подвергаете себя риску того, что кто-то может создать что-то с fruit_type, установленным на "users", а затем продолжить и создать учетную запись администратора. Возможно, более ответственным является дополнительная проверка того, что то, что вы делаете, действительно относится к нужному классу.

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

При загрузке классов таким способом следует обратить внимание на то, что constantize не будет автоматически загружать классы, как это делается в вашем приложении. В режиме разработки вы не сможете создавать подклассы, на которые нет явной ссылки. Вы можете избежать этого, используя таблицу сопоставления, которая решает потенциальную проблему безопасности и предварительно загружая все сразу:

fruit_class = Fruit::SUBCLASS_FOR[fruit_type]

Вы можете определить эту константу следующим образом:

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

Использование константы литерального класса в вашей модели приведет к их немедленной загрузке.

...