Динамически создавать экземпляр вложенного подкласса STI Rails?

Скажем, у меня есть класс вроде:

class Basket < ActiveRecord::Base
  has_many :fruits

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

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

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 еще не существует).

Я совершенно озадачен. У кого-нибудь есть идеи?


person Dan Barowy    schedule 04.11.2010    source источник


Ответы (2)


Вместо того, чтобы добавлять сеттер-метод в 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 таким образом, так как он защищен от массового назначения.

ИЗМЕНИТЬ

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

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

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
person zetetic    schedule 04.11.2010
comment
Это работает для моих целей. Но если я правильно понимаю, это означает, что любые обратные вызовы, которые у меня могут быть в моих подклассах STI, не будут выполняться, верно? - person Dan Barowy; 05.11.2010

В терминах Ruby "#{x}" просто эквивалентен x.to_s, который для строковых значений точно такой же, как и сама строка. В других языках, таких как 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

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

person tadman    schedule 04.11.2010
comment
Хорошо, это хорошая идея, но я все еще застрял. Корзина, похоже, не ищет класс, потому что мы создаем некий вид фруктов с помощью ассоциации Basket has_many :fruits. Итак, это работает: Basket.fruits.create(params) или Basket.apples.create(params), но не Basket.fruit_class.create(params). - person Dan Barowy; 05.11.2010
comment
Вы бы не создали это через ассоциацию, так как сама ассоциация типизирована. Что вы можете сделать, так это создать дочерний объект с объединенным свойством basket, как в вашем вопросе. - person tadman; 05.11.2010
comment
Но вы не можете этого сделать, потому что Basket.id еще не известен -- Basket еще не сохранен. - person Dan Barowy; 06.11.2010