четверг, ноября 02, 2006

Сборка / разборка данных

Недавно понадобился такой функционал:

В базе в поле типа 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. Будьте осторожны с этим. Вам в конструкторе могут подсунуть аргументов меньше, чем должно быть.

1 комментарий:

Unknown комментирует...

позновательно, спасибо! =)

60 resuce nil - поправь resuce =)