Очень интересный пост по поводу использования разных языков программирования в IT индустрии. Очень рекомендую:
What if there are no gods ?
пятница, декабря 22, 2006
среда, ноября 08, 2006
Работа с данными в ActiveRecord
Недавно я продолжил изучение внутренностей ActiveRecord, потому как для меня до сих пор оставалось загадкой, как же правильно работать с данными в ActiveRecord (например, осуществлять конвертацию).
Начнем с сердца экземпляра 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. Сделаем отдельный аттрибут для текстового представления:
Ну вот, собственно, и все.
Начнем с сердца экземпляра 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
Ну вот, собственно, и все.
четверг, ноября 02, 2006
Сборка / разборка данных
Недавно понадобился такой функционал:
В базе в поле типа integer хранится время события (кол-во минут с начала суток).
Надо организовать редактирование этих данных в привычном для пользователя виде (т.е. часы : минуты). Пришлось залазить в кишки рельсов.
В HTML редактирование времени выглядит как 2 SELECT'а (часы и минуты). Проблема: собрать два этих параметра в одно поле (общее_количество_минут), чтобы потом сохранить его в базе.
В рельсах есть поддержка механизма сборки одного поля из нескольких request-параметров. Она активируется, если поле имеет суффикс вида "(x)", где x - порядковый номер параметра. Если рельсы встречают такие параметры, они их собирают в отдельную кучку и начинают по ним создавать "правильный" тип данных. Для этого они сначала выводят тип (класс) будущего значения, и вызывают у него метод new с соответствующим (соответствующим кол-во параметров в мультипараметре) количеством аргументов.
Теперь надо понять, как подсунуть нужный класс. Класс будет или классом типа данных соответствующего столбца (имеющего то же имя) в базе данных, или именем класса аггрегации. Нас интересует именно эта аггрегация. В рельсах объявление агрегации осуществляется посредством вызова метода composed_of.
Итак начнем писать наш тип данных для времени. Нам понадобится конструктор с одним параметром - число минут с начала суток (данные, которые будут храниться в базе). Также, сразу же объявим ридеры для часов и минут, и метод to_s:
Теперь добавим агрегацию в модель:
Отлично. Теперь при обращении к event.time мы получим экземпляр CalendarTime.
Теперь представление. Надо сделать хелпер который будет выдавать два селекта с хитрыми именами.
Теперь в представлении сделаем вызов этого хелпера:
Отлично, осталось только собрать данные назад.
Как я уже говорил, при обработке мультипараметров ActiveRecord сконструирует агрегирующий объект (в нашем случае - CalendarTime) с соответствующим количеством параметров. Значит надо обработать два параметра в конструкторе CalendarTime. Для этого перепишем его так:
Собственно, все.
Чтобы еще больше понимать механизмы работы рельсов, было полезно узнать, как (и когда) работает отображение данных в composed_of. Так вот, агрегат создается по каждому требованию и ему в качестве аргемнтов конструктора передаются значения всех замапленых атрибутов (перечисленных в :mapping опции) в указанном порядке (поэтому значение этой опции или массив из двух элементов, или массив массивов из двух элементов - чтобы можно было отследить порядок аргументов). Обратное присвоение происходит при присвоении модели нового значения агрегата.
PS есть один gotcha: при сборке агрегата из мультипараметра, рельсы по каким-то причинам отфильтровывают все пустые строки или nil. Будьте осторожны с этим. Вам в конструкторе могут подсунуть аргументов меньше, чем должно быть.
В базе в поле типа integer хранится время события (кол-во минут с начала суток).
Надо организовать редактирование этих данных в привычном для пользователя виде (т.е. часы : минуты). Пришлось залазить в кишки рельсов.
В HTML редактирование времени выглядит как 2 SELECT'а (часы и минуты). Проблема: собрать два этих параметра в одно поле (общее_количество_минут), чтобы потом сохранить его в базе.
В рельсах есть поддержка механизма сборки одного поля из нескольких request-параметров. Она активируется, если поле имеет суффикс вида "(x)", где x - порядковый номер параметра. Если рельсы встречают такие параметры, они их собирают в отдельную кучку и начинают по ним создавать "правильный" тип данных. Для этого они сначала выводят тип (класс) будущего значения, и вызывают у него метод new с соответствующим (соответствующим кол-во параметров в мультипараметре) количеством аргументов.
Теперь надо понять, как подсунуть нужный класс. Класс будет или классом типа данных соответствующего столбца (имеющего то же имя) в базе данных, или именем класса аггрегации. Нас интересует именно эта аггрегация. В рельсах объявление агрегации осуществляется посредством вызова метода composed_of.
Итак начнем писать наш тип данных для времени. Нам понадобится конструктор с одним параметром - число минут с начала суток (данные, которые будут храниться в базе). Также, сразу же объявим ридеры для часов и минут, и метод to_s:
class CalendarTime
attr_reader :minutes_since_midnight
def initialize(minutes)
@minutes_since_midnight = minutes
end
def hour
@minutes_since_midnight / 60 rescue nil
end
def min
@minutes_since_midnight % 60 rescue nil
end
def to_s
"%d:%02d" % [ hour, min ]
end
end
Теперь добавим агрегацию в модель:
class CalendarEvent < ActiveRecord::Base
composed_of :time,
:class_name => 'CalendarTime',
# отображаем AR атрибут time на поле
# minutes_since_midnight нашего класса
:mapping => [:time, :minutes_since_midnight ],
# разрешить nil в качестве значения агрегата
:allow_nil => true
end
Отлично. Теперь при обращении к event.time мы получим экземпляр CalendarTime.
Теперь представление. Надо сделать хелпер который будет выдавать два селекта с хитрыми именами.
module ApplicationHelper
def time_select(object_name, method, options = {})
# Получим сам объект
object = options[:object] ||
instance_variable_get('@'+object_name)
# и значение
value = object.send(method)
# Подготовим опции
options.delete :object
object[:discard_type] = true
# Подготовим шаблон для имени элементов
name = "#{object_name}[#{attr}(%di)]"
# Наш объект CalendarTime поддерживает свойства hour и minute
# поэтому можно будет воспользоваться стандартными хэлперами
select_hour(value, options.merge :prefix => name%1) + ' : ' +
select_minute(value, options.merge :prefix => name%2)
end
end
Теперь в представлении сделаем вызов этого хелпера:
<p>
<label for="event_time">Time</label><br/>
<%= time_select 'event', 'time', :include_blank => true %>
</p>
Отлично, осталось только собрать данные назад.
Как я уже говорил, при обработке мультипараметров ActiveRecord сконструирует агрегирующий объект (в нашем случае - CalendarTime) с соответствующим количеством параметров. Значит надо обработать два параметра в конструкторе CalendarTime. Для этого перепишем его так:
class CalendarTime
def initialize(*args)
if args.size == 1 && args[0].is_a?(Numeric)
@minutes_since_midnight = args[0]
elsif (args.size == 2 &&
args[0].is_a?(Numeric) &&
args[1].is_a?(Numeric))
@minutes_since_midnight = args[0]*60 + args[1]
end
end
end
Собственно, все.
Чтобы еще больше понимать механизмы работы рельсов, было полезно узнать, как (и когда) работает отображение данных в composed_of. Так вот, агрегат создается по каждому требованию и ему в качестве аргемнтов конструктора передаются значения всех замапленых атрибутов (перечисленных в :mapping опции) в указанном порядке (поэтому значение этой опции или массив из двух элементов, или массив массивов из двух элементов - чтобы можно было отследить порядок аргументов). Обратное присвоение происходит при присвоении модели нового значения агрегата.
PS есть один gotcha: при сборке агрегата из мультипараметра, рельсы по каким-то причинам отфильтровывают все пустые строки или nil. Будьте осторожны с этим. Вам в конструкторе могут подсунуть аргументов меньше, чем должно быть.
вторник, октября 03, 2006
Inline layouts
В Rails уже давно есть возможность брать контент экшена не из файла, а передовать его в строке. Таким образом легко сделать простой CMS: берем шаблон из базы и делаем render :inline => . Проблема же построения более менее пригодного CMS заключалась в том, что подобную операцию нельзя было провести с лэйаутами.
Недавно я написал маленький плагин - Nested layouts (http://nested-layouts.rubyforge.org). Я решил, что возможность использования inline лэйаутов будет полезной добавкой к нему.
Так что, кому надо - берите, пользуйте, пишите впечатления.
Недавно я написал маленький плагин - Nested layouts (http://nested-layouts.rubyforge.org). Я решил, что возможность использования inline лэйаутов будет полезной добавкой к нему.
<% inside_inline_layout @layout_template do %>
content
<% end %>
Так что, кому надо - берите, пользуйте, пишите впечатления.
понедельник, октября 02, 2006
IF/UNLESS блоки в RJS шаблонах
Недавние занятия программированием с Ajax в Rails привели к созданию патча для поддержки создания IF блоков в javascript'е. Основная идея (помимо условий в виде строк) - поддержка элемент-прокси:
Проблема в том, что page['element_id'].visible сгенерит код до того, как будет обернуто в IF блок.
Также можно использовать более традиционные методы:
Патч был отклонен в core, но было предложено сделать плангин для обкатки.
Собственно, вот и он: http://rubyforge.org/projects/js-if-blocks. Инсталировать можно так:
Тепрь думаю о добавлении возможности повторного использования proxy объектов, например:
page.if page['element_id'].visible do
page['element_id'].hide
end
Проблема в том, что page['element_id'].visible сгенерит код до того, как будет обернуто в IF блок.
Также можно использовать более традиционные методы:
page.unless "$('element_id').visible()" do
page['element_id'].show
end
Патч был отклонен в core, но было предложено сделать плангин для обкатки.
Собственно, вот и он: http://rubyforge.org/projects/js-if-blocks. Инсталировать можно так:
./script/plugin install svn://rubyforge.org/var/svn/js-if-blocks/trunk/js-if-blocks
Тепрь думаю о добавлении возможности повторного использования proxy объектов, например:
element = page['element_id]
page.if element.visible do
element.hide
end
Подписаться на:
Сообщения (Atom)