Как запустить проверки подкласса в наследовании одной таблицы?
В моем приложении у меня есть класс, который называется Budget. Бюджет может быть разных типов. Например, предположим, что существует два бюджета: FlatRateBudget и HourlyRateBudget. Оба наследуют от класса Budget.
Это то, что я так далеко:
class Budget < ActiveRecord::Base
validates_presence_of :price
end
class FlatRateBudget < Budget
end
class HourlyRateBudget < Budget
validates_presence_of :quantity
end
В консоли, если я делаю:
b = HourlyRateBudget.new(:price => 10)
b.valid?
=> false
b.errors.full_messages
=> ["Quantity can't be blank"]
Как и ожидалось.
Проблема в том, что поле "type" в STI происходит от params... Так что мне нужно сделать что-то вроде:
b = Budget.new(:type => "HourlyRateBudget", :price => 10)
b.valid?
=> true
Это означает, что rails выполняет проверки в суперклассе вместо создания экземпляра подкласса после того, как я установил тип.
Я знаю, что это ожидаемое поведение, так как я создаю экземпляр класса, которому не нужно поле количества, но мне интересно, есть ли способ сказать rails запустить валидацию для подкласса вместо супер.
7 ответов
Вероятно, вы могли бы решить эту проблему с помощью специального валидатора, аналогичного ответу на этот вопрос: две модели, одна STI и валидация. Однако, если вы можете просто создать экземпляр предполагаемого подтипа для начала, вы избежите необходимости в настраиваемом валидатор вообще в этом случае.
Как вы заметили, установка только поля типа не может волшебным образом изменить экземпляр с одного типа на другой. В то время как ActiveRecord будет использовать type
поле для создания экземпляра правильного класса при чтении объекта из базы данных, и наоборот, создание экземпляра суперкласса, а затем изменение поля типа вручную) не приводит к изменению типа объекта во время работы приложения. просто так не работает.
Пользовательский метод проверки, с другой стороны, может проверить type
независимо друг от друга, создайте копию соответствующего типа (на основе значения type
поле), а затем запустить .valid?
для этого объекта, что приводит к тому, что проверки подкласса выполняются динамическим образом, даже если в процессе создается экземпляр соответствующего подкласса.
Я сделал что-то подобное.
Адаптируем это к вашей проблеме:
class Budget < ActiveRecord::Base
validates_presence_of :price
validates_presence_of :quantity, if: :hourly_rate?
def hourly_rate?
self.class.name == 'HourlyRateBudget'
end
end
Для тех, кто ищет пример кода, вот как я реализовал первый ответ:
validate :subclass_validations
def subclass_validations
# Typecast into subclass to check those validations
if self.class.descends_from_active_record?
subclass = self.becomes(self.type.classify.constantize)
self.errors.add(:base, "subclass validations are failing.") unless subclass.valid?
end
end
Вместо того, чтобы устанавливать тип, прямо установите тип как этот... Вместо этого попробуйте:
new_type = params.fetch(:type)
class_type = case new_type
when "HourlyRateBudget"
HourlyRateBudget
when "FlatRateBudget"
FlatRateBudget
else
raise StandardError.new "unknown budget type: #{new_type}"
end
class_type.new(:price => 10)
Вы даже можете преобразовать строку в ее класс:new_type.classify.constantize
но если это приходит из параметров, это кажется немного опасным.
Если вы сделаете это, то получите класс HourlyRateBudget, в противном случае это будет просто Budget.
По аналогии с ответом @franzlorenzon, но с использованием утки, чтобы избежать ссылки на тип класса в суперклассе:
class Budget < ActiveRecord::Base
validates_presence_of :price
validates_presence_of :quantity, if: :hourly_rate?
def hourly_rate?
false
end
end
class HourlyRateBudget < Budget
def hourly_rate?
true
end
end
Еще лучше, используйте type.constantize.new("10")
однако это зависит от того, что тип из params должен быть правильной строкой, идентичной HourlyRateBudget.class.to_s
Я также потребовал того же самого и с помощью ответа Брайса я сделал это:
class ActiveRecord::Base
validate :subclass_validations, :if => Proc.new{ is_sti_supported_table? }
def is_sti_supported_table?
self.class.columns_hash.include? (self.class.inheritance_column)
end
def subclass_validations
subclass = self.class.send(:compute_type, self.type)
unless subclass == self.class
subclass_obj= self.becomes(subclass)
self.errors.add(:base, subclass_obj.errors.full_messages.join(', ')) unless subclass_obj.valid?
end
end
end