Как запустить проверки подкласса в наследовании одной таблицы?

В моем приложении у меня есть класс, который называется 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
Другие вопросы по тегам