Как запустить проверки подкласса в Single Table Inheritance? - PullRequest
13 голосов
/ 10 февраля 2012

В моем приложении есть класс 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 происходит из параметров. Поэтому мне нужно сделать что-то вроде:

b = Budget.new(:type => "HourlyRateBudget", :price => 10)
b.valid?
=> true

Что означает, что rails выполняет проверки всуперкласс вместо того, чтобы создавать экземпляр подкласса после того, как я настроил тип.

Я знаю, что это ожидаемое поведение, так как я создаю экземпляр класса, которому не нужно поле количества, но мне интересноесли в любом случае есть указание rails запускать проверки для подкласса вместо супер.

Ответы [ 7 ]

9 голосов
/ 10 февраля 2012

Вероятно, вы могли бы решить эту проблему с помощью пользовательского валидатора, аналогичного ответу на этот вопрос: Две модели, одна STI и валидация Однако, если вы можете просто создать экземпляр предполагаемого подтипа для началав этом случае вы полностью избавились бы от необходимости использования специального валидатора.

Как вы заметили, установка одного поля типа не может волшебным образом изменять экземпляр с одного типа на другой.В то время как ActiveRecord будет использовать поле type для создания экземпляра соответствующего класса при чтении объекта из базы данных, делая это наоборот (создание суперкласса, а затем изменение поля типа вручную) не имеетэффект изменения типа объекта во время работы приложения - он просто не работает таким образом.

С другой стороны, пользовательский метод проверки может независимо проверять поле type, создавать экземпляр копиисоответствующего типа (на основе значения поля type), а затем запустите .valid? для этого объекта, в результате чего проверки на подклассе будут выполняться динамически, даже если этофактически создает в процессе экземпляр соответствующего подкласса.

6 голосов
/ 01 апреля 2014

Я сделал нечто подобное.

Адаптируем его к вашей проблеме:

class Budget < ActiveRecord::Base

    validates_presence_of :price
    validates_presence_of :quantity, if: :hourly_rate?

    def hourly_rate?
        self.class.name == 'HourlyRateBudget'
    end

end
3 голосов
/ 18 февраля 2013

Для тех, кто ищет пример кода, вот как я реализовал первый ответ:

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
2 голосов
/ 10 февраля 2012

Вместо того, чтобы устанавливать тип, прямо установите тип как этот ... Вместо этого попробуйте:

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, в противном случае это будет просто Бюджет.

0 голосов
/ 19 марта 2018

В соответствии с ответом @ 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
0 голосов
/ 28 июня 2016

Мне также потребовалось то же самое, и с помощью ответа Брайса я сделал это:

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
0 голосов
/ 17 сентября 2013

Еще лучше, используйте type.constantize.new("10"), однако это зависит от того, что тип из params должен быть правильной строкой, идентичной HourlyRateBudget.class.to_s

...