Давненько собирался написать про валидацию виртуальных атрибутов в моделях Rails, а тут по случаю опробовал Rails 3, наступил на грабли в данном вопросе, так что повод появился.
Зачем могут понадобиться виртуальные атрибуты модели? Затем, чтобы опять же отделить внутренний способ хранения данных от их отображения, например в базе дата хранится как соответствующий тип Date, а на клиенте должна быть представлена в американском формате mm/dd/yy. Вводить её будут так же. Нужно проверять и только потом конвертировать в свой внутренний формат.
Немного о граблях. Были они вызваны тем, что в коде сеттера для такого виртуального атрибута я делал проверку valid? у модели. Модель пропускалась через валидации, некоторые поля модели к тому времени установлены не были (nil), соответственно валидаторы (в частности, один мой собственный) записывали в массив ошибок сообщение, что поле с нулевым значением неверно. Когда приходило время настоящей валидации, эта ошибка сохранялась, и, несмотря на то, что модель уже валидна, она считалась неверной.
А теперь как надо.
class SomeModel < ActiveRecord::Base
validates :v_field, :presence => true, :custom_format => true
before_save :set_attr
def set_attr
self.field = @v_field.convert_to_internal
end
def v_field
@v_field || field.convert_to_formatted
end
def v_field=(val)
@v_field = val
end
end
class CustomFormatValidator < ActiveModel::EachValidator
def validate_each(object, attribute, value)
real_attr = attribute.to_s.gsub /v_/, ''
begin
if( value.length > 0 )
return
else
object.errors[real_attr.to_sym] << (options[:message] || "is not valid")
end
rescue Exception => e
Rails.logger.add(0, e.to_s )
object.errors[real_attr.to_sym] << (options[:message] || "is not valid")
end
end
end
Теперь всё идёт как надо - форматированное значение лежит в отдельном поле, валидация происходит 1 раз после того, как модель полностью установлена, после валидации - конвертация во внутренний формат.