Начнем с сердца экземпляра ActiveRecord - переменной @attributes. Это то самое хранилище, где хранятся все данные экземпляра AR. По сути, это хэш "атрибут => значение" и именно его (и ничего больше) инициализирует класс, вынимая запись из базы данных.
Но изменять значения напрямую - не самая хорошая идея. Для доступа к значениям есть методы read_attribute(name) / write_attribute(name, value). Помимо вынимания/засовывания значения из/в @attributes, эти методы делают некоторые преобразования:
- read_attribute осуществляет приведение типа к типу, соответствующему типу столбца БД (так, например, данные sql типа данных date превращаются в экземпляры класса Date). Также этот метод осуществляет десериализацию данных из YAML (помните еще о такой возможности ? =)). Если атрибуту не соответствует столбец базы данных - значение возвращается как есть.
- write_attribute проще: он преобразовывает boolean данные в числа, пустую строку - в nil для числовых столбцов.
Для этой парочки есть укороченные варианты: [] / []=. Т.е. если вы переопределили аццессор для вашего атрибута, то читать / писать данные внутри аццессора надо посредством этих "индексных" методов.
Далее, есть свойство attributes. При чтении, оно, в принципе, просто возвращает копии всех атрибутов (не пытайтесь менять значения того, что вернул attributes - сам экземпляр AR не изменится), прогнанных сквозь read_attribute (т.е. данные с правильными типами и десериализованные). Также этот метод поддерживает опции, позволяющие исключить некоторые атрибуты или оставить конкретные из них (опции :except и :only).
Врайтер attributes= или же направляет значение соответствующему врайтеру атрибута (например, firstname=), если это обычный атрибут, или же собирает мультиатрибут (тот, элементы которыго, имеют одинаковое имя и суффикс в виле порякового номера (и опционально - типа данных) в круглых скобках).
И теперь самое интересное: каждый атрибут поддерживает _before_type_cast ридер. Этот ридер возвращает значение соответствующего ключа @attributes напрямую, минуя преобразование данных как в read_attribute.
Выводы:
* нельзя получить значения частей мультиатрибута (потому что они не сохраняются в @attributes. В предыдущей статье я описывал один из способов сделать сборку / разборку данных посредством composed_of. Так вот в этом способе нельзя сохранить на форме введенные неправильные данные.
* при обновлении атрибутов можно получить сырые обновленные данные с поправкой на то, что для числовых столбцов True/False будет преобразован в 1/0, пустая строка - в nil, посредством _before_type_cast аццессора.
Теперь попробуем переделать предыдущий пример с временем в календаре: сделаем так, чтобы можно было редактировать время как текст HH:MM. Для этого:
1. Сделаем отдельный аттрибут для текстового представления:
class Event < ActiveRecord::Base
def time_text
"%d:%02d" % [self.time/60, self.time%60]
end
def time_text=(value)
self.time = begin
parts = value.split ':'
parts[0].to_i*60+parts.to_i
rescue
nil
end
end
end
Отлично! Теперь делаем на форме text_field :event, :time_text и оно уже отображается. Но он стирает текст, если в нем есть ошибки. Хотелось бы сохранять его.
2. Надо сохранять несконвертированное значение:
class Event < ActiveRecord::Base
def time_text
self[:time_text] || ("%d:%02d" % [self.time/60, self.time%60])
end
def time_text=(value)
self[:time_text] = value
self.time = begin
parts = value.split ':'
parts[0].to_i*60+parts[1].to_i
rescue
nil
end
end
end
text_field один из немногих (если не единственный) хелперов, которые используют _before_type_cast аццессоры. Теперь, поскольку мы сохранили неконвертированное значение, он сможет вывести его, если возникли ошибки.
Теперь надо подумать про валидацию:
class Event < ActiveRecord::Base
validates_format_of :time_text, :with => /\d?\d\:\d\d/
end
Ну вот, собственно, и все.