КулЛиб - Скачать fb2 - Читать онлайн - Отзывы  

Путь Rails. Подробное руководство по созданию приложений в среде Ruby on Rails (pdf)

Книга в формате pdf! Изображения и текст могут не отображаться!


Настройки текста:



The Rails Way

Obie Fernandez

Путь Rails
Подробное руководство
по созданию приложений
в среде Ruby on Rails
Оби Фернандес

Санкт-Петербург – Москва
2009

Серия «High tech»

Оби Фернандес

Путь Rails. Подробное руководство
по cозданию приложений в среде Ruby on Rails
Перевод А. Слинкина
Главный редактор
Зав. редакцией
Выпускающий редактор
Редактор
Корректор
Верстка
Художник

А. Галунов
Н. Макарова
Л. Пискунова
Е. Бекназарова
Т. Золотова
Н. Пискунова
В. Гренда

Фернандес О.
Путь Rails. Подробное руководство по созданию приложений в среде Ruby on
Rails. – Пер. с англ. – СПб: Символ-Плюс, 2009. – 768 с., ил.
ISBN­13: 978-5-93286-137-0
ISBN­10: 5-93286-137-1
Среда Ruby on Rails стремительно занимает ведущее место в ряду наиболее
популярных платформ для разработки веб-приложений. Она основана на одном
из самых элегантных языков программирования, Ruby, и доставляет истинное
удовольствие своим приверженцам. Хотите оказаться в первых рядах? Тогда
эта книга для вас! Ее автор, Оби Фернандес, и целая группа экспертов подробно
описывают основные возможности и подсистемы Rails: контроллеры, маршрутизацию, поддержку стиля REST, объектно-реляционное отображение с помощью библиотеки ActiveRecord, применение технологии AJAX в Rails-приложениях и многое другое. Отталкиваясь от своего уникального опыта и приводя
подробные примеры кода, Оби демонстрирует, как с помощью инструментов и
рекомендованных методик Rails добиться максимальной продуктивности и
получать наслаждение от создания совершенных приложений.
ISBN­13: 978-5-93286-137-0
ISBN­10: 5-93286-137-1
ISBN 0-321-44561-9 (англ)
© Издательство Символ-­Плюс, 2009
Authorized translation from the English language edition, entitled RAILS WAY, THE, 1st
Edition, ISBN 0321445619, by FERNANDEZ, OBIE, published by Pearson Education, Inc,
publishing as Addison Wesley Professional, Copyright © 2008 Pearson Education, Inc.
All rights reserved. No part of this book may be reproduced or transmitted in any form
or by any means, electronic or mechanical, including photocopying, recording or by any
information storage retrieval system, without permission from Pearson Education,
Inc. Russian language edition published by SYMBOL-PLUS PUBLISHING LTD,
Copyright © 2009.
Все права на данное издание защищены Законодательством РФ, включая право на пол­ное или час­тич­
ное воспроизведение в любой форме. Все товарные знаки или за­ре­­­­­гист­ри­ро­ван­ные то­вар­ные зна­ки,
упоминаемые в настоящем издании, являются собст­вен­­­ностью со­от­вет­ст­ву­­­ю­­­щих фирм.

Издательство «Символ-­Плюс». 199034, Санкт­-Петербург, 16 линия, 7,
тел. (812) 324­5353, www.symbol.ru. Лицензия ЛП N 000054 от 25.12.98.
Подписано в печать 23.10.2008. Формат 70х100 1/16. Печать офсетная.
Объем 48 печ. л. Тираж 2000 экз. Заказ N
Отпечатано с готовых диапозитивов в ГУП «Типография «Наука»
199034, Санкт-­Петербург, 9 линия, 12.

Дези – моей любимой, подруге, музе.

Оглавление
Предисловие........................................................................... 22
Благодарности........................................................................ 22
Об авторе............................................................................... 26
Введение................................................................................ 27
1. Среда и конфигурирование Rails............................................. 38
Запуск.....................................................................................39
Параметры среды по умолчанию.............................................39
Начальная загрузка.............................................................. 40
Пакеты RubyGem.................................................................. 42
Инициализатор.................................................................... 42
Подразумеваемые пути загрузки............................................. 42
Rails, модули и код автозагрузки............................................ 43
Встройка Rails Info............................................................... 44
Конфигурирование................................................................ 45
Дополнительные конфигурационные параметры....................... 49
Режим разработки..................................................................... 49
Динамическая перезагрузка классов. ...................................... 50
Загрузчик классов в Rails....................................................... 50
Режим тестирования................................................................. 52
Режим эксплуатации................................................................. 52
Протоколирование.................................................................... 53
Протоколы Rails................................................................... 55
Анализ протоколов............................................................... 56
Syslog.................................................................................. 58
Заключение..............................................................................59

2. Работа с контроллерами.......................................................... 60
Диспетчер: с чего все начинается................................................. 61
Обработка запроса................................................................. 61
Познакомимся с диспетчером поближе.................................... 62

Оглавление



Рендеринг представления........................................................... 64
Если сомневаетесь, рисуйте.................................................... 64
Явный рендеринг.................................................................. 65
Рендеринг шаблона другого действия...................................... 65
Рендеринг совершенно постороннего шаблона...........................66
Рендеринг подшаблона.......................................................... 67
Рендеринг встроенного шаблона.............................................. 67
Рендеринг текста.................................................................. 67
Рендеринг структурированных данных других типов................. 68
Пустой рендеринг................................................................. 68
Параметры рендеринга.......................................................... 68
Переадресация.......................................................................... 71
Коммуникация между контроллером и представлением................. 74
Фильтры.................................................................................. 75
Наследование фильтров......................................................... 76
Типы фильтров..................................................................... 77
Упорядочение цепочки фильтров............................................ 78
Aroundфильтры................................................................... 78
Пропуск цепочки фильтров.................................................... 80
Условная фильтрация........................................................... 80
Прерывание цепочки фильтров............................................... 81
Потоковая отправка.................................................................. 81
send_data(data, options = {}).................................................... 81
send_file(path, options = {})..................................................... 82
Как заставить сам вебсервер отправлять файлы........................ 85
Заключение.............................................................................. 86

3. Маршрутизация...................................................................... 87
Две задачи маршрутизации........................................................ 88
Связанные параметры................................................................90
Метапараметры («приемники»)...................................................91
Статические строки...................................................................91
Файл routes.rb..........................................................................93
Маршрут по умолчанию......................................................... 94
О поле :id.............................................................................95
Генерация маршрута по умолчанию........................................96
Модификация маршрута по умолчанию...................................97
Предпоследний маршрут и метод respond_to................................. 97
Метод respond_to и заголовок HTTPAccept.............................. 98
Пустой маршрут.......................................................................99
Самостоятельное создание маршрутов........................................ 100
Использование статических строк............................................. 100
Использование собственных «приемников»................................ 101



Оглавление

Замечание о порядке маршрутов............................................... 102
Применение регулярных выражений в маршрутах...................... 103
Параметры по умолчанию и метод url_for................................... 104
Что случилось с :id.............................................................. 105
Использование литеральных URL. ............................................ 106
Маскирование маршрутов........................................................ 106
Маскирование пар ключ/значение............................................. 107
Именованные маршруты.......................................................... 108
Создание именованного маршрута......................................... 108
Что лучше: name_path или name_url?.................................... 108
Замечания......................................................................... 109
Как выбирать имена для маршрутов.......................................... 109
Синтаксическая глазурь...................................................... 111
Еще немного глазури?......................................................... 111
Метод организации контекста with_options................................. 112
Заключение............................................................................ 113

4. REST, ресурсы и Rails............................................................. 114
О REST в двух словах............................................................... 115
REST в Rails........................................................................... 116
Маршрутизация и CRUD.......................................................... 117
Ресурсы и представления......................................................... 118
Ресурсы REST и Rails.......................................................... 118
От именованных маршрутов к поддержке REST...................... 119
И снова о глаголах HTTP...................................................... 120
Стандартные REST-совместимые действия контроллеров.............. 121
Хитрость для методов PUT и DELETE.................................... 122
Одиночные и множественные
RESTсовместимые маршруты.............................................. 123
Специальные пары: new/create и edit/update.......................... 123
Одиночные маршруты к ресурсам.............................................. 124
Вложенные ресурсы................................................................ 125
Явное задание :path_prefix................................................... 127
Явное задание :name_prefix.................................................. 127
Явное задание RESTсовместимых контроллеров..................... 129
А теперь все вместе.............................................................. 129
Замечания......................................................................... 131
О глубокой вложенности...................................................... 131
Настройка REST-совместимых маршрутов.................................. 133
Маршруты к дополнительным действиям............................... 133
Дополнительные маршруты к наборам................................... 134
Замечания......................................................................... 134
Ресурсы, ассоциированные только с контроллером...................... 136

Оглавление



Различные представления ресурсов........................................... 138
Метод respond_to................................................................ 138
Форматированные именованные маршруты............................ 139
Набор действий в Rails для REST............................................... 139
index................................................................................. 140
show.................................................................................. 143
destroy.............................................................................. 143
new и create........................................................................ 144
edit и update....................................................................... 146
Заключение............................................................................ 146

5. Размышления о маршрутизации в Rails................................. 147
Исследование маршрутов в консоли приложения......................... 147
Распечатка маршрутов........................................................ 148
Анатомия объекта Route...................................................... 149
Распознавание и генерация с консоли.................................... 151
Консоль и именованные маршруты........................................ 153
Тестирование маршрутов......................................................... 153
Подключаемый модуль Routing Navigator.................................. 155
Заключение............................................................................ 156

6. Работа с ActiveRecord............................................................ 157
Основы.................................................................................. 158
Миграции.............................................................................. 160
Создание миграций............................................................. 161
Migration API..................................................................... 164
Определение колонок.......................................................... 166
Методы в стиле макросов.......................................................... 171
Объявление отношений........................................................ 172
Примат соглашения над конфигурацией................................ 173
Приведение к множественному числу.................................... 173
Задание имен вручную......................................................... 175
Унаследованные схемы именования...................................... 175
Определение атрибутов............................................................ 176
Значения атрибутов по умолчанию........................................ 177
Сериализованные атрибуты.................................................. 179
CRUD: создание, чтение, обновление, удаление........................... 179
Создание новых экземпляров ActiveRecord............................. 179
Чтение объектов ActiveRecord.............................................. 180
Чтение и запись атрибутов................................................... 182
Доступ к атрибутам и манипулирование ими
до приведения типов............................................................ 184
Перезагрузка..................................................................... 185

10

Оглавление

Динамический поиск по атрибутам....................................... 185
Специальные SQLзапросы................................................... 186
Кэш запросов..................................................................... 187
Обновление........................................................................ 189
Обновление с условием........................................................ 190
Обновление конкретного экземпляра..................................... 191
Обновление конкретных атрибутов.......................................... 191
Вспомогательные методы обновления.................................... 192
Контроль доступа к атрибутам.............................................. 192
Удаление и уничтожение..................................................... 193
Блокировка базы данных......................................................... 194
Оптимистическая блокировка............................................... 194
Пессимистическая блокировка............................................. 196
Замечание.......................................................................... 197
Дополнительные средства поиска.............................................. 197
Условия............................................................................. 198
Упорядочение результатов поиска......................................... 199
Параметры limit и offset...................................................... 200
Параметр select................................................................... 201
Параметр from.................................................................... 201
Группировка...................................................................... 202
Параметры блокировки....................................................... 202
Соединение и включение ассоциаций..................................... 202
Параметр readonly.............................................................. 203
Соединение с несколькими базами данных в разных моделях........ 203
Прямое использование соединений с базой данных...................... 204
Модуль DatabaseStatements.................................................. 204
Другие методы объекта connection......................................... 206
Другие конфигурационные параметры....................................... 208
Заключение............................................................................ 209

7. Ассоциации в ActiveRecord.................................................... 211
Иерархия ассоциаций.............................................................. 211
Отношения один-ко-многим..................................................... 213
Добавление ассоциированных объектов в набор....................... 215
Методы класса AssociationCollection...................................... 215
Ассоциация belongs_to............................................................. 218
Перезагрузка ассоциации..................................................... 218
Построение и создание связанных объектов через ассоциацию.... 219
Параметры метода belongs_to............................................... 220
Ассоциация has_many.............................................................. 225
Параметры метода has_many................................................ 225
Методы проксиклассов....................................................... 232

Оглавление

11

Отношения многие-ко-многим.................................................. 233
Метод has_and_belongs_to_many........................................... 233
Конструкция has_many :through........................................... 240
Параметры ассоциации has_many :through............................. 244
Отношения один-к-одному........................................................ 247
Ассоциация has_one............................................................ 247
Параметры ассоциации has_one............................................ 249
Несохраненные объекты и ассоциации....................................... 251
Ассоциации одинкодному.................................................. 251
Наборы.............................................................................. 252
Расширения ассоциаций.......................................................... 252
Класс AssociationProxy............................................................ 253
Методы reload и reset........................................................... 253
Методы proxy_owner, proxy_reflection и proxy_target.............. 253
Заключение............................................................................ 255

8. Валидаторы в ActiveRecord.................................................... 256
Нахождение ошибок................................................................ 256
Простые декларативные валидаторы.......................................... 257
validates_acceptance_of........................................................ 257
validates_associated............................................................. 258
validates_confirmation_of.................................................... 258
validates_each..................................................................... 259
validates_inclusion_of и validates_exclusion_of........................ 259
validates_existence_of.......................................................... 260
validates_format_of............................................................. 261
validates_length_of. ............................................................ 262
validates_numericality_of..................................................... 262
validates_presence_of........................................................... 262
validates_uniqueness_of....................................................... 263
Исключение RecordInvalid................................................... 264
Общие параметры валидаторов.................................................. 264
:allow_nil........................................................................... 265
:if..................................................................................... 265
:message............................................................................ 265
:on.................................................................................... 265
Условная проверка.................................................................. 266
Замечания по поводу применения......................................... 266
Работа с объектом Errors.......................................................... 267
Манипулирование набором Errors......................................... 268
Проверка наличия ошибок................................................... 268
Нестандартный контроль......................................................... 268
Отказ от контроля................................................................... 270
Заключение............................................................................ 271

12

Оглавление

9. Дополнительные возможности ActiveRecord.......................... 272
Обратные вызовы.................................................................... 272
Регистрация обратного вызова.............................................. 273
Парные обратные вызовы before/after................................... 274
Прерывание выполнения..................................................... 275
Примеры применения обратных вызовов................................ 275
Особые обратные вызовы: after_initialize и after_find.............. 278
Классы обратных вызовов.................................................... 279
Наблюдатели.......................................................................... 282
Соглашения об именовании.................................................. 282
Регистрация наблюдателей.................................................. 283
Момент оповещения............................................................ 283
Наследование с одной таблицей................................................. 283
Отображение наследования на базу данных............................ 285
Замечания об STI................................................................ 287
STI и ассоциации................................................................ 288
Абстрактные базовые классы моделей........................................ 290
Полиморфные отношения has_many.......................................... 291
Случай модели с комментариями.......................................... 291
Замечание об ассоциации has_many....................................... 294
Модули как средство повторного использования
общего поведения.................................................................... 294
Несколько слов об области видимости класса и контекстах....... 297
Обратный вызов included..................................................... 298
Модификация классов ActiveRecord во время выполнения............ 299
Замечания......................................................................... 300
Ruby и предметноориентированные языки............................ 301
Заключение............................................................................ 302

10. ActionView............................................................................ 303
Основы ERb............................................................................ 304
Практикум по ERb.............................................................. 304
Удаление пустых строк из вывода ERb................................... 306
Закомментирование ограничителей ERb................................ 306
Условный вывод................................................................. 306
RHTML? RXML? RJS?......................................................... 307
Макеты и шаблоны.................................................................. 307
Подстановка содержимого.................................................... 308
Переменные шаблона.......................................................... 310
Защита целостности представления от данных,
введенных пользователем.................................................... 313
Подшаблоны.......................................................................... 314
Простые примеры............................................................... 314
Повторное использование подшаблонов................................. 316

Оглавление

13

Разделяемые подшаблоны.................................................... 316
Передача переменных подшаблонам...................................... 317
Рендеринг наборов.............................................................. 319
Протоколирование.............................................................. 320
Кэширование.......................................................................... 320
Кэширование в режиме разработки?...................................... 321
Кэширование страниц......................................................... 321
Кэширование действий........................................................ 321
Кэширование фрагментов.................................................... 323
Истечение срока хранения кэшированного содержимого.......... 326
Автоматическая очистка кэша с помощью дворников.............. 328
Протоколирование работы кэша............................................ 329
Подключаемый модуль Action Cache...................................... 329
Хранилища для кэша.......................................................... 330
Заключение............................................................................ 332

11. Все о помощниках................................................................. 333
Модуль ActiveRecordHelper...................................................... 333
Отчет об ошибках контроля.................................................. 334
Автоматическое создание формы........................................... 335
Настройка выделения ошибочных полей................................ 338
Модуль AssetTagHelper............................................................ 339
Помощники для формирования заголовка.............................. 339
Только для подключаемых модулей:
добавление включаемых по умолчанию JavaScriptсценариев..... 343
Модуль BenchmarkHelper......................................................... 343
Модуль CacheHelper................................................................ 343
Модуль CaptureHelper.............................................................. 344
Модуль DateHelper.................................................................. 345
Помощники для выбора даты и времени................................. 345
Помощники для задания отдельных элементов
даты и времени................................................................... 346
Параметры, общие для всех помощников,
связанных с датами............................................................. 349
Методы distance_in_time со сложными именами..................... 349
Модуль DebugHelper................................................................ 351
Модуль FormHelper................................................................. 351
Создание форм для моделей ActiveRecord............................... 351
Как помощники формы получают свои значения..................... 358
Модуль FormOptionsHelper....................................................... 359
Помощники select............................................................... 359
Другие помощники............................................................. 361
Модуль FormTagHelper............................................................ 365

14

Оглавление

Модуль JavaScriptHelper.......................................................... 368
Модуль NumberHelper............................................................. 370
Модуль PaginationHelper.......................................................... 372
will_paginate...................................................................... 372
paginator........................................................................... 373
Paginating Find................................................................... 374
Модуль RecordIdentificationHelper............................................ 374
Модуль RecordTagHelper.......................................................... 375
Модуль TagHelper................................................................... 376
Модуль TextHelper.................................................................. 378
Модуль UrlHelper.................................................................... 384
Написание собственных модулей............................................... 390
Мелкие оптимизации: помощник Title................................... 390
Инкапсуляция логики представления: помощник photo_for..... 391
Более сложное представление: помощник breadcrumbs............. 392
Обертывание и обобщение подшаблонов..................................... 393
Помощник tiles................................................................... 393
Обобщение подшаблонов...................................................... 396
Заключение............................................................................ 399

12. Ajax on Rails.......................................................................... 400
Библиотека Prototype.............................................................. 401
Подключаемый модуль FireBug............................................ 402
Prototype API..................................................................... 403
Функции верхнего уровня.................................................... 403
Объект Class....................................................................... 405
Расширения класса JavaScript Object.................................... 406
Расширения класса JavaScript Array..................................... 407
Расширения объекта document............................................. 408
Расширения класса Event.................................................... 409
Расширения класса JavaScript Function................................. 410
Расширения класса JavaScript Number.................................. 412
Расширения класса JavaScript String.................................... 413
Объект Ajax....................................................................... 415
Объект Ajax.Responders....................................................... 415
Объект Enumerable.............................................................. 416
Класс Hash......................................................................... 421
Объект ObjectRange............................................................. 422
Объект Prototype................................................................ 422
Модуль PrototypeHelper........................................................... 422
link_to_remote.................................................................... 422
remote_form_for................................................................. 426
periodically_call_remote....................................................... 427

Оглавление

15

observe_field...................................................................... 428
observe_form...................................................................... 429
RJS – пишем Javascript на Ruby................................................ 429
RJSшаблоны..................................................................... 431

=>
>>
=>

Time.now
Mon Nov 27 16:32:51 -0500 2006
Time.now.getgm
Mon Nov 27 21:32:56 UTC 2006

На консоль все выводится правильно. Значит, написать собственный
метод преобразования будет несложно, правда?
В сообщении, отправленном в список рассылки rails-mailing-list в августе 2006 года2, Гжегош Данилюк (Grzegorz Daniluk) привел пример,
показывающий, как это сделать (и продемонстрировал, что он работает от 7 до 9 раз быстрее TZInfo). Добавьте следующий код в любой модуль-помощник своего приложения или оформите его в виде отдельного класса в папке lib:
# Чтобы преобразовать полученное дату и время в UTC и сохранить в БД
def user2utc(t)
ENV["TZ"] = current_user.time_zone_name
res = Time.local(t.year, t.month, t.day, t.hour, t.min, t.sec).utc
ENV["TZ"] = "UTC"
res
end
# Чтобы отобразить дату и время
def utc2user(t)
ENV["TZ"] = current_user.time_zone_name
res = t.getlocal
ENV["TZ"] = "UTC"
res
end

Филип Росс (Philip Ross), автор TZInfo, в том же списке рассылки поместил подробный ответ, показывающий, что это решение не работает для
пользователей на платформе Windows3. Он также привел комментарии
1
2
3

http://tzinfo.rubyforge.org/
www.ruby-forum.com/topic/79431
http://article.gmane.org/gmane.comp.lang.ruby.rails/75790

Режим разработки

49

по поводу обработки некорректно заданного времени: «Еще один аспект,
в котором TZInfo лучше, чем использование переменной окружения TZ,
связан с обработкой некорректно и неоднозначно заданного локального
времени (например, при переходе на летнее время и обратно). Time.local
всегда возвращает время, пусть даже оно некорректно или неоднозначно. TZInfo сообщает о некорректно заданном времени и позволяет разрешить неоднозначность, указав, следует ли использовать летнее или
обычное время, или выполнив блок, в котором производится выбор».
Короче говоря, не пользуйтесь Windows. Шучу, шучу. Из всего это
нужно извлечь урок: корректная обработка времени – не такое простое
дело, и подходить к решению этой задачи следует очень аккуратно.

Дополнительные конфигурационные параметры
Мы рассмотрели все конфигурационные параметры, для которых в стандартном файле environment.rb имеются примеры. Существуют и другие
параметры, но я подозреваю, что вы о них не знаете, и вряд ли они когда-нибудь понадобятся. Если хотите ознакомиться со всем списком,
загляните в исходный текст или в документацию по классу Configuration, которая начинается примерно со строки 400 файла railties/lib/
initializer.rb.
Помните, мы говорили, что переменная окружения RAILS_ENV, определяет, какие параметры среды загружать дальше? Теперь самое время
рассмотреть параметры, принимаемые по умолчанию для каждого из
стандартных режимов Rails.

Режим разработки
Режим разработки принимается в Rails по умолчанию, именно в нем
вы будете проводить большую часть времени:
# Определенные здесь параметры имеют больший приоритет, чем параметры
# в файле config/environment.rb
# В режиме разработки код приложения перезагружается при каждом запросе.
# Это увеличивает время реакции, но идеально подходит для разработки,
# так как вам не приходится перезагружать веб-сервер после внесения каждого
# изменения в код.
config.cache_classes = false
# Записывать в протокол сообщения об ошибках при случайном вызове метода
# для объекта nil.
config.whiny_nils = true
# Активировать сервер точек останова, с которыми соединяется
# script/breakpointer
config.breakpoint_server = true

50

Глава 1. Среда и конфигурирование Rails
# Показывать полные отчеты об ошибках и запретить кэширование
config.action_controller.consider_all_requests_local = true
config.action_controller.perform_caching
= false
config.action_view.cache_template_extensions
= false
config.action_view.debug_rjs
= true
# Не обращать внимание, если почтовый клиент не может отправить сообщение
config.action_mailer.raise_delivery_errors = false

В следующих разделах я подробно рассмотрю наиболее важные параметры, начав с кэширования классов; этот параметр отвечает за динамическую перезагрузку классов в Rails.

Динамическая перезагрузка классов
Одна из отличительных особенностей Rails – скорость цикла внесения
изменений в режиме разработки. Правите код, щелкаете по кнопке Обновить в броузере – и все! Изменения волшебным образом отражают­ся на приложении. Такое поведение управляется параметром config.
cache_classes, который, как видите, установлен в false в самом начале
сценария config/environments/development.rb.
Не вдаваясь в технические детали, скажу, что если параметр config.
cache_classes равен true, то Rails загружает классы с помощью предложения require, а если false – то с помощью предложения load.
Когда вы затребуете файл с кодом на Ruby с помощью require, интерпретатор исполнит и кэширует его. Если файл затребуется снова (при
последующих запросах), интерпретатор пропустит предложение require и пойдет дальше. Если же файл загрузится предложением load, то
интерпретатор считает и разберет его снова вне зависимости от того,
сколько раз файл загружался раньше.
Теперь рассмотрим загрузку классов в Rails более пристально, по­
скольку иногда вам не удается заставить код перезагружаться автоматически, и это может довести до белого каления, если не понимаешь,
как на самом деле работает механизм загрузки классов!

Загрузчик классов в Rails
В старом добром Ruby нет никаких соглашений об именовании файла
в соответствии с содержимым. Однако в Rails, как легко заметить, почти всегда имеется прямая связь между именем Ruby-файла и содержащимся в нем классом. В Rails используется тот факт, что Ruby предоставляет механизм обратных вызовов для отсутствующих констант.
Когда Rails встречает в коде неопределенную константу, он вызывает
подпрограмму загрузки класса, которая, полагаясь на соглашения об
именовании файлов, пытается найти и затребовать нужный сценарий.

Режим разработки

51

Откуда загрузчик классов знает, где искать файл? Мы уже затрагивали этот вопрос выше при обсуждении роли сценария initializer.rb
в процессе запуска Rails. В Rails имеется концепция путей загрузки,
и по умолчанию множество путей включает практически все каталоги,
куда вам может прийти в голову поместить код приложения.
Метод default_load_paths определяет, в каком порядке Rails просматри-

вает каталоги в пути загрузки. Мы разберем код этого метода и объясним назначение каждой части пути загрузки.
Каталог test/mocks (подробно рассматривается в главе 17 «Тестирование») дает возможность переопределить поведение стандартных классов Rails:
paths = ["#{root_path}/test/mocks/#{environment}"]
# Добавить каталог контроллера приложения.
paths.concat(Dir["#{root_path}/app/controllers/"])
# Затем подкаталоги компонентов.
paths.concat(Dir["#{root_path}/components/[_a-z]*"])
# Затем стандартные каталоги для включаемых файлов.
paths.concat %w(
app
app/models
app/controllers
app/helpers
app/services
app/apis
components
config
lib
vendor
).map { |dir| "#{root_path}/#{dir}" }.select { |dir|
File.directory?(dir) }
paths.concat Dir["#{root_path}/vendor/plugins/*/lib/"]
paths.concat builtin_directories
end

Хотите посмотреть содержимое пути загрузки для своего проекта? Запустите консоль и распечатайте переменную $:. Вот так:
$ console
Loading development environment.
>> $:
=> ["/usr/local/lib/ruby/gems/1.8/gems/ ... # выводятся примерно 20 строк

Для экономии места я опустил часть выведенного на консоль текста.
В пути загрузки типичного проекта Rails обычно бывает 30 и более каталогов. Убедитесь сами.

52

Глава 1. Среда и конфигурирование Rails

Режим тестирования
Если Rails запускается в режиме тестирования (то есть значение переменной окружения RAILS_ENV равно test), то действуют следующие параметры:
# Определенные здесь параметры имеют больший приоритет, чем параметры
# в файле config/environment.rb
# Среда тестирования служит исключительно для прогона набора тестов
# вашего приложения. Ни для чего другого она не предназначена. Помните,
# что тестовая база данных – это "рабочая область", она уничтожается
# и заново создается при каждом прогоне. Не полагайтесь на хранящиеся
# в ней данные!
config.cache_classes = true
# Записывать в протокол сообщение об ошибке при случайном вызове метода
# для объекта nil.
config.whiny_nils = true
# Показывать полные отчеты об ошибках и запретить кэширование
config.action_controller.consider_all_requests_local = true
config.action_controller.perform_caching = false
# Не разрешать объекту ActionMailer отправлять почтовые сообщения
# реальным адресатам. Метод доставки :test сохраняет отправленные
# сообщения в массиве ActionMailer::Base.deliveries.
config.action_mailer.delivery_method = :test

Как правило, вообще не возникает необходимости модифицировать параметры среды тестирования.

Режим эксплуатации
Режим эксплуатации предназначен для запуска приложений Rails,
развернутых в среде хостинга для обслуживания пользовательских запросов. Между режимом эксплуатации и другими режимами есть ряд
существенных отличий, и на одном из первых мест стоит повышение
быстродействия, поскольку классы приложения не перезагружаются
при каждом запросе.
# Определенные здесь параметры имеют больший приоритет, чем параметры
# в файле config/environment.rb
# Среда эксплуатации предназначена для готовых, "живых" приложений.
# Код не перезагружается при каждом запросе.
config.cache_classes = true
# Для распределенной среды использовать другой протокол.
# config.logger = SyslogLogger.new

53

Протоколирование
# Полные отчеты об ошибках запрещены, кэширование включено.
config.action_controller.consider_all_requests_local = false
config.action_controller.perform_caching = true
# Разрешить отправку изображений, таблиц стилей и javascript-сценариев
# с сервера статического контента.
# config.action_controller.asset_host = "http://assets.example.com"
# Отключить ошибки доставки почты, недопустимые электронные адреса
# игнорируются.
# config.action_mailer.raise_delivery_errors = false

Нестандартные среды
При необходимости для приложения Rails можно создать нестандартную среду исполнения, скопировав и изменив один из существующих файлов в каталоге config/environments. Чаще всего
это делают ради формирования дополнительных вариантов среды эксплуатации, например, для постадийной работы (staging)
или контроля качества.
У вас есть доступ к промышленной базе данных с рабочей станции, на которой ведется разработка? Тогда имеет смысл организовать смешанную (triage) среду. Для этого клонируйте обычные
параметры режима разработки, но в описании соединения с базой данных укажите на промышленный сервер. Такая комбинация может оказаться полезной для быстрой диагностики ошибок
в процессе промышленной эксплуатации.

Протоколирование
В большинстве программных контекстов в Rails (моделях, контроллерах, шаблонах представлениях) присутствует атрибут logger, в котором
хранится ссылка на объект протоколирования, согласованный с интерфейсом Log4r или с применяемым по умолчанию в Ruby 1.8+ классом
Logger. Чтобы получить ссылку на объект logger из любого места программы, воспользуйтесь константой RAILS_DEFAULT_LOGGER. Для нее даже
есть специальная комбинация клавиш в редакторе TextMate (rdb →).
В Ruby совсем нетрудно создать новый объект Logger:
$ irb
> require 'logger'
=> true
irb(main):002:0> logger = Logger.new STDOUT
=> # logger.warn "do not want!!!"
W, [2007-06-06T17:25:35.666927 #7303] WARN -- : do not want!!!
=> true
> logger.info "in your logger, giving info"
I, [2007-06-06T17:25:50.787598 #7303] INFO -- : in your logger, giving
your info
=> true

Обычно сообщение в протокол добавляется путем вызова того или иного метода объекта logger, в зависимости от серьезности ситуации. Определены следующие стандартные уровни серьезности (в порядке возрастания):
• debug – указывайте этот уровень для вывода данных, полезных в будущем для отладки. В режиме эксплуатации сообщения такого уровня обычно не пишутся;
• info – этот уровень служит для вывода информационных сообщений. Я обычно использую его, чтобы запротоколировать, снабдив
временными штампами, необычные события, которые все же укладываются в рамки корректного поведения приложения;
• warn – данный уровень служит для вывода информации о необычных ситуациях, которые имеет смысл расследовать подробнее.
Иногда я вывожу в протокол предупреждающие сообщения, когда
в программе срабатывает сторожевой код, препятствующий клиенту выполнить недопустимое действие. Цель при этом – уведомить
лицо, ответственное за сопровождение, о злонамеренном пользователе или об ошибке в пользовательском интерфейсе, например:
def create
begin
@group.add_member(current_user)
flash[:notice] = "Вы успешно присоединились к #{@scene.display_name}"
rescue ActiveRecord::RecordInvalid
flash[:error] = "Вы уже входите в группу #{@group.name}"
logger.warn "Пользователь пытался дважды присоединиться к одной и
той же группе. Пользовательский интерфейс не должен
это разрешать."
end
redirect_to :back
end

• error – этот уровень служит для вывода информации об ошибках, не
требующих перезагрузки сервера;
• fatal – самое страшное, что может случиться. Ваше приложение теперь неработоспособно, для его перезапуска необходимо ручное вмешательство.

Протоколирование

55

Протоколы Rails
В папке log приложения Rails хранится три файла-протокола, соответствующих трем стандартным средам, а также протокол и pid-файл
сервера Mongrel. Файлы-протоколы могут расти очень быстро. Чтобы
упростить очистку протоколов, предусмотрено задание rake:
rake log:clear # Усекает все файлы *.log files в log/ до нулевой длины

На этапе разработке очень полезным может оказаться содержимое
файла log/development.log. Разработчики часто открывают окно терминала, в котором запущена команда tail -f, чтобы следить, что пишется
в этот файл:
$ tail -f log/development.log
User Load (0.000522) SELECT * FROM users WHERE (users.'id' = 1)
CACHE (0.000000) SELECT * FROM users WHERE (users.'id' = 1)

В протокол разработки выводится много интересной информации, например при каждом запросе в протокол – полезные сведения о нем. Ниже приведен пример, взятый из одного моего проекта, и описание выводимой информации:
Processing UserPhotosController#show (for 127.0.0.1 at 2007-06-06
17:43:13) [GET]
Session ID: b362cf038810bb8dec076fcdaec3c009
Parameters: {"/users/8-Obie-Fernandez/photos/406"=>nil,
"action"=>"show", "id"=>"406", "controller"=>"user_photos",
"user_id"=>"8-Obie-Fernandez"}
User Load (0.000477) SELECT * FROM users WHERE (users.'id' = 8)
Photo Columns (0.003182) SHOW FIELDS FROM photos
Photo Load (0.000949) SELECT * FROM photos WHERE (photos.'id' = 406
AND (photos.resource_id = 8 AND photos.resource_type = 'User'))
Rendering template within layouts/application
Rendering photos/show
CACHE (0.000000) SELECT * FROM users WHERE (users.'id' = 8)
Rendered adsense/_medium_rectangle (0.00155)
User Load (0.000541) SELECT * FROM users WHERE (users.'id' = 8)
LIMIT 1
Message Columns (0.002225) SHOW FIELDS FROM messages
SQL (0.000439) SELECT count(*) AS count_all FROM messages WHERE
(messages.receiver_id = 8 AND (messages.'read' = 0))
Rendered layouts/_header (0.02535)
Rendered adsense/_leaderboard (0.00043)
Rendered layouts/_footer (0.00085)
Completed in 0.09895 (10 reqs/sec) | Rendering: 0.03740 (37%) | DB:
0.01233 (12%) | 200 OK [http://localhost/users/8-ObieFernandez/photos/406]
User Columns (0.004578) SHOW FIELDS FROM users

56

Глава 1. Среда и конфигурирование Rails

• контроллер и вызванное действие;
• IP-адрес компьютера, отправившего запрос;
• временной штамп, показывающий, когда поступил запрос;
• идентификатор сеанса, ассоциированного с этим запросом;
• хеш параметров запроса;
• информация о запросе к базе данных, включая время и текст предложения SQL;
• информация о попадании в кэш, включая время и текст SQL-запроса, ответ на который был получен из кэша, а не путем обращения
к базе данных;
• информация о рендеринге каждого шаблона, использованного при
выводе представления, и время, затраченное на обработку шаблона;
• общее время выполнения запроса и вычисленное по нему число запросов в секунду;
• сравнение времени, потраченного на операции с базой данных и на
рендеринг;
• код состояния HTTP и URL для ответа, отправленного клиенту.

Анализ протоколов
Используя протокол разработки и толику здравого смысла, можно легко выполнить различные виды неформального анализа.
Производительность. Изучение производительности приложения –
один из напрашивающихся видов анализа. Чем быстрее выполняется
запрос, тем больше запросов сможет обслужить данный процесс Rails.
Поэтому производительность часто выражается в запросах в секунду.
Найдите, для каких запросов обращение к базе данных и рендеринг
выполняются долго, и разберитесь в причинах.
Важно понимать, что время, сохраняемое в протоколе, не отличается
повышенной точностью. Оно, скорее, даже неправильно просто потому, что очень трудно замерить временные характеристики процесса,
находясь внутри него. Сложив процентные доли времени, затраченного на обращение к базе данных и на рендеринг, вы далеко не всегда получите величину, близкую к 100%.
Однако пусть объективно цифры не точны, зато они дают прекрасную
основу для субъективных сравнений в контексте одного и того же приложения. С их помощью мы можете понять, стало ли некоторое действие занимать больше времени, чем раньше, как его время соотносится
с временем выполнения другого действия и т. д.

Протоколирование

57

SQL-запросы. ActiveRecord ведет себя не так, как вы ожидали? Протоколирование текста SQL-запроса, сгенерированного ActiveRecord, часто помогает отладить ошибки, связанные со сложными запросами.
Выявление ошибок вида N+1 select. При отображении некоторой записи вместе с ассоциированным с ней набором записей есть шанс допустить так называемую ошибку вида N+1 select. Ее признак – наличие серии из многих предложений SELECT, отличающихся только значением первичного ключа.
Вот, например, фрагмент протокола реального приложения Rails, демонстрирующий ошибку N+1 select в том, как загружаются экземпляры класса FlickrPhoto:
FlickrPhoto Load (0.001395) SELECT * FROM flickr_photos WHERE
(flickr_photos.resource_id = 15749 AND flickr_photos.resource_type =
'Place' AND (flickr_photos.'profile' = 1)) ORDER BY updated_at desc
LIMIT 1
FlickrPhoto Load (0.001734) SELECT * FROM flickr_photos WHERE
(flickr_photos.resource_id = 15785 AND flickr_photos.resource_type =
'Place' AND (flickr_photos.'profile' = 1)) ORDER BY updated_at desc
LIMIT 1
FlickrPhoto Load (0.001440) SELECT * FROM flickr_photos WHERE
(flickr_photos.resource_id = 15831 AND flickr_photos.resource_type =
'Place' AND (flickr_photos.'profile' = 1)) ORDER BY updated_at desc
LIMIT 1

… и так далее, и так далее на протяжении многих и многих страниц
протокола. Знакомо?
К счастью, на каждый из этих запросов к базе уходит очень небольшое
время – примерно 0,0015 с. Это объясняется тем, что:
1) MySQL исключительно быстро выполняет простые предложения
SELECT;
2) мой процесс Rails работал на машине, где находилась база данных.
И тем не менее суммарно эти N запросов способны свести производительность на нет. Если бы не вышеупомянутые компенсирующие факторы, я столкнулся бы с серьезной проблемой, причем наиболее явст­
венно она проявлялась бы при расположении базы данных на отдель­
ной машине, поскольку ко времени выполнения каждого запроса добавлялись бы еще и сетевые задержки.
Проблема N+1 select – это еще не конец света. В большинстве случаев
для ее решения достаточно правильно пользоваться параметром :include
в конкретном вызове метода find.
Разделение ответственности. Правильно спроектированное приложение на основе паттерна модель-вид-контроллер следует определенным

58

Глава 1. Среда и конфигурирование Rails

протоколам, описывающим распределение по логическим ярусам операций с базой данных (которая выступает в роли модели) и рендеринга
(вид). Вообще говоря, желательно, чтобы контроллер взял на себя загрузку из базы всех данных, которые понадобятся для рендеринга.
В Rails это достигается за счет того, что код контроллера запрашивает
у модели необходимые данные и сохраняет их в переменных экземпляра, доступных виду (представлению).
Доступ к базе данных на этапе рендеринга обычно считается дурной
практикой. Вызов методов find напрямую из кода шаблона нарушает
принцип разделения ответственности и способен стать причиной ночных кошмаров у персонала службы сопровождения1.
Однако существует немало возможностей для ползучего проникновения в ваш код неявных операций доступа к базе данных на этапе рендеринга. Иногда эти операции инкапсулируются в модели, а иногда выполняются в ходе отложенной загрузки ассоциаций. Можем ли мы решительно осудить такую практику? Трудно дать определенный ответ.
Бывают случаи (например, при кэшировании фрагментов), когда обращение к базе на этапе рендеринга имеет смысл.

Использование альтернативных
схем протоколирования
Легко! Достаточно присвоить одной из переменных класса logger, например ActiveRecord::Base.logger, объект класса, совместимого с классом Logger из стандартного дистрибутива Ruby.
Простой прием, основанный на возможности подмены объектов протоколирования, демонстрировался Дэвидом на различных встречах, в том числе в основном докладе на конференции
Railsconf 2007. Открыв консоль, присвойте ActiveRecord::Base.
logger новый экземпляр класса Logger, указывающий на STDOUT.
Это позволит вам просматривать генерируемые SQL-запросы
прямо на консоли. Джемис подробно рассматривает эту технику и другие возможности на странице http://weblog.jamisbuck.
org/2007/1/31/more-on-watchingactiverecord.

Syslog
В различных вариантах ОС UNIX имеется системная служба syslog.
Есть ряд причин, по которым она может оказаться более удобным
средством протоколирования работы Rails-приложения в режиме эксплуатации:
1

Практически все когда-либо написанные приложения для PHP страдают от
этой проблемы.

Заключение

59

• более точный контроль над уровнями протоколирования и содержимым сообщений;
• консолидация протоколов нескольких приложений Rails;
• при использовании дистанционных средств syslog возможна консолидация протоколов приложений Rails, работающих на разных серверах. Конечно, это удобнее, чем обрабатывать разрозненные протоколы, хранящиеся на каждом сервере приложений в отдельности.
Можно воспользоваться написанной Эриком Ходелем (Eric Hodel) библиотекой SyslogLogger1, чтобы организовать интерфейс приложения
Rails с syslog. Для этого придется загрузить библиотеку, затребовать ее
с помощью require в сценарии environment.rb и подменить экземпляр
RAILS_DEFAULT_LOGGER.

Заключение
Мы начали путешествие в мир Rails с обзора различных сред исполнения Rails и механизма загрузки зависимостей, в том числе и кода вашего приложения. Подробно рассмотрев сценарий environment.rb и его
варианты, зависящие от режима, мы узнали, как настроить поведение
Rails под свои нужды. В процессе обсуждения различных версий библиотек Rails, используемых в конкретном проекте, мы попутно затронули вопрос о «сидении на острие» и о том, когда оно имеет смысл.
Мы также изучили процедуру начальной загрузки Rails, для чего потребовалось заглянуть в исходные тексты (мы и дальше будем при необходимости совершать такие погружения в исходный код Rails).
В главе 2 «Работа с контроллерами» мы продолжим путешествие и рассмотрим диспетчер Rails и ActionController.

1

http://seattlerb.rubyforge.org/SyslogLogger/.

2
Работа с контроллерами
Уберите всю бизнес-логику из контроллеров и переместите ее
в модель. Контроллеры должны отвечать только за отображение
URL (включая и данные из других HTTP-запросов), координацию
между моделями и видами и отправку результатов в виде
HTTP-ответа. Попутно контроллеры могут заниматься
контролем доступа, но больше почти ничем. Эти указания
очень определенны, но для того чтобы им следовать,
необходима интуиция и тонкий расчет.
Ник Каллен, Pivotal Labs
http://www.pivotalblabs.com/articles/2007/07/16/the-controller-formula

В Rails, как и в любом другом приложении, имеется поток передачи
управления между частями программы. Но в Rails этот поток довольно
сложен. Среда состоит из многих компонентов, которые вызывают
друг друга. Среди прочего платформа должна на лету определить, какие файлы приложения вызываются и что в них находится. Разумеется, решение этой задачи зависит от конкретного приложения.
Сердце данного механизма – контроллер. Клиент, обращающийся
к вашему приложению, просит его выполнить некое действие контроллера. Происходить это может разными способами, и в некоторых
граничных случаях не происходит вовсе… но, если вы знаете, какое
место контроллер занимает в жизненном цикле приложения, то сможете разобраться, как с ним сопрягается все остальное. Вот почему мы
рассматриваем контроллеры ранее других частей части Rails API.

Диспетчер: с чего все начинается

61

В аббревиатуре MVC (модель-вид-контроллер) контроллер обозначается буквой «C». После диспетчера контроллер является первым из компонентов, обрабатывающих входящий запрос. Контроллер отвечает за
поток управления в программе; он извлекает информацию из базы данных (обычно с помощью интерфейса ActiveRecord) и предоставляет ее
видам (представлениям).
Контроллеры очень тесно связаны с видами – более тесно, чем с моделями. Можно написать весь слой приложения, относящийся к модели, не
создав ни единого контроллера, или поручить работу над контроллером
и моделью разным людям, которые никогда не общаются между собой.
С другой стороны, виды и контроллеры сцеплены гораздо сильнее. Они
разделяют много общей информации, представленной, главным образом, в форме переменных экземпляра. Это означает, что имена переменных, выбранные в контроллере, влияют на действия в представлении.
В этой главе мы рассмотрим, что происходит на пути к исполнению
действия контроллера и что получается в результате. По ходу мы обратим пристальное внимание на настройку самих классов контроллеров,
особенно в том, что касается многообразных способов рендеринга представлений. Завершим мы эту главу обсуждением еще двух тем, относящихся к контроллерам: фильтров и потоковой отправки.

Диспетчер: с чего все начинается
Среда Rails используется для создания веб-приложений, поэтому сначала запрос обрабатывается веб-сервером: Apache, Lighttpd, Nginx и т. д.
Затем сервер переправляет запрос приложению Rails, где он попадает
к диспетчеру.

Обработка запроса
Выполнив свою часть обработки запроса, сервер передает диспетчеру
различную информацию:
• URI запроса (например, http://localhost:3000/timesheets/show/3 или
что-то в этом роде);
• окружение CGI (список имен параметров CGI и соответствующих
им значений).
В задачу диспетчера входит:
• выяснить, какой контроллер должен обработать запрос;
• определить, какое действие следует выполнить;
• загрузить файл нужного контроллера, который содержит определение
класса контроллера на языке Ruby (например, TimesheetsController);
• создать экземпляр класса контроллера;
• сказать этому экземпляру, какое действие нужно выполнить.

62

Глава 2. Работа с контроллерами

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

Познакомимся с диспетчером поближе
В педагогических целях выполним функции диспетчера вручную. Так
вы сможете лучше почувствовать поток управления в приложениях
Rails.
Для этого небольшого упражнения запустим новое приложение Rails:
$ rails dispatch_me

Теперь создадим простой контроллер с действием index:
$ cd dispatch_me/
$ ruby ./script/generate controller demo index

Заглянув в код только что сгенерированного контроллера в файле app/
controllers/demo_controller.rb, вы обнаружите в нем действие index:
class DemoController < ApplicationController
def index
end
end

Сценарий generate также автоматически создал файл app/views/demo/index.rhtml, который содержит шаблон представления, соответствующего этому действию. Шаблон включает некоторые подстановочные переменные. Чтобы не усложнять задачу, заменим его более простым файлом, который сможем опознать с первого взгляда. Сотрите все содержимое файла index.rhtml и введите такую строку:
Hello!

Ее не назовешь дизайнерским шедевром, но для наших целей сойдет.
Итак, мы выстроили косточки домино в ряд, теперь пора толкнуть переднюю: диспетчер. Для этого запустим консоль Rails, находясь в каталоге приложения. Введите команду ruby script/console:
$ ruby script/console
Loading development environment.
>>

Теперь мы находимся в самом сердце приложения Rails, которое ожидает инструкций.

Диспетчер: с чего все начинается

63

Обычно при передаче запроса диспетчеру Rails веб-сервер устанавливает две переменные окружения. Поскольку мы собираемся вызвать диспетчер вручную, эти переменные придется установить самостоятельно:
>>
=>
>>
=>

ENV['REQUEST_URI'] = "/demo/index"
"/demo/index"
ENV['REQUEST_METHOD'] = "get"
"get"

Теперь мы готовы обмануть диспетчер, заставив его думать, будто он
получил запрос. На самом деле, диспетчер действительно получает
запрос только не от сервера, а от человека, сидящего за консолью.
Вот как выглядит команда:
>> Dispatcher.dispatch

А вот и ответ от приложения Rails:
Content-Type: text/html; charset=utf-8
Set-Cookie: _dispatch_me_session_id=336c1302296ab4fa1b0d838d; path=/
Status: 200 OK
Cache-Control: no-cache
Content-Length: 7
Hello!

Мы вызвали метод dispatch класса Dispatcher, и в результате было выполнено действие index и рендеринг соответствующего шаблона (в том
виде, в каком мы его оставили), к результатам рендеринга добавлены
HTTP-заголовки, и все вместе возвращено нам.
Теперь представьте: если бы вы были не человеком, а веб-сервером,
и проделали все то же самое, то сейчас могли бы вернуть документ, состоящий из заголовков и строки Hello!, клиенту. Именно так все
и происходит. Загляните в подкаталог public приложения dispatch_me
(или любого другого приложения Rails). Среди прочего вы найдете там
следующие файлы диспетчера:
$ ls dispatch.*
dispatch.cgi dispatch.fcgi dispatch.rb

Каждый раз, когда Rails получает запрос, веб-сервер передает управление одному из этих файлов. Какому именно, зависит от конфигурации сервера. В конечном итоге, все они делают одно и то же: вызывают метод класса Dispatcher.dispatch, как мы перед этим сделали с консоли.
Вы можете пройти по следу и дальше, ознакомившись с файлом public/.htaccess и конфигурацией своего сервера. Но для понимания цепочки событий, возникающих при получении запроса к Rails, и той
роли, которую играет в них контроллер, сказанного выше достаточно.

64

Глава 2. Работа с контроллерами

Рендеринг представления
Цель типичного действия контроллера – выполнить рендеринг шаблона представления, то есть заполнить шаблон и передать результаты
(обычно в форме HTML-документа) серверу, чтобы тот доставил их
клиенту.
Как ни странно (по крайней мере, это может показаться немного странным, хотя и не лишенным логики), можно не определять действие контроллера, если существует шаблон с таким же именем, как у дейст­вия.
Можете проверить это в ручном режиме. Откройте файл app/controller/demo_controller.rb и удалите действие index, после чего файл будет
выглядеть так:
class DemoController < ApplicationController
end

Не удаляя файл app/views/demo/index.rhtml, попробуйте выполнить
с консоли то же упражнение, что и выше (вызвать метод Dispatcher.
dispatch и т. д.). Результат получится точно таким же, как и раньше.
Кстати, не забывайте перезагружать консоль после внесения изменений – автоматически она не распознает, что код изменился. Самый
простой способ перезагрузить консоль – просто ввести команду reload!.
Но имейте в виду, что все существующие экземпляры ActiveRecord, на
которые вы храните ссылки, также придется перезагрузить (с помощью их собственных методов reload). Иногда проще выйти из консоли
и запустить ее заново.

Если сомневаетесь, рисуйте
Rails знает, что, получив запрос к действию index демонстрационного
контроллера, он должен любой ценой вернуть что-то серверу. Раз
действия index в файле контроллера нет, Rails пожимает плечами
и говорит: «Что ж, если бы действие index было, оно все равно оказалось бы пустым и я бы выполнил рендеринг шаблона index.rhtml. Так
и сделаю это».
Однако даже на примере пустого действия контроллера кое-чему можно научиться. Вернемся к исходной версии демонстрационного контроллера:
class DemoController < ApplicationController
def index
end
end

Можно сделать вывод, что если в действии контроллера ничего другого
не задано, то по умолчанию выполняется рендеринг шаблона, имя ко-

Рендеринг представления

65

торого соответствует имени контроллера и действия. В данном случае
это шаблон views/demo/index.rhtml.
Иными словами, в каждом действии контроллера имеется неявная команда render. Причем render – это самый настоящий метод. Предыдущий пример можно было бы переписать следующим образом:
def index
render :template => "demo/index"
end

Но это необязательно, поскольку и так подразумевается, что именно
данное действие нужно вам. Это частный случай принципа примата
соглашения над конфигурацией, часто упоминаемого в разговорах разработчиков для Rails. Не заставляйте разработчика писать код, который можно было бы добавить по соглашению.
Однако команда render – это не просто способ попросить Rails выполнить то, что он и так собирался сделать.

Явный рендеринг
Рендеринг шаблона – это как выбор рубашки. Если вам не нравится
самая ближняя из висящих в шкафу – назовем ее рубашкой по умолчанию, – можно протянуть руку и достать другую.
Если действие контроллера не хочет рисовать шаблон по умолчанию,
то может нарисовать любой другой, вызвав метод render явно. Доступен любой шаблон, находящийся в поддереве с корнем app/views (на самом деле, это не совсем точно – Доступен вообще любой шаблон в системе). Но зачем контроллеру может понадобиться выполнять рендеринг шаблона, отличного от шаблона по умолчанию? Причин несколько, и, познакомившись с некоторыми, мы сможем узнать о многих
полезных возможностях метода контроллера render.

Рендеринг шаблона другого действия
Типичный случай, когда нужно выполнять рендеринг совсем другого
шаблона, – это повторный вывод формы, в которую пользователь ввел
недопустимые данные. Обычно в такой ситуации выводят ту же самую
форму с данными, введенными пользователем, а также дополнительную информацию об ошибках, чтобы пользователь мог их исправить
и отправить форму еще раз.
Причина, по которой в этом случае приходится рисовать не шаблон по
умолчанию, заключается в том, что действие обработки формы и действие вывода формы – чаще всего разные действия. Поэтому действию,
которое обрабатывает форму, необходим способ повторно вывести исходный шаблон (форму), а не переходить к следующему экрану, как
в случае корректно заполненной формы.

66

Глава 2. Работа с контроллерами

Ух, какое многословное объяснение получилось. Вот практический
пример:
class
def
#
#
#
end

EventController < ActionController::Base
new
Это (пустое) действие выполняет рендеринг шаблона new.rhtml, который
содержит форму для ввода информации о новом событии, оно по существу
не нужно.

def create
# Этот метод обрабатывает данные из формы. Данные доступны через
# вложенный хеш params, являющийся значением ключа :event.
@event = Event.new(params[:event])
if @event.save
flash[:notice] = "Событие создано!"
redirect_to :controller => "main" # пока не обращайте внимания на
# эту строку
else
render :action => "new" # не выполняет метод new!
end
end
end

В случае ошибки – когда вызов @event.save не возвращает true – мы
снова выполняем рендеринг шаблона new то есть файла new.rhtml.
В предположении, что шаблон new.rhtml написан правильно, будет автоматически включена информация об ошибке, хранящаяся в новом
(но не сохраненном) объекте @event класса Event.
Отметим, что сам шаблон new.rhtml не «знает», что он рисуется действием create, а не new. Он просто делает то, что требуется: выполняет
подстановки на основе содержащихся в нем инструкций и переданных
контроллером данных (в данном случае – объекта @event).

Рендеринг совершенно постороннего шаблона
Точно так же, как рисуется шаблон другого действия, выполняется
и рендеринг любого хранящегося в системе шаблона. Для этого следует
вызвать метод render, передав в параметре :template или :file путь
к нужному файлу шаблона.
Параметр :template должен содержать путь относительно корня дерева
шаблонов (app/views, если вы ничего не меняли, что было бы весьма необычно), а параметр :file – абсолютный путь в файловой системе.
Честно говоря, параметр :template редко используется при разработке
приложений Rails.
render :template => "abuse/report" # рендеринг app/views/abuse/report.rhtml
render :file => "/railsapps/myweb/app/views/templates/common.rhtml"

67

Рендеринг представления

Рендеринг подшаблона
Еще один случай – рендеринг подшаблона (partial template или просто
partial). Вообще говоря, подшаблоны позволяют представить всю совокупность шаблонов в виде небольших файлов, избежав громоздкого
кода и выделив модули, допускающие повторное использование.
Контроллер прибегает к рендерингу подшаблонов чаще всего для
AJAX-вызовов, когда необходимо динамически обновлять участки
уже выведенной страницы. Эта техника, равно как и вообще рассмотрение подшаблонов, более подробно излагается в главе 10 «Компонент
ActionView».

Рендеринг встроенного шаблона
Иногда броузеру нужно послать результат трансляции какого-нибудь
фрагмента шаблона, который слишком мал, чтобы оформлять его в виде отдельной части. Признаю, что такая практика спорна, так как является вопиющим нарушением принципа разделения ответственности
между различными слоями MVC.
Один из часто встречающихся случаев употребления встроенного рендеринга и, пожалуй, единственная причина, по которой такая возможность вообще включена, – это использование помощников при обработке AJAX-запросов, например auto_complete_result (см. главу 12 «Ajax
on Rails»).
render :inline => „"

Rails обрабатывает такой встроенный код точно так же, как если бы это
был шаблон представления.

Говорит Кортенэ…
Будь вы моим подчиненным, я отругал бы вас за использование
в контроллере кода, относящегося к представлениям, даже если
это всего одна строка.
То, что относится к представлениям, должно там и находиться!

Рендеринг текста
Что если нужно отправить броузеру всего лишь простой текст, особенно когда речь идет об ответах на AJAX-запросы и некоторые запросы
к веб-службам?
render :text => 'Данные приняты'

68

Глава 2. Работа с контроллерами

Рендеринг структурированных данных других типов
Команда render принимает ряд параметров, облегчающих возврат струк­
турированных данных в таких форматах, как JSON или XML. При этом
в ответе правильно выставляется заголовок content-type и другие характеристики.

:json
JSON1 – это небольшое подмножество языка JavaScript, применяемое
в качестве простого формата обмена данными. Чаще всего он используется для отправки данных JavaScript-сценарию, который работает на
стороне клиента в обогащенном веб-приложении и посылает серверу
AJAX-запросы. В библиотеку ActiveRecord встроена поддержка для
преобразования в формат JSON, что делает Rails идеальной платформой для возврата данных в этом формате, например:
render :json => @record.to_json

:xml
В ActiveRecord встроена также поддержка для преобразования в формат
XML, например:
render :xml => @record.to_xml

Вопросы, связанные с XML, мы будем подробно рассматривать в главе 15
«XML и ActiveResource».

Пустой рендеринг
Редко, но бывает, что не нужно рисовать вообще ничего (для обхода
ошибки: в броузере Safari «ничего» на самом деле означает отправку
броузеру одного пробела).
render :nothing => true, :status => 401 # Не авторизован

Стоит отметить, что, как показано в этом примере, render :nothing =>
true часто используется в сочетании с некоторым кодом состояния
HTTP (см. раздел «Параметры рендеринга»).

Параметры рендеринга
В большинстве обращений к методу render задаются дополнительные
параметры. Ниже они перечислены в алфавитном порядке.

:content_type
С любым контентом, который циркулирует в Сети, ассоциирован тип
MIME2. Например, HTML-контенту соответствует тип text/html. Но
1

Дополнительную информацию о JSON см. на сайте http://www.json.org/.

2

Спецификация MIME занимает пять документов RFC, поэтому удобнее ознакомиться с вполне приличным описанием в «Википедии» на странице
http://en.wikipedia.org/wiki/MIME.

69

Рендеринг представления

иногда необходимо отправить клиенту данные в формате, отличном от
HTML. Rails не проверяет формат переданного идентификатора MIME,
поэтому вы должны сами позаботиться о том, чтобы значение параметра :content_type было допустимым.

:layout
По умолчанию Rails придерживается определенных соглашений о шаблоне размещения, в который обертывается ваш ответ. Эти соглашения
подробно рассматриваются в главе 10 «Компонент ActionView». Параметр :layout позволяет указать, нужен вам шаблон размещения или нет.

:status
В протоколе HTTP определено много стандартных кодов состояния1, описывающих вид ответа на запрос клиента. В большинстве типичных случаев Rails выбирает подходящий код автоматически, например 200 OK для
успешно обработанного запроса.
Для изложения теории и практики использования всех возможных кодов состояния HTTP потребовалась бы отдельная глава, а то и целая
книга. Для удобства в табл. 2.1 приведено несколько кодов, которые, по
моему опыту, полезны при повседневном программировании для Rails.
Таблица 2.1. Общеупотребительные коды состояния HTTP
Код состояния

Описание

307 Temporary
Redirect
Запрошенному ресурсу
временно присвоен
другой URI

Иногда необходимо временно переадресовать поль­
зователя на другое действие, например, потому что
работает какой-то длительный процесс или учет­
ная запись владельца конкретного ресурса приоста­
новлена.
Этот код состояния говорит, что текущий URI запрошенного ресурса указан в HTTP-заголовке
Location. Поскольку методу render не передается
хеш заголовков ответа, вы должны установить их
самостоятельно перед вызовом render. К счастью,
хеш response находится в области видимости методов контроллера, как в примере:
def paid_resource
if current_user.account_expired?
response.headers['Location'] =
account_url(current_user)
render :text => "Account expired", :status =>
307
end
end

1

Полный перечень кодов состояния HTTP см. на странице http://www.w3.org/
Protocols/rfc2616/rfc2616-sec10.html

70

Глава 2. Работа с контроллерами

Таблица 2.1. Общеупотребительные коды состояния HTTP (окончание)
Код состояния

Описание

401 Unauthorized

Иногда пользователь не предоставляет верительных грамот, необходимых для просмотра ресурса
с ограниченным доступом, или процедура аутентификации/авторизации завершается неудачно.
Если применяется базовая (Basic) схема аутентификации или аутентификация дайджестом (Digest
Authentication), вы, скорее всего, должны вернуть
код 401

403 Forbidden
Сервер понял запрос,
но отказывается его
выполнять

Я предпочитаю использовать код 403 в сочетании
с коротким сообщением (render :text) в ситуации,
когда клиент запросил ресурс, который в обычных обстоятельствах недоступен через интерфейс
веб-приложения. Иными словами, запрос, скорее
всего, был сформирован искусственно. Человек
или робот с добрыми или дурными намерениями
(это неважно) пытается заставить сервер делать
то, что он делать не должен.
Например, приложение Rails, над которым я сейчас работаю, открыто для всех, и ежедневно на него заходит робот GoogleBot. Возможно, из-за когда-то существовавшей ошибки был проиндексирован URL /favorites.
Однако каталог /favorites должен быть доступен
только зарегистрированным пользователям. Но,
коль скоро Google знает об этом URL, он будет заходить туда снова и снова.
Вот как я его торможу:
def index
return render :nothing => true,
:status => 403 unless logged_in?
@favorites = current_user.favorites.find(:all)
end

404 Not Found
Сервер не может найти
запрошенный ресурс

Код 404 можно использовать, например, когда ресурс с указанным идентификатором отсутствует
в базе данных (то ли потому что идентификатор
указан неверно, то ли потому что ресурс удален).
Например, ресурса, соответствующего запросу
«GET /people/2349594934896107», в нашей базе
данных нет вовсе, так что же мы должны показать? Сообщение о том, что человека с таким идентификатором не существует? Нет, в соответствии
с архитектурным стилем REST правильно вернуть
ответ с кодом 404.
А если у вас параноидальные наклонности и вы
знаете, что такой ресурс существовал в прошлом,
то можете послать в ответ код 410 Gone

71

Переадресация
Код состояния

Описание

503 Service Unavailable
Сервер временно
недоступен

Код 503 удобен, когда сайт на некоторое время закрывается на техническое обслуживание, особенно при обновлении веб-служб, написанных в соответствии с архитектурным стилем REST.
Один из рецензентов книги, Сьюзан Поттер, поделилась таким советом:
В своих проектах для Rails я создаю приложение-заглушку, которое возвращает код 503
в ответ на любой запрос. Клиентами моих
служб обычно выступают другие службы,
поэтому разработчики клиентов, потребляющих мои веб-службы, знают, что это временный перерыв, вызванный, скорее всего,
плановым обслуживанием (заодно это служит напоминанием о необходимости просматривать, а не игнорировать, электронную почту, которую я отправляю по вы­
ходным).

Переадресация
Жизненный цикл приложения Rails разбит на запросы. При поступлении каждого нового запроса все начинается сначала.
Рендеринг шаблона, который подразумевается по умолчанию, является
альтернативным, частичным, просто текстом или чем-нибудь еще, – это
последний шаг обработки запроса. Переадресация же означает, что обработка текущего запроса завершается, и начинается обработка нового.
Рассмотрим еще раз пример метода create для обработки формы:
def create
@event = Event.new(params[:event])
if @event.save
flash[:notice] = "Событие создано!"
redirect_to :controller => "main"
else
render :action => "new"
end
end

Если операция сохранения завершается успешно, мы записываем сообщение в хеш flash и вызываем метод redirect_to для перехода к совершенно другому действию. В данном случае это действие index (оно
не задано явно, но принимается по умолчанию) контроллера main.
Смысл в том, что при сохранении записи о новом событии Event надлежит вернуть пользователя к представлению верхнего уровня. Так почему просто не выполнить рендеринг шаблона main/index.rhtml?

72

Глава 2. Работа с контроллерами

Говорит Кортенэ…
Помните, что код, следующий за вызовом метода redirect или
render, исполняется и приложение отправит данные броузеру не
раньше, чем он завершится.
В случае сложной логики часто бывает желательно вернуться
сразу после обращения к redirect или render, находящегося глубоко внутри последовательности предложений if, чтобы предотвратить ошибку DoubleRenderError:
def show
@user = User.find(params[:id])
if @user.activated?
render :action => 'activated' and return
end
case @user.info
...
end
end

if @event.save
flash[:notice] = "Событие создано!"
render :controller => "main", :action => "index"
...

В результате действительно будет выполнен рендеринг шаблона main/
index.rhtml. Но тут есть подводные камни. Предположим, например,
что действие main/index выглядит следующим образом:
def index
@events = Event.find(:all)
end

Если выполнить рендеринг index.rhtml из действия event/create, то
действие main/index не будет выполнено. Поэтому переменная @events
останется неинициализированной. Следовательно, при рендеринге index.rhtml возникнет ошибка, так как в этом шаблоне (предположительно) используется @events:
Schedule Manager
Текущий список ваших событий:

здесь какая-то HTML-разметка

Вот почему мы должны выполнить переадресацию на действие main/index, а не просто позаимствовать его шаблон. Команда redirect_to начнет
все с чистого листа: создаст новый запрос, инициирует новое действие
и решит, по какому шаблону выводить ответ.

73

Переадресация

Говорит Себастьян…
Какая переадресация правильна?
Используя метод redirect_to, вы говорите пользовательскому агенту (то
есть броузеру), что необходимо выполнить новый запрос с другим URL.
Такой ответ может интерпретироваться по-разному, поэтому в современной спецификации протокола HTTP определены четыре разных
кода состояния для переадресации.
В старой версии HTTP 1.0 было два кода: 301 Moved Permanently (Перемещен постоянно) и 302 Moved Temporarily (Перемещен временно).
Постоянная переадресация означает, что пользовательский агент должен забыть о старом URL и использовать новый, обновив все хранящиеся ссылки (например, закладку или в Google запись в поисковой
базе данных). Временная переадресация – это одноразовое действие.
Исходный URL все еще действителен, но для данного конкретного запроса агент должен запросить ресурс с указанным URL.
Однако тут кроется проблема: какой метод использовать для переадресованного запроса, если первоначально был выполнен POST-запрос?
В случае постоянной переадресации безопасно предположить, что новый запрос должен выполняться методом GET, так как это справедливо для всех сценариев применения. Но временная переадресация используется как для переадресации на представление ресурса, только
что модифицированного в ходе обработки исходного POST-запроса (наиболее часто встречающийся случай), так и для переадресации всего
исходного POST-запроса на новый URL, который и должен позаботиться об его обработке.
В HTTP 1.1 это проблема решена путем определение двух новых кодов
состояния: 303 See other (См. в другом месте) и 307 Temporary Redirect
(Временная переадресация). Код 303 говорит пользовательскому агенту, что нужно выполнить GET-запрос вне зависимости от того, каким
методом выполнялся исходный запрос, а 307 – что нужно обязательно
использовать тот же самый метод, что и для исходного запроса.
Большинство современных броузеров трактует код 302 так же, как
303, то есть посылают GET-запрос. Именно поэтому метод redirect_to
в Rails по-прежнему отправляет код 302. Код 303 был бы лучше, поскольку при этом не остается места для интерпретации (а, стало быть,
и путаницы), но я подозреваю, что никому эта проблема не показалась
достаточно серьезной, чтобы поместить запрос на исправление.
Если вам когда-нибудь потребуется переадресация с кодом 307, например, чтобы продолжить обработку POST-запроса в другом действии,
всегда можно организовать это самостоятельно, для чего достаточно
записать путь в заголовок response.header["Location"], а затем выполнить рендеринг, вызвав метод render :status => 307.

74

Глава 2. Работа с контроллерами

Коммуникация между контроллером
и представлением
При выполнении рендеринга шаблона обычно используются данные,
которые контроллер извлек из базы. Иными словами, контроллер получает то, что ему нужно, от модели и передает представлению.
В Rails передача данных от контроллера представлению осуществляется с помощью переменных экземпляра. Обычно действие контроллера
инициализирует одну или несколько переменных. Затем они могут использоваться представлением.
В выборе переменных, через которые осуществляется обмен данными,
кроется некая ирония (и возможный источник путаницы для новичков). Основная причина, по которой эти переменные вообще существуют, заключается в том, чтобы объекты (будь то объекты Controller,
String или какие-то другие) могли хранить ссылки на данные, которые
они не разделяют с другими объектами. При выполнении действия
контроллера все происходит в контексте объекта контроллера – скажем, экземпляра класса DemoController или EventController. Говоря
«контекст», мы имеем в виду и то, что любая переменная экземпляра
в коде принадлежит экземпляру контроллера.
Но рендеринг шаблона выполняется в контексте другого объекта – экземпляра класса ActionView::Base. У этого объекта есть собственные переменные экземпляра, и он не имеет доступа к переменным экземпляра контроллера.
Поэтому, на первый взгляд, переменные экземпляра – это самый плохой способ организовать совместный доступ двух объектов к общим
данным. Однако это возможно; по крайней мере, можно сделать так,
что будет казаться, будто общий доступ имеется. В действительности
Rails в цикле обходит все переменные объекта контроллера и для каждой из них создает переменную экземпляра в объекте представления
с тем же именем и данными.
Для среды это довольно тяжелая работа – все равно что вручную копировать список вещей, которые нужно купить в бакалейной лавке. Зато
жизнь программиста упрощается. Если вы сторонник концептуальной
чистоты Ruby, то можете скривиться при мысли о том, что переменные
экземпляра служат для связывания объектов, а не их отделения друг
от друга. Но, с другой стороны, поборник чистоты Ruby должен понимать, что в Ruby можно делать массу самых разных вещей, в том числе
и копировать переменные экземпляра в цикле. Ничего противоречащего идеологии Ruby в этом нет. А с точки зрения программиста это
дает возможность организовать прозрачную связь между контроллером и шаблоном, который он рисует.

75

Фильтры

Фильтры
Фильтры позволяют контроллеру выполнять пред- и постобработку
своих действий. Фильтры можно использовать для аутентификации,
кэширования или аудита, перед тем как выполнять требуемое действие. Методы фильтрации реализованы как макросы, то есть находятся в начале метода контроллера, в контексте класса, перед определением других методов. Мы опускаем скобки вокруг аргументов метода,
чтобы подчеркнуть декларативную природу фильтров:
before_filter :require_authentication

Как и большинству других макрометодов в Rails, методу фильтрации
можно передать произвольное число символов:
before_filter :security_scan, :audit, :compress

или расположить их в отдельных строках:
before_filter :security_scan
before_filter :audit
before_filter :compress

В отличие от чем-то похожих методов обратного вызова в ActiveRecord,
невозможно реализовать метод фильтрации в контроллере, просто добавив метод с именем before_filter или after_filter.

Говорит Кортенэ…
Некоторые любят использовать фильтры для загрузки записи,
когда операция ожидает всего одну запись, а логика относительно сложна. Разумеется, переменные экземпляра, установленные
фильтром, доступны любым действиям. Но это спорный подход;
некоторые разработчики считают, что все обращения к базе данных должны быть вынесены из фильтра и помещены в метод action.
before_filter :load_product, :only => [ :show,
:edit, :update, :destroy ]
def load_product
@product =
current_user.products.find_by_permalink(params[:id]
)
redirect_to :action => 'index' and return false
unless @product.active?
end

76

Глава 2. Работа с контроллерами

Методы фильтрации следует объявлять как protected или private,
в противном случае их можно будет вызывать как открытые действия
контроллера (с помощью маршрута, принимаемого по умолчанию).
Важно, что фильтры имеют доступ к запросу, ответу и всем переменным экземпляра, которые устанавливаются другими фильтрами в цепочке или самим действием (в случае фильтров after). Фильтры могут
устанавливать переменные экземпляра, которые будут использоваться
в запрошенном действии; очень часто так и поступают.

Наследование фильтров
Фильтры распространяются вниз по иерархии наследования контроллеров. В типичном приложении Rails имеется класс ApplicationController, которому наследуют все остальные контроллеры, поэтому, если вы хотите, чтобы некий фильтр выполнялся в любом случае, поместите его именно в этот класс.
class ApplicationController < ActionController::Base
after_filter :compress

Подклассы могут добавлять и/или пропускать ранее определенные
фильтры, не оказывая влияния на суперкласс. Рассмотрим, например,
взаимодействие двух связанных отношением наследования классов
(листинг 2.1).
Листинг 2.1. Два кооперативных фильтра before
class BankController < ActionController::Base
before_filter :audit
private
def audit
# Записать действия и параметры этого контроллера в контрольный журнал
end
end
class VaultController < BankController
before_filter :verify_credentials
private
def verify_credentials
# проверить, что пользователю разрешен доступ в хранилище
end
end

77

Фильтры

Перед выполнением любого действия контроллера BankController (или
его подкласса) будет вызван метод audit. Для действий же контроллера
VaultController сначала вызывается метод audit, а потом verify_ credentials, поскольку фильтры заданы именно в таком порядке (фильтры
исполняются в контексте класса, в котором объявлены, а класс BankController должен быть загружен раньше, чем VaultController, так как
является родителем последнего).
Если метод audit по какой-то причине вернет false, то ни метод verify_
credentials, ни запрошенное действие не выполняются. Это называется
прерыванием цепочки фильтров (halting the filter chain), и, заглянув
в протокол режима обработки, вы обнаружите в нем запись о том, что
фильтр такой-то прервал обработку запроса.

Типы фильтров
Фильтры можно реализовать одним из трех способов: ссылкой на метод
(символ), внешним классом или встроенным методом (Proc-объектом).
Первый способ встречается чаще всего; в этом случае фильтр ссылается
на какой-нибудь защищенный или закрытый метод где-то в иерар­хии
наследования контроллера. В листинге 2.1 так реализованы фильтры
в обоих классах BankController и VaultController.

Классы фильтров
С помощью внешних классов проще реализовать повторно используемые фильтры, например, для сжатия выходной информации. Для этого в любом классе определяется статический метод фильтрации, и этот
класс передается фильтру, как показано в листинге 2.2.
Листинг 2.2. Фильтр сжатия выходной информации
class OutputCompressionFilter
def self.filter(controller)
controller.response.body = compress(controller.response.body)
end
end
class NewspaperController < ActionController::Base
after_filter OutputCompressionFilter
end

Метод self.filter класса Filter передается экземпляру фильтруемого
контроллера, при этом фильтр получает доступ ко всем аспектам контроллера и может манипулировать последним по своему усмотрению.

Встроенная фильтрация
Встраивание (путем передачи параметра-блока методу фильтрации)
можно применять для выполнения короткой операции, не требующей

78

Глава 2. Работа с контроллерами

пояснений, или просто в качестве теста на скорую руку. Работает этот
способ следующим образом:
class WeblogController < ActionController::Base
before_filter {|controller| false if controller.params["stop"]}
end

Как видите, блок ожидает, что ему будет передан контроллер, после
того как последний запишет ссылку-запрос во внутренние переменные. Это означает, что блок имеет доступ к объектам запроса и ответа
вместе со всеми вспомогательными методами для доступа к параметрам, сеансу, шаблону и т. д. Отметим, что встроенный метод не обязан
быть блоком – любой объект, отвечающий на вызов метода call, например Proc или Method, тоже подойдет.
Фильтры around ведут себя несколько иначе, чем обычные фильтры before и after (подробнее об этом см. раздел, посвященный around-фильтрам).

Упорядочение цепочки фильтров
Методы before_filter и after_filter добавляют указанные фильтры
в конец цепочки существующих. Обычно именно это и требуется, но
иногда порядок выполнения фильтров важен. В таких случаях можно
воспользоваться методами prepend_before_filter и prepend_after_filter.
Фильтр помещается в начало соответствующей цепочки и выполняется раньше всех остальных (листинг 2.3).
Листинг 2.3. Пример добавления фильтров before в начало цепочки
class ShoppingController < ActionController::Base
before_filter :verify_open_shop
class CheckoutController < ShoppingController
prepend_before_filter :ensure_items_in_cart, :ensure_items_in_stock

Теперь цепочка фильтров для контроллера CheckoutController выглядит так: :ensure_items_in_cart, :ensure_items_in_stock, :verify_open_shop.
Если хотя бы один из фильтров ensure вернет false, мы так и не узнаем,
открыт магазин или нет, – цепочка фильтров будет прервана.
Можно передавать несколько аргументов-фильтров любого типа, а также фильтр-блок. Если передан блок, он трактуется как последний аргумент.

Aroundфильтры
Around-фильтры обертывают действие, то есть выполняют некий код до
и после действия. Их можно объявлять в виде ссылок на методы, блоков или объектов, отвечающих на вызов метода filter или оба метода
before и after.

79

Фильтры

Чтобы использовать в качестве around-фильтра метод, передайте символ, именующий некоторый метод. Для выполнения этого метода внутри блока воспользуйтесь предложением yield (или block.call). В листинге 2.4 показан around-фильтр, протоколирующий исключения (я не
хочу сказать, что вы должны делать нечто подобное в своем приложении; это просто пример).
Листинг 2.4. Around-фильтр для протоколирования исключений
around_filter :catch_exceptions
private
def catch_exceptions
yield
rescue => exception
logger.debug "Перехвачено исключение! #{exception}"
raise
end

Чтобы использовать в качестве around-фильтра блок, передайте блок,
который в виде аргументов принимает контроллер и блок действия.
Вызывать yield из блока around-фильтра напрямую нельзя – вместо этого явно вызовите блок действия:
around_filter do |controller, action|
logger.debug "перед #{controller.action_name}"
action.call
logger.debug "после #{controller.action_name}"
end

Чтобы совместно с around-фильтром использовать фильтрующий объект, передайте объект, отвечающий на вызов метода :filter или на вызовы :before и :after. Из метода фильтрации передайте управление
блоку следующим образом:
around_filter BenchmarkingFilter
class BenchmarkingFilter
def self.filter(controller, &block)
Benchmark.measure(&block)
end
end

Фильтрующий объект с методами before и after обладает одной особенностью – вы должны явно вернуть true из метода before, если хотите
вызвать метод after.
around_filter Authorizer
class Authorizer
# Этот метод вызывается до действия. Возврат false отменяет действие.
def before(controller)
if user.authorized?

80

Глава 2. Работа с контроллерами
return true
else
redirect_to login_url
return false
end
end
def after(controller)
# Выполняется после действия, только если before вернул true
end
end

Пропуск цепочки фильтров
Фильтр, объявленный в базовом классе, применяется ко всем подклассам. Это удобно, но иногда в подклассе необходимо пропустить фильт­
ры, унаследованные от суперкласса:
class ApplicationController < ActionController::Base
before_filter :authenticate
around_filter :catch_exceptions
end
class SignupController < ApplicationController
skip_before_filter :authenticate
end
class ProjectsController < ApplicationController
skip_filter :catch_exceptions
end

Условная фильтрация
Применение фильтров можно ограничить определенными действиями.
Для этого достаточно указать, какие действия включаются или исключаются. В обоих случаях можно задать как одиночное действие (например, :only => :index), так и массив действий (:except => [:foo, :bar]).
class Journal < ActionController::Base
before_filter :authorize, :only => [:edit, :delete]
around_filter :except => :index do |controller, action_block|
results = Profiler.run(&action_block)
controller.response.sub! "", "#{results}"
end
private
def authorize
# Переадресовать на login, если не аутентифицирован.
end
end

Потоковая отправка

81

Прерывание цепочки фильтров
Методы before_filter и around_filter могут прервать обработку запроса
до выполнения действия контроллера. Это полезно, например, чтобы
отказать в доступе неаутентифицированным пользователям.
Как уже отмечалось выше, для прерывания цепочки фильтров достаточно, чтобы фильтр вернул значение false. Вызов метода render или
redirect_to также прерывает цепочку фильтров. Если цепочка фильтров прервана, то after-фильтры не выполняются. Around-фильтры прекращают обработку запроса, если не был вызван блок действия.
Если around-фильтр возвращает управление до вызова блока, то цепочка прерывается, и after-фильтры не вызываются.
Если before-фильтр возвращает false, вторая часть любого around-фильтра все равно выполняется, но сам метод действия не вызывается, равно как не вызываются и after-фильтры.

Потоковая отправка
Мало кто знает, что в Rails, помимо рендеринга шаблонов, встроена определенная поддержка потоковой отправки броузеру двоичного контента.
Потоковая отправка удобна, когда необходимо послать броузеру динамически сгенерированный файл (например, изображение или PDF-файл).
В модуле ActionController::Streaming module для этого предусмотрено два
метода: send_data и send_file. Один из них полезен, вторым почти никогда не следует пользоваться. Сначала рассмотрим полезный метод.

send_data(data, options = {})
Метод send_data позволяет отправить пользователю текстовые или двоичные данные в виде именованного файла. Можно задать параметры,
определяющие тип контента и видимое имя файла; указать, надо ли
пытаться отобразить данные в броузере вместе с другим содержимым,
или предложить пользователю загрузить их в виде вложения.

Параметры метода send_data
У метода send_data есть следующие параметры:
• :filename – задает имя файла, видимое броузеру;
• :type – задает тип контента HTTP. По умолчанию подразумевается
' application/octetstream';
• :disposition – определяет, следует ли отправлять файл в одном потоке с другими данными или загружать отдельно;
• :status – задает код состояния, сопровождающий ответ. По умолчанию принимается '200 OK'.

82

Глава 2. Работа с контроллерами

Примеры использования
Для загрузки динамически сгенерированного tgz-архива можно поступить следующим образом:
send_data generate_tgz('dir'), :filename => 'dir.tgz'

В листинге 2.5 приведен пример отправки броузеру динамически сгенерированного изображения; это часть реализации системы captcha,
которая мешает злонамеренным роботам использовать ваше веб-приложение нежелательным образом.
Листинг 2.5. Контроллер Captcha, в котором используется библиотека
RMagick и метод send_data
require 'RMagick'
class CaptchaController < ApplicationController
def image
# Создать холст RMagic и нарисовать на нем трудночитаемый текст
...
image = canvas.flatten_images
image.format = "JPG"
# отправить броузеру
send_data(image.to_blob, :disposition => 'inline',
:type => 'image/jpg')
end
end

send_file(path, options = {})
Метод send_file отправляет клиенту файл порциями по 4096 байтов.
В документации по API говорится: «Это позволяет не читать сразу весь

Замечание по поводу безопасности
Заметим, что метод send_file применим для чтения любого файла, доступного пользователю, от имени которого работает серверный процесс Rails. Поэтому очень тщательно проверяйте1 параметр path, если его источником может быть не заслуживающая
доверия веб-страница.

1

Хейко Веберс (Heiko Webers) написал прекрасную статью о проверке имен
файлов, которая доступна по адресу http://www.rorsecurity.info/2007/03/27/
working-with-files-in-rails/.

Потоковая отправка

83

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

Параметры метода send_file
На случай, если вы все-таки решите воспользоваться методом send_file
(и не говорите потом, что вас не предупреждали), приведу список его
параметров:
• :filename – имя файла, видимое броузеру. По умолчанию принимается File.basename(path);
• :type – тип контента HTTP. По умолчанию ‘application/octetstream’;
• :disposition – отправлять ли файл в общем потоке или загружать
отдельно;
• :stream – посылать ли файл пользовательскому агенту по мере считывания (true) или предварительно прочитать весь файл в память
(false). По умолчанию true;
• :buffer_size – размер буфера (в байтах), используемого для потоковой отправки файла. По умолчанию 4096;
• :status – код состояния, сопровождающий ответ. По умолчанию
‘200 OK’;
• :url_based_filename – должно быть true, если вы хотите, чтобы броузер вывел имя файла из URL; это необходимо для некоторых броузеров при использовании имен файлов, содержащих не-ASCII-символы (задание параметра :filename отменяет этот режим).
Большинство этих параметров обрабатывается закрытым методом
send_file_headers! из модуля ActionController::Streaming, который и устанавливает соответствующие заголовки ответа. Поэтому если для отправки файлов вы используете веб-сервер, то, возможно, захотите
взглянуть на исходный текст этого метода. Если вы пожелаете предоставить пользователю дополнительную информацию, которую Rails не
поддерживает (например Content-Description), придется кое-что почитать о других HTTP заголовках Content-*1.
1

См. официальную спецификацию по адресу http://www.w3.org/Protocols/
rfc2616/rfc2616-sec14.html.

84

Глава 2. Работа с контроллерами

Говорит Кортенэ…
Мало найдется разумных причин обслуживать статические файлы с помощью Rails.
Очень, очень мало.
Если вам абсолютно необходимо воспользоваться одним из методов send_data или send_file, настоятельно рекомендую закэшировать файл перед отправкой. Сделать это можно несколькими
способами (не забывайте, что правильно сконфигурированный
веб-сервер сам обслуживает файлы в каталоге public/ и не заходит в каталог rails).
Можно, например, просто скопировать файл в каталог public:
public_dir = File.join(RAILS_ROOT, 'public',
controller_path)
FileUtils.mkdir_p(public_dir)
FileUtils.cp(filename, File.join(public_dir,
filename))

Все последующие обращения к этому ресурсу будут обслуживаться самим веб-сервером.
Можно вместо этого попробовать воспользоваться директивой
caches_page, которая автоматически сделает нечто подобное (кэширование рассматривается в главе 10).

Наконец, имейте в виду, что документ может кэшироваться проксисервером или броузером. Способом кэширования на промежуточных
узлах управляют заголовки Pragma и Cache-Control. По умолчанию требуется, чтобы клиент запросил у сервера, можно ли возвращать кэшированный ответ1.

Еще одна причина ненавидеть Internet Explorer
По умолчанию заголовки Content-Type и Content-Disposition устанавливаются так, чтобы поддержать загрузку произвольных двоичных файлов для максимально возможного числа броузеров.
Но как будто специально чтобы подогреть ненависть к Internet
Explorer, версии 4, 5, 5.5 и 6 этого богом проклятого броузера
весьма специфически обрабатывают загрузку файлов, особенно
по протоколу HTTPS.

1

Обзор кэширования в Сети см. на странице http://www.mnot.net/cache_docs/.

Потоковая отправка

85

Примеры использования
Для начала простейший пример загрузки ZIP-файла:
send_file '/path/to.zip'

Для отправки JPG-файла в потоке с другими данными требуется указать MIME-тип контента:
send_file '/path/to.jpg',
:type => 'image/jpeg',
:disposition => 'inline'

Следующий пример выведет в броузере HTML-страницу с кодом 404.
Мы добавили в описание типа объявление кодировки с помощью параметра charset:
send_file '/path/to/404.html,
:type => 'text/html; charset=utf-8',
:status => 404

А как насчет потокой отправки FLV-файла флэш-плееру внутри броузера?
send_file @video_file.path,
:filename => video_file.title + '.flv',
:type => 'video/x-flv',
:disposition => 'inline'

Как заставить сам вебсервер отправлять файлы
Решение проблемы переполнения памяти, возникающей в связи с использованием метода send_file, заключается в том, чтобы воспользоваться средствами, которые такие веб-серверы, как Apache, Lighttpd
и Nginx предлагают для прямой отправки файлов, даже если они не
находятся в каталоге общедоступных документов. Для этого нужно задать в ответе специальный HTTP-заголовок, указав в нем путь к файлу, который веб-сервер должен отправить клиенту.
Вот как это делается для Apache и Lighttpd:
response.headers['X-Sendfile'] = path

А вот так – для Nginx:
response.headers['X-Accel-Redirect'] = path

В обоих случаях вы должны завершить действие контроллера, попросив Rails ничего посылать, поскольку этим займется веб-сервер.
render :nothing => true

В любом случае возникает вопрос, зачем вообще нужен механизм отправки файлов броузеру, если уже имеется готовый – запрашивать
файлы из каталога public. Например, потому что часто встречаются

86

Глава 2. Работа с контроллерами

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

Заключение
В этой главе мы рассмотрели некоторые основополагающие аспекты
работы Rails: диспетчер и механизм рендеринга представлений контроллерами. Заодно мы обсудили фильтры действий контроллера; вы
часто будете применять их для самых разных целей. API ActionController относится к числу наиболее фундаментальных концепций, которыми вы должны овладеть на пути к становлению экспертом по
Rails.
Далее мы перейдем еще к одной теме, тесно связанной с диспетчерами
и контроллерами: вопросу о том, как Rails решает, каким образом отвечать на запрос. То есть к системе маршрутизации.

1

Бен Кэртис (Ben Curtis) описал замечательный подход к защищенной загрузке файлов в статье по адресу http://www.bencurtis.com/archives/2006/
11/serving-protected-downloads-with-rails/.

3
Маршрутизация
Во сне я видел тысячи новых дорог….
Но проснулся и пошел по старой.
Китайская пословица

Под маршрутизацией в Rails понимается механизм, который на основе
URL входящего запроса решает, какое действие должно предпринять
приложение. Однако этим его функции отнюдь не ограничиваются.
Система маршрутизации в Rails – крепкий орешек. Но за кажущейся
сложностью не так уж много концепций. Стоит их усвоить – и все кусочки головоломки встают на свои места.
В этой главе мы познакомимся с основными приемами определения
маршрутов и манипулирования ими, а в следующей рассмотрим предлагаемые Rails механизмы для поддержки написания приложений,
согласующихся с принципами архитектурного стиля Representational
State Transfer (REST). Эти механизмы могут оказаться исключительно
полезными, даже если вы не собираетесь углубляться в теоретические
дебри REST.
Многие примеры, приведенные в этих двух главах, основаны на небольшом аукционном приложении. Они достаточно просты и понятны.
Идея такова: есть аукционы, на каждом из которых торгуется один
лот. Кроме того, есть пользователи, предлагающие заявки со своими
ценами. Вот по существу и все.
Главное событие в жизненном цикле соединения с приложением Rails –
срабатывание какого-либо действия контроллера. Следовательно, кри-

88

Глава 3. Маршрутизация

тически важна процедура определения того, какой контроллер и какое
действие выбрать. Вот эта-то процедура и составляет сущность системы маршрутизации.
Система маршрутизации отображает URL на действия. Для этого
применяются правила, которые вы задаете с помощью команд на языке Ruby в конфигурационном файле config/routes.rb. Если не переопределять правила, прописанные в этом файле по умолчанию, вы получите некое разумное поведение. Но не так уж трудно написать собственные правила и обратить гибкость системы маршрутизации себе
во благо.
На самом деле у системы маршрутизации две задачи. Она отображает
запросы на действия и конструирует URL, которые вы можете передавать в качестве аргументов таким методам, как link_to, redirect_to
и form_tag. Система знает, как преобразовать URL, заданный посетителем, в последовательность контроллер/действие. Кроме того, она знает, как изготовить представляющие URL строки по вашим спецификациям.
Когда вы пишете такой код:
"items", :action => "list" %>

система маршрутизации передает помощнику link_to следующий
URL:
http://localhost:3000/items/list

Таким образом, система маршрутизации – мощный двусторонний механизм. Она распознает URL и маршрутизирует их соответствующим
образом, а также генерирует URL, используя в качестве шаблона правила маршрутизации. По мере изложения мы будем обращать внимание на обе эти одинаково важные цели.

Две задачи маршрутизации
Механизм распознавания URL важен, потому что именно он позволяет
приложению решить, что делать с поступившим запросом:
http://localhost:3000/myrecipes/apples Что нам делать?!

Генерация URL полезна, так как позволяет в шаблонах представлений
и контроллерах применять синтаксис сравнительно высокого уровня
для вставки URL. Вам не придется писать такой код:
Мои рецепты блюд из яблок
Не хочется писать такое вручную!

Две задачи маршрутизации

89

Система маршрутизации решает обе задачи: интерпретацию (распознавание) URL запроса и запись (генерирование) URL. Они основаны на
формулируемых вами правилах. Эти правила вставляются в файл config/routes.rb с применением особого синтаксиса (это обычный код на
Ruby, но в нем используются специальные методы и параметры).
Каждое правило – или, применяя общеупотребительный термин, маршрут – включает строку-образец, которая служит шаблоном как для
сопоставления с URL, так и для их порождения. Образец состоит из
статических подстрок, символов косой черты (имитирующих синтаксис URL) и позиционных метапараметров, служащих «приемниками»
для отдельных компонентов URL как при распознавании, так и при генерации.
Маршрут может также включать один или несколько связанных параметров в форме пар «ключ/значение» из некоторого хеша. Судьба этих
пар зависит от того, что собой представляет ключ. Есть два «волшебных» ключа (:controller и :action), которые определяют, что нужно
сделать. Остальные ключи (:blah, :whatever и т. д.) сохраняются для
ссылок в будущем. Вот конкретный маршрут, связанный с предыдущими примерами:
map.connect 'myrecipes/:ingredient',
:controller => "recipes",
:action => "show"

В этом примере вы видите:
• статическую строку (myrecipes);
• метапараметр, соответствующий компоненту URL (:ingredient);
• связанные параметры (:controller => "recipes", :action => "show").
Синтаксис маршрутов достаточно развит – этот пример вовсе не является самым сложным (как, впрочем, и самым простым), – поскольку
на них возлагается много обязанностей. Один-единственный маршрут типа приведенного выше должен содержать достаточно информации как для сопоставления с существующим URL, так и для изготовления нового. Синтаксис маршрутов разработан с учетом обеих процедур.
Разобраться в нем не так уж сложно, если рассмотреть разные типы полей
по очереди. Мы займемся этим на примере маршрута для «ингредиентов». Не расстраивайтесь, если сначала не все будет понятно. На протяжении этой главы мы обсудим различные приемы и раскроем все тайны.
Изучая анатомию маршрутов, мы познакомимся с ролью, которую каждая часть играет в распознавании и генерации URL. Не забывайте, что
это всего лишь демонстрационный пример. Маршруты позволяют многое, но, чтобы понять, как все это работает, лучше начать с простого.

90

Глава 3. Маршрутизация

Связанные параметры
На этапе распознавания связанные параметры – пары «ключ/значение» в хеше, задаваемом в конце списка аргументов маршрута, – определяют, что должно происходить, если данный маршрут соответст­
вует поступившему URL. Предположим, что в броузере был введен
такой URL:
http://localhost:3000/myrecipes/apples

Этот URL соответствует маршруту для ингредиентов. В результате будет выполнено действие show контроллера recipes. Чтобы понять причину, снова рассмотрим наш маршрут:
map.connect 'myrecipes/:ingredient',
:controller => "recipes",
:action => "show"

Ключи :controller и :action – связанные: если URL сопоставился с этим
маршрутом, то запрос всегда будет обрабатываться именно данным
контроллером и данным действием. Ниже вы познакомитесь с техникой определения контроллера и действия на основе сопоставления
с метапараметрами. Однако в этом примере метапараметры не участвуют. Контроллер и действие «зашиты» в код.
Желая сгенерировать URL, вы должны предоставить значения всех необходимых связанных параметров. Тогда система маршрутизации
сможет отыскать нужный маршрут (если передано недостаточно информации для формирования маршрута, Rails возбудит исключение).
Параметры обычно представляются в виде хеша. Например, чтобы сгенерировать URL из маршрута для ингредиентов, нужно написать примерно такой код:
"recipes",
:action => "show",
:ingredient => "apples" %>

Значения "recipes" и "show" для параметров :controller и :action сопо­
ставляются с маршрутом для ингредиентов, в котором значения этих
параметров постоянны. Значит строка-образец, указанная в данном
маршруте, может служить шаблоном генерируемого URL.
Применение хеша для задания компонентов URL – общая техника всех
методов порождения URL (link_to, redirect_to, form_for и т. д.). Внутри
они обращаются к низкоуровневому методу url_for, о котором мы поговорим чуть ниже.
Мы пока ни слова не сказали о компоненте :ingredient. Это метапараметр в строке-образце.

Статические строки

91

Метапараметры («приемники»)
Символ :ingredient в рассматриваемом маршруте называется метапараметром (wildcard parameter), или переменной. Можете считать его
приемником; он должен быть заменен неким значением. Значение,
подставляемое вместо метапараметра, определяется позиционно в ходе
сопоставления URL с образцом:
http://localhost:3000/myrecipes/apples Кто-то запрашивает данный URL…
'myrecipes/:ingredient' который соответствует

этому образцу

В данном случае приемник :ingredient получает из URL значение apples. Следовательно, в элемент хеша params[:ingredient] будет записана
строка "apples". К этому элементу можно обратиться из действия recipes/show. При генерации URL необходимо предоставить значения для
всех приемников – метапараметров в строке-образце. Для этого применяется синтаксис «ключ => значение». В этом и состоит смысл последней строки в предшествующем примере:
"recipes",
:action => "show",
:ingredient => "apples" %>

В данном обращении к методу link_to мы задали значения трех параметров. Два из них должны соответствовать зашитым в код связанным
параметрам маршрута; третий, :ingredient, будет подставлен в образец
вместо метапараметра :ingredient.
Но все они – не более чем пары «ключ/значение». Из обращения
к link_to не видно, передаются ли «зашитые» или подставляемые значения. Известно лишь, что есть три значения, связанные с тремя ключами, и этого должно быть достаточно для идентификации маршрута,
а, стало быть, строки-образца, а, стало быть, шаблона URL.

Статические строки
В нашем примере маршрута образец содержит статическую строку myrecipes.
map.connect 'myrecipes/:ingredient',
:controller => "myrecipes",
:action => "show"

Эта строка служит отправной точкой для процедуры распознавания.
Когда система маршрутизации видит URL, начинающийся с /myrecipes, она сопоставляет его со статической строкой в маршруте для ингредиентов. Любой URL, который не содержит строку myrecipes в начале, не будет сопоставлен с этим маршрутом.

92

Глава 3. Маршрутизация

При генерации URL статические строки просто копируются в URL,
формируемый системой маршрутизации. Следовательно, в рассматриваемом примере такой вызов link_to:
"recipes",
:action => "show",
:ingredient => "apples" %>

породит следующий HTML-код:
Мои рецепты блюд из яблок

Строка myrecipes при вызове link_to не указывается. Сопоставление
с маршрутом основывается на параметрах, переданных link_to. Затем
генератор URL использует заданный в маршруте образец как шаблон
для порождения URL. А уже в этом образце присутствует подстрока
myrecipes.
Распознавание и генерация URL – две задачи, решаемые системой маршрутизации. Можно провести аналогию с адресной книгой, хранящейся в мобильном телефоне. Когда вы выбираете из списка контактов имя
Гэвин, телефон находит соответствующий номер. А когда Гэвин звонит
вам, телефон просматривает все номера в адресной книге и определяет,
что вызывающий номер принадлежит именно Гэвину; в результате на
экране высвечивается имя Гэвин.
Маршрутизация в Rails несколько сложнее поиска в адресной книге,
поскольку в ней участвуют переменные. Отображение не взаимно однозначно, но идея та же самая: распознать, что пришло в запросе, и сгенерировать выходную HTML-разметку.
Теперь обратимся к правилам маршрутизации. Читая текст, вы должны все время держать в уме двойственную природу распознавания/генерации. Вот два принципа, которые особенно полезно запомнить:
• и распознавание, и генерация управляются одним и тем же правилом. Вся система построена так, чтобы вам не приходилось записывать правила дважды. Каждое правило пишется один раз и применяется в обоих направлениях;
• URL, генерируемые системой маршрутизации (с помощью метода
link_to и родственных ему), имеют смысл только для самой системы маршрутизации. Путь recipes/apples, который генерирует система, не содержит никакой информации о том, как будет происходить
обработка, – он лишь отображается на некоторое правило маршрутизации. Именно правило предоставляет информацию, необходимую
для вызова определенного действия контроллера. Не зная правил
маршрутизации, невозможно понять, что означает данный URL.
Как это выглядит на практике, мы детально рассмотрим по ходу обсуждения.

Файл routes.rb

93

Файл routes.rb
Маршруты определяются в файле config/routes.rb, как показано в листинге 3.1 (с некоторыми дополнительными комментариями). Этот
файл создается в момент первоначального создания приложения Rails.
В нем уже прописано несколько маршрутов, и в большинстве случаев
вам не придется ни изменять их, ни добавлять новые.
Листинг 3.1. Файл routes.rb, подразумеваемый по умолчанию
ActionController::Routing::Routes.draw do |map|
# Приоритет зависит от порядка следования.
# Чем раньше определен маршрут, тем выше его приоритет.
# Пример простого маршрута:
# map.connect 'products/:id', :controller => 'catalog',
:action => 'view'
#
#
#
#

Помните, что значения можно присваивать не только параметрам
:controller и :action
Пример именованного маршрута:
map.purchase 'products/:id/purchase', :controller => 'catalog',
:action => 'purchase'

#
#
#
#

Этот маршрут можно вызвать как purchase_url(:id => product.id)
Вы можете задать маршрут к корню сайта, указав значение ''
-- не забудьте только удалить файл public/index.html.
map.connect '', :controller => "welcome"

# Разрешить загрузку WSDL-документа веб-службы в виде файла
# с расширением 'wsdl', а не фиксированного файла с именем 'wsdl'
map.connect ':controller/service.wsdl', :action => 'wsdl'
# Установить маршрут по умолчанию с самым низким приоритетом.
map.connect ':controller/:action/:id.:format'
map.connect ':controller/:action/:id'
end

Код состоит из единственного вызова метода ActionController::Routing::Routes.draw, который принимает блок. Все, начиная со второй
и кончая предпоследней строкой, тело этого блока.
Внутри блока есть доступ к переменной map. Это экземпляр класса ActionController::Routing::RouteSet::Mapper. С его помощью конфигурируется вся система маршрутизации в Rails – правила маршрутизации
определяются вызовом методов объекта Mapper. В подразумеваемом по
умолчанию файле routes.rb встречается несколько обращений к методу map.connect. Каждое обращение (по крайней мере, незакомментированное) создает новый маршрут и регистрирует его в системе маршрутизации.
Система маршрутизации должна найти, какому образцу соответствует
распознаваемый URL, или провести сопоставление с параметрами ге-

94

Глава 3. Маршрутизация

нерируемого URL. Для этого все правила – маршруты – обходятся
в порядке их определения, то есть следования в файле routes.rb. Если
сопоставить с очередным маршрутом не удалось, процедура сопоставления переходит к следующему. Как только будет найден подходящий
маршрут, поиск завершается.

Говорит Кортенэ…
Маршрутизация – пожалуй, один из самых сложных аспектов
Rails. Долгое время вообще был только один человек, способный
вносить изменения в исходный код этой подсистемы, настолько
она запутана. Поэтому не расстраивайтесь, если не сможете ухватить ее суть с первого раза. Так было с большинством из нас.
Но при этом синтаксис файла routes.rb довольно прямолинеен.
На задание маршрутов в типичном проекте Rails у вас вряд ли
уйдет больше пяти минут.

Маршрут по умолчанию
В самом конце файла routes.rb находится маршрут по умолчанию:
map.connect ':controller/:action/:id'

Маршрут по умолчанию – это в некотором роде конец пути; он определяет, что должно произойти, когда больше ничего не происходит. Однако это еще и неплохая отправная точка. Если вы понимаете, что такое маршрут по умолчанию, то сможете разобраться и в более сложных
примерах.
Маршрут по умолчанию состоит из строки-образца, содержащей три
метапараметра-приемника. Два из них называются :controller и :action. Следовательно, действие, определяемое этим маршрутом, зависит
исключительно от метапараметров; нет ни связанных параметров, ни
параметров, зашитых в код контроллера и действия.
Рассмотрим следующий сценарий. Поступает запрос на такой URL:
http://localhost:3000/auctions/show/1

Предположим, что он не соответствует никакому другому образцу.
Тогда поиск доходит до последнего маршрута в файле – маршрута по
умолчанию. В этом маршруте есть три приемника, а в URL – три значения, поэтому складывается три позиционных соответствия:
:controller/:action/:id
auctions / show / 1

Таким образом, мы получили контроллер auctions, действие show и значение «1» для параметра id (которое должно быть сохранено в params[:id]).
Теперь диспетчер знает, что делать.

95

Файл routes.rb

Поведение маршрута по умолчанию иллюстрирует некоторые особенности системы маршрутизации. Например, по умолчанию для любого
запроса в качестве действия подразумевается index. Другой пример: если в образце есть метапараметр, например :id, то система маршрутизации предпочитает найти для него значение, но если такового в URL не
оказывается, она присвоит ему значение nil, а не придет к выводу, что
соответствие не найдено.
В табл. 3.1 приведены примеры нескольких URL и показаны результаты применения к ним этого правила.
Таблица 3.1. Примеры применения маршрута по умолчанию
URL

Результат

Значение id

Контроллер

Действие

/auctions/show/3

auctions

show

3

/auctions/index

auctions

index

nil

/auctions

auctions

nil

/auctions/show

auctions

index
(по умолчанию)
show

nil –
возможно, ошибка!

В последнем случае nil, вероятно, ошибка, так как действия show без
идентификатора, скорее всего, быть не должно!

О поле :id
Отметим, что в обработке поля :id этого URL нет ничего магического;
оно трактуется просто как значение с именем. При желании можно было бы изменить правило, изменив :id на :blah, но тогда надо не забыть
внести соответствующее изменение в действие контроллера:
@auction = Auction.find(params[:blah])

Имя :id выбрано исходя из общепринятого соглашения. Оно отражает
тот факт, что действию часто нужно получать конкретную запись из
базы данных. Основная задача маршрутизатора – определить контроллер и действие, которые надо вызвать. Поле id – это дополнение, которое позволяет действиям передавать друг другу данные.
Поле id в конечном итоге попадает в хеш params, доступный всем дейст­
виям контроллера. В типичном, классическом случае его значение используется для выборки записи из базы данных:
class ItemsController < ApplicationController
def show
@item = Item.find(params[:id])
end
end

96

Глава 3. Маршрутизация

Генерация маршрута по умолчанию
Маршрут по умолчанию не только лежит в основе распознавания URL
и выбора правильного поведения, но и играет определенную роль при генерации URL. Вот пример обращения к методу link_to, в котором для
генерации URL применяется маршрут по умолчанию:
"item",
:action => "show",
:id => item.id %>

Здесь предполагается, что существует локальная переменная item, содержащая (опять же предположительно) объект Item. Идея в том, чтобы создать гиперссылку на действие show контроллера item и включить
в нее идентификатор id данного лота. Иными словами, гиперссылка
должна выглядеть следующим образом:
Фотография Гудини с автографом

Именно такой URL любезно создает механизм генерации маршрутов.
Взгляните еще раз на маршрут по умолчанию:
map.connect ':controller/:action/:id'

При вызове метода link_to мы задали значения всех трех полей, присутствующих в образце. Системе маршрутизации осталось лишь подставить эти значения и включить результирующую строку в URL:
item/show/3

При щелчке по этой ссылке ее URL будет распознан благодаря второй
половине системы маршрутизации, что вызовется нужное действие
подходящего контроллера, которому в элементе params[:id] будет передано значение 3.
В данном примере при генерации URL используется логика подстановки метапараметров: в образце указываются три символа – :controller,
:action,:id, вместо них в генерируемый URL подставляются значения,
которые мы передали. Сравните с предыдущим примером:
map.connect 'recipes/:ingredient',
:controller => "recipes",
:action => "show"

Чтобы заставить генератор выбрать именно этот маршрут, вы должны
при обращении к link_to передать строки recipes и show в качестве значений параметров :controller и :action. В случае маршрута по умолчанию, да и вообще любого маршрута, образец которого содержит символы, соответствие все равно должно быть найдено, но значения могут
быть любыми.

Предпоследний маршрут и метод respond_to

97

Модификация маршрута по умолчанию
Чтобы прочувствовать систему маршрутизации, лучше всего попробовать что-то изменить и посмотреть, что получится. Проделаем это с марш­
рутом по умолчанию. Потом надо будет вернуть все в исходное состояние, но модификация кое-чему вас научит.
Давайте поменяем местами символы :controller и :action в образце:
# Установить маршрут по умолчанию с самым низким приоритетом.
map.connect ':action/:controller/:id'

Теперь в маршруте по умолчанию первым указывается действие. Это означает,чтоURL,которыйраньшезаписывалсяввидеhttp://localhost:3000/
auctions/show/3, теперь должен выглядеть как http://localhost:3000/
show/auctions/3. А при генерации URL из этого маршрута мы будем
получать результат в порядке /show/auctions/3.
Это не очень логично – предыдущий маршрут по умолчанию был лучше. Зато вы стали лучше понимать, что происходит, особенно в части
магических символов :controller и :action. Попробуйте еще какие-нибудь изменения и посмотрите, какой эффект они дадут (и не забудьте
вернуть все назад).

Предпоследний маршрут и метод respond_to
Сразу перед маршрутом по умолчанию находится маршрут:
map.connect ':controller/:action/:id.:format'

Строка .:format в конце сопоставляется с точкой и значением метапараметра format после поля id. Следовательно, этот маршрут соответст­
вует, например, такому URL:
http://localhost:3000/recipe/show/3.xml

Здесь в элемент хеша params[:format] будет записано значение xml. Поле
:format – особое; оно интепретируется специальным образом в действии
контроллера. И связано это с методом respond_to.
Метод respond_to позволяет закодировать действие так, что оно будет
возвращать разные результаты в зависимости от запрошенного формата. Вот пример действия show в контроллере items, которое возвращает
результат в формате HTML или XML:
def show
@item = Item.find(params[:id])
respond_to do |format|
format.html

98

Глава 3. Маршрутизация
format.xml { render :xml => @item.to_xml }
end
end

Здесь в блоке respond_to есть две ветви. Ветвь HTML состоит из предложения format.html. Запрос HTML-данных будет обработан путем обычного рендеринга представления RHTML. Ветвь XML включает блок
кода. При запросе XML-данных этот блок будет выполнен, а результаты выполнения возвратятся клиенту.
Проиллюстрируем это, вызвав программу wget из командной строки
(выдача слегка сокращена):
$ wget http://localhost:3000/items/show/3.xml -O Resolving localhost... 127.0.0.1, ::1
Connecting to localhost|127.0.0.1|:3000... connected.
HTTP request sent, awaiting response... 200 OK
Length: 295 [application/xml]

2007-02-16T04:33:00-05:00
Violin treatise
3
Leopold Mozart
paper

1744

Суффикс .xml в конце URL заставляет метод respond_to пойти по ветви
xml и вернуть XML-представление лота.

Метод respond_to и заголовок HTTPAccept
Вызвать ветвление в методе respond_to может также заголовок HTTPAccept в запросе. В этом случае нет необходимости добавлять в URL
часть .:format.
В следующем примере мы не задаем в wget суффикс .xml, а устанавливаем заголовок Accept:
wget http://localhost:3000/items/show/3 -O - —header="Accept:
text/xml"
Resolving localhost... 127.0.0.1, ::1
Connecting to localhost|127.0.0.1|:3000... connected.
HTTP request sent, awaiting response...
200 OK
Length: 295 [application/xml]

2007-02-16T04:33:00-05:00
Violin treatise
3
Leopold Mozart
paper

99

Пустой маршрут

1744

Результат получился точно такой же, как в предыдущем примере.

Пустой маршрут
Если нет желания учиться на собственном опыте, можно вообще не
трогать маршрут по умолчанию. Но в файле routes.rb есть еще один
маршрут, который тоже в какой-то мере считается умалчиваемым,
и вот его-то вы, скорее всего, захотите изменить. Речь идет о пустом
маршруте.
В нескольких строках от маршрута по умолчанию (см. листинг 3.1) вы
обнаружите такой фрагмент:
# Вы можете задать маршрут к корню сайта, указав значение ''
# -- не забудьте только удалить файл public/index.html.
# map.connect '', :controller => "welcome"

То, что вы видите, и называется пустым маршрутом; это правило говорит о том, что должно произойти, если кто-то наберет следующий URL:
http://localhost:3000

Отметьте отсутствие "/anything" в конце!

Пустой маршрут – в определенном смысле противоположность маршруту по умолчанию. Если маршрут по умолчанию говорит: «Мне нужно три значения, и я буду интерпретировать их как контроллер, действие и идентификатор», то пустой маршрут говорит: «Мне не нужны
никакие значения; я вообще ничего не хочу, я уже знаю, какой контроллер и действие вызывать!»
В только что сгенерированном файле routes.rb пустой маршрут закомментирован, поскольку для него нет универсального или хотя бы разумного значения по умолчанию. Вы сами должны решить, что в вашем приложении означает «пустой» URL.
Ниже приведено несколько типичных примеров правил для пустых
маршрутов:
map.connect '', :controller => "main", :action => "welcome"
map.connect '', :controller => "top", :action => "login"
map.connect '', :controller => "main"

Последний маршрут ведет к действию main/index, поскольку действие
index подразумевается по умолчанию, когда никакое другое не указано.
Отметим, что в Rails 2.0 в объект map добавлен метод root, поэтому теперь определять пустой маршрут для приложения Rails рекомендуется так:
map.root :controller => "homepage"

100

Глава 3. Маршрутизация

Наличие пустого маршрута позволяет пользователям что-то увидеть,
когда они заходят на ваш сайт, указав лишь его доменное имя.

Самостоятельное создание маршрутов
Маршрут по умолчанию является общим. Он предназначен для перехвата всех маршрутов, которым не нашлось более точного соответствия
выше. А теперь займемся этим самым «выше», то есть маршрутами,
которые в файле routes.rb предшествуют маршруту по умолчанию.
Вы уже знакомы с основными компонентами маршрута: статическими
строками, связанными параметрами (как правило, в их число входит
:controller, а часто еще и :action) и метапараметрами-приемниками,
которым значения присваиваются позиционно из URL или на основе
ключей, заданных в хеше, определяющем URL.
При создании новых маршрутов вы должны рассуждать так же, как
система маршрутизации:
• для распознавания необходимо, чтобы в маршруте было достаточно
информации – либо зашитой в код, либо готовой для приема значений из URL, – чтобы состоялся выбор контроллера и действия (по
крайней мере, контроллера – по умолчанию будет выбрано действие
index, если вас это устраивает);
• для генерации необходимо, чтобы зашитых параметров и метапараметров было достаточно для создания нужного маршрута.
Коль скоро эти условия соблюдены и маршруты перечислены в порядке убывания приоритетов (в порядке «проваливания»), система будет
работать должным образом.

Использование статических строк
Помните – тот факт, что для выполнения любого запроса нужен контроллер и действие, еще не означает, что необходимо точное соответствия между количеством полей в строке-образце и количеством связанных параметров.
Например, допустимо написать такой маршрут:
map.connect ":id", :controller => "auctions", :action => "show"

и он будет распознавать следующий URL:
http://localhost:3000/8

Система маршрутизации запишет 8 в params[:id] (исходя из позиции
приемника :id, который соответствует позиции «8» в URL) и выполнит
действие show контроллера auctions. Разумеется, визуально такой маршрут воспринимается странно. Лучше поступить примерно так, как
в листинге 2.2, где семантика выражена более отчетливо:

Использование собственных «приемников»

101

map.connect "auctions/:id", :controller => "auctions", :action => "show"

Такой маршрут распознал бы следующий URL:
http://localhost:3000/auctions/8

Здесь auctions – статическая строка. Система будет искать ее в распознаваемом URL и вставлять в URL, который генерируется следующим
кодом:
"auctions",
:action => "show",
:id => auction.id %>

Использование собственных «приемников»
До сих пор нам встречались магические параметры :controller и :action и хоть и не магический, но стандартный параметр :id. Можно также завести собственные параметры – зашитые или мета. Тогда и ваши
маршруты и код приложения окажутся более выразительными и самодокументированными.
Основная причина для заведения собственных параметров состоит
в том, чтобы ссылаться на них из программы. Например, вы хотите,
чтобы действие контроллера выглядело следующим образом:
def show
@auction = Auction.find(params[:id])
@user = User.find(params[:user_id])
end

Здесь символ :user_id, как и :id, выступает в роли ключа хеша. Но,
значит, он должен как-то туда попасть. А попадает он точно так же,
как параметр :id – вследствие указания в маршруте, по которому мы
добрались до действия show.
Вот как выглядит этот маршрут:
map.connect 'auctions/:user_id/:id',
:controller => "auctions",
:action => "show"

При распознавании URL
/auctions/3/1

этот маршрут вызовет действие auctions/show и установит в хеше params
оба ключа – :user_id и :id (при позиционном сопоставлении :user_id
получает значение 3, а :id – значение 1).
Для генерации URL достаточно добавить ключ :user_id в спецификацию URL:
"auctions",
:action => "show",
:user_id => current_user.id,
:id => ts.id %>

Ключ :user_id в хеше сопоставится с приемником :user_id в образце
маршрута. Ключ :id также сопоставится, равно как и параметры :controller и :action. Результатом будет URL, сконструированный по шаблону auctions/:user_id/:id.
В хеш, описывающий URL, при вызове link_to и родственных методов
можно поместить много спецификаторов. Если какой-то параметр не
найдется в правиле маршрутизации, он будет добавлен в строку запроса генерируемого URL. Например, если добавить
:some_other_thing => "blah"

в хеш, передаваемый методу link_to в примере выше, то получится такой URL:
http://localhost:3000/auctions/3/1?some_other_thing=blah

Замечание о порядке маршрутов
И при распознавании, и при генерации маршруты перебираются в том
порядке, в котором они определены в файле routes.rb. Перебор завершается при обнаружении первого соответствия, поэтому следует остерегаться ложных срабатываний.
Предположим, например, что в файле routes.rb есть два следующих
маршрута:
map.connect "users/help", :controller => "users"
map.connect ":controller/help", :controller => "main"

Если пользователь зайдет на URL /users/help, то справку выдаст дейст­
вие users/help, а если на URL /any_other_controller/help, – сработает
действие help контроллера main. Согласен, нетривиально.
А теперь посмотрим, что случится, если поменять эти маршруты местами:
map.connect ":controller/help", :controller => "main"
map.connect "users/help", :controller => "users"

Если пользователь заходит на /users/help, то сопоставляется первый
маршрут, поскольку более специализированный маршрут, в котором
часть users обрабатывается по-другому, определен в файле ниже.
Тут есть прямая аналогия с другими операциями сопоставления, например с предложением case:
case string
when /./
puts "Сопоставляется с любым символом!"

Применение регулярных выражений в маршрутах

103

when /x/
puts "Сопоставляется с 'x'!"
end

Во вторую ветвь when мы никогда не попадем, потому что строка 'x' будет сопоставлена в первой ветви. Необходимо всегда сначала располагать частные, а потом – общие случаи:
case string
when /x/
puts "Сопоставляется с 'x'!"
when /./
puts "Сопоставляется с любым символом!"
end

В примерах предложений case мы воспользовались регулярными выражениями – /x/ и т. д. – для записи образцов, с которыми сопоставляется строка. Регулярные выражения встречаются и в синтаксисе маршрутов.

Применение регулярных выражений
в маршрутах
Иногда требуется не просто распознать маршрут, а извлечь из него более детальную информацию, чем позволяют компоненты и поля. Можно воспользоваться регулярными выражениями1.
Например, можно маршрутизировать все запросы show на действие
error, если поле id не числовое. Для этого следует создать два маршрута:
один – для числовых идентификаторов, а другой – для всех остальных:
map.connect ':controller/show/:id',
:id => /\d+/, :action => "show"
map.connect ':controller/show/:id',
:action => "alt_show"

Если хотите (в основном ради понятности), можете обернуть ограничения, записанные в терминах регулярных выражений, в специальный
хеш параметров с именем :requirements:
map.connect ':controller/show/:id',
:action => "show", :requirements => { :id => /\d+/ }

Регулярные выражения в маршрутах могут быть полезны, особенно
если существуют маршруты, отличающиеся только образцами компонентов. Но это нельзя считать полноценной заменой контролю целостности данных. URL, сопоставившийся с маршрутом, в котором
1

Дополнительную информацию о регулярных выражениях см. в книге Хэла
Фултона (Hal Fulton) The Ruby Way, опубликованной в этой же серии.

104

Глава 3. Маршрутизация

есть регулярные выражения, можно уподобить кандидату, прошедшему интервью первой ступени. Необходимо еще убедиться, что по­
ступившее значение допустимо с точки зрения предметной области
приложения.

Параметры по умолчанию и метод url_for
Методы генерации URL, которыми вы, скорее всего, будете пользоваться, – link_to, redirect_to и им подобные – на самом деле являются
обертками низкоуровневого метода url_for. Но метод url_for заслу­
живает рассмотрения и сам по себе, поскольку это позволит кое-что
узнать о генерировании URL в Rails (а, возможно, вам когда-нибудь
захочется вызвать url_for напрямую).
Метод url_for предназначен для генерации URL по вашим спецификациям с учетом правил в сопоставившемся маршруте. Этот метод не выносит пустоты: при генерации URL он пытается заполнить максимально возможное число полей, а если не может найти для конкретного поля значение в переданном вами хеше, то ищет его в параметрах текущего запроса.
Другими словами, столкнувшись с отсутствием значений для каких-то
частей URL, url_for по умолчанию использует текущие значения :controller, :action и, если нужно, других параметров, необходимых маршруту.
Это означает, что можно не повторять задание одной и той же информации, если вы находитесь в пределах одного контроллера. Например,
внутри представления show для шаблона, принадлежащего контроллеру auctions, можно было бы создать ссылку на действие edit следующим образом:
"edit", :id => @auction.id %>

В предположении, что рендеринг этого представления выполняют
только действия контроллера auctions, текущим контроллером на этапе рендеринга всегда будет auctions. Поскольку в хеше для построения
URL нет ключа :controller, генератор автоматически выберет auctions,
и после подстановки в маршрут по умолчанию (:controller/:action/:id)
получится следующий URL (для аукциона 5):
Редактировать аукцион

То же справедливо и в отношении действий. Если не передавать ключ :
action, будет подставлено текущее действие. Однако имейте в виду, что
довольно часто одно действие выполняет рендеринг шаблона, принадлежащего другому действию. Поэтому выбор текущего действия по
умолчанию встречается реже, чем выбор текущего контроллера.

Параметры по умолчанию и метод url_for

105

Что случилось с :id
Отметим, что в предыдущем примере мы выбрали :controller по умолчанию, но были вынуждены явно задать значение для :id. Объясняется это тем, как работает механизм выбора умолчаний в методе url_for.
Генератор маршрутов просматривает сегменты шаблона URL слева направо, а шаблон в нашем случае выглядит так:
:controller/:action/:id

Поля заполняются параметрами из текущего запроса до тех пор, пока
не встретится поле, для которого явно задано значение:
:controller/:action/:id
по умолчанию!
задано!

Встретив поле, для которого вы задали значение, генератор проверяет,
совпадает ли это значение с тем, которое он все равно использовал бы
по умолчанию. Поскольку в нашем примере задействуется шаблон
show, а ссылка ведет на действие edit, то для поля :action передано не то
значение, которое было бы выбрано по умолчанию.
Обнаружив значение, отличающееся от умалчиваемого, метод url_for
вообще прекращает использовать умолчания. Он решает, что раз уж
вы один раз отошли от умолчаний, то и в дальнейшем к ним не вернетесь, – первое поле со значением, отличным от умалчиваемого, и все
поля справа от него не получают значений из текущего запроса.
Именно поэтому мы задали для :id конкретное значение, хотя оно
вполне могло бы совпадать со значением params[:id], оставшемся от
предыдущего запроса.
Контрольный вопрос: что произойдет, если данный маршрут сделать
маршрутом по умолчанию
map.connect ':controller/:id/:action'

а потом произвести следующие изменения в шаблоне show.rhtml:
"edit" %>

Ответ: поскольку :id теперь находится не справа, а слева от :action, генератор с радостью заполнит поля :controller и :id значениями из текущего запроса. Затем вместо :action он подставит строку "edit", по­
скольку мы зашили ее в шаблон. Справа от :action ничего не осталось,
следовательно, все уже сделано.
Поэтому, если это представление show для аукциона 5, то мы получим
ту же гиперссылку, что и раньше. Почти. Так как маршрут по умолчанию изменился, поменяется и порядок полей в URL:
Редактировать аукцион

106

Глава 3. Маршрутизация

Никаких преимуществ это не дает. Но у нас и цель другая – понять,
как работает система маршрутизации, наблюдая за тем, что происходит при небольших изменениях.

Использование литеральных URL
Если угодно, можете зашить пути и URL в виде строковых аргументов
метода link_to, redirect_to и им подобных. Например, вместо:
"main", :action => "help" %>

можно было бы написать:

Однако при использовании литерального пути или URL вы полностью
обходите систему маршрутизации. Применяя литеральные URL, вы
берете на себя ответственность за их сопровождение. (Можете, конечно, для вставки значений пользоваться механизмом интерполяции
строк, который предоставляет Ruby, но прежде чем становиться на
этот путь, подумайте, надо ли вам заново реализовывать функциональность Rails.)

Маскирование маршрутов
В некоторых случаях желательно выделить из маршрута один или несколько компонентов, не проводя поочередное сопоставление с конкретными позиционными параметрами. Например, ваши URL могут
отражать структуру дерева каталогов. Если пользователь заходит на
URL
files/list/base/books/fiction/dickens
то вы хотите, чтобы действие files/list получило доступ ко всем четырем оставшимся полям. Однако иногда полей может быть всего три:
/files/list/base/books/fiction

или пять:
/files/list/base/books/fiction/dickens/little_dorrit

Следовательно, необходим маршрут, который сопоставлялся бы со
всем после второй компоненты URI (для данного примера).
Добиться этого можно с помощью маскирования маршрутов (route
globbing). Для маскирования употребляется звездочка:
map.connect 'files/list/*specs'

Теперь действие files/list будет иметь доступ к массиву полей URL через элемент params[:specs]:

Маскирование пар ключ/значение

107

def list
specs = params[:specs] # например, ["base", "books", "fiction", "dickens"]
end

Маска может встречаться только в конце строки-образца. Такая конструкция недопустима:
map.connect 'files/list/*specs/dickens' # Не работает!

Маска проглатывает все оставшиеся компоненты URL, поэтому из самой
ее семантики вытекает, что она должна находиться в конце образца.

Маскирование пар ключ/значение
Маскирование маршрутов могло бы составить основу более общего механизма составления запросов о лотах, выставленных на аукцион.
Предположим, что нужно придумать схему для представления URL
следующего вида:
http://localhost:3000/items/field1/value1/field2/value2/...

В ответ на такой запрос необходимо вернуть список всех лотов, для которых указанные поля имеют указанные значения, причем количество
пар «поле-значение» в URL неограниченно.
Иными словами, URL http://localhost:3000/items/year/1939/medium/wood
должен генерировать список всех деревянных изделий, произведенных в 1939 году.
Эту задачу решает следующий маршрут:
map.connect 'items/*specs', :controller => "items", :action => "specify"

Разумеется, для поддержки такого маршрута необходимо соответствующим образом написать действие specify, например так, как показано
в листинге 3.2.
Листинг 3.2. Действие specify
def specify
@items = Item.find(:all, :conditions => Hash[params[:specs]])
if @items.any?
render :action => "index"
else
flash[:error] = "Не могу найти лоты с такими свойствами"
redirect_to :action => "index"
end
end

А что делает метод «квадратные скобки» класса Hash? Он преобразует
одномерный массив пар «ключ/значение» в хеш! Еще одно свидетель­
ство в пользу того, что без глубокого знания Ruby не стать экспертом
в Rails.

108

Глава 3. Маршрутизация

Следующая остановка: именованные маршруты – способ инкапсуляции логики маршрутизации в специализированных методах-помощниках.

Именованные маршруты
Тема именованных маршрутов заслуживает отдельной главы. То, что
вы сейчас узнаете, найдет непосредственное продолжение при изучении связанных с REST аспектов маршрутизации в главе 4.
Идея именованных маршрутов призвана главным образом облегчить
жизнь программисту. С точки зрения приложения никаких видимых
эффектов это не дает. Когда вы присваиваете маршруту имя, в контроллерах и представлениях определяется новый метод с именем name_
url (где name – имя, присвоенное маршруту). При вызове этого метода
с подходящими аргументами генерируется URL для маршрута. Кроме
того, создается еще и метод name_path, который генерирует только путевую часть URL без протокола и имени хоста.

Создание именованного маршрута
Чтобы присвоить имя маршруту, вызывается особый метод объекта
map, которому передается имя, а не обычный метод connect:
map.help 'help',
:controller => "main",
:action => "show_help"

В данном случае вы получаете методы help_url и help_path, которые
можно использовать всюду, где Rails ожидает URL или его компонент:

И, разумеется, обычные правила распознавания и генерации остаются
в силе. Образец включает только статическую строку "help". Поэтому
в гиперссылке вы увидите путь
/help

При щелчке по этой ссылке будет вызвано действие show_help контроллера main.

Что лучше: name_path или name_url?
При создании именованного маршрута в действительности создаются по
крайней мере два метода-помощника. В предшествующем примере они
назывались help_url и help_path. Разница между этими методами в том,
что метод _url генерирует полный URL, включая протокол и доменное
имя, а _path – только путь (иногда говорят относительный путь).

Как выбирать имена для маршрутов

109

Согласно спецификации HTTP, при переадресации следует задавать
URI, и некоторые считают, что речь идет о полностью квалифицированном URL.1 Поэтому, если вы хотите быть педантом, то, вероятно,
следует пользоваться методом _url при передаче именованного маршрута в качестве аргумента методу redirect_to в коде контроллера.
Метод redirect_to, похоже, отлично работает и с относительными путями, которые генерирует помощник _path, поэтому споры на эту тему
более-менее бессмысленны. На самом деле, если не считать переадресации, перманентных ссылок и еще некоторых граничных случаев, путь
Rails состоит в том, чтобы использовать _path, а не _url. Первый метод
порождает более короткую строку, а пользовательский агент (броузер
или еще что-то) должен уметь выводить полностью квалифицированный URL, зная HTTP-заголовки запроса, базовый адрес документа
и URL запроса.
Читая эту книгу и изучая код и примеры из других источников, помните, что help_url и help_path делают по существу одно и то же. Я предпочитаю употреблять метод _url при обсуждении техники именованных маршрутов, но пользоваться методом _path внутри шаблонов представлений
(например, при передаче параметров методам link_to и form_for). В общем, это вопрос стиля, базирующийся на теории о том, что URL – общая
вещь, а путь – специализированная. В любом случае имейте в виду оба
варианта и запомните, что они очень тесно связаны между собой.

Замечания
Именованные маршруты позволяют немного сэкономить на генерации
URL. Именованный маршрут сразу выводит вас на тот маршрут, который вам нужен, минуя процедуру сопоставления. Это означает, что
можно предоставлять меньше деталей, чем пришлось бы в противном
случае. Задавать значения всех метапараметров в образце маршрута
все равно необходимо, но про зашитые в код связанные параметры
можно забыть. Единственная причина, по которой последние задаются, состоит в том, чтобы при генерации URL направить систему маршрутизации к нужному маршруту. Однако, когда вы используете именованный маршрут, система уже знает, какое правило вы хотите применить, поэтому налицо (небольшое) повышение производительности.

Как выбирать имена для маршрутов
Самый лучший способ понять, какие именованные маршруты вам
нужны, – применить подход «сверху вниз». Подумайте, что вы хотите
1

Зед Шоу (Zed Shaw), автор веб-сервера Mongrel и эксперт во всем, что относится к протоколу HTTP, не смог дать мне исчерпывающий ответ на этот
вопрос, а это кое о чем говорит (о нестрогости спецификации HTTP, а не
о некомпетентности Зеда).

110

Глава 3. Маршрутизация

написать в коде приложения, а потом создайте маршруты, которые помогут решить стоящую перед вами задачу.
Рассмотрим, например, следующее обращение к link_to:
"auctions",
:action => "show",
:id => auction.id %>

Правило маршрутизации для сопоставления с этим путем (обобщенный маршрут) выглядит так:
map.connect "auctions/:id",
:controller => "auctions",
:action => "show"
Как-то не хочется еще раз перечислять все параметры маршрутизации
только для того, чтобы система поняла, какой маршрут нам нужен.
И было бы очень неплохо сократить код вызова link_to. В конце концов,
в правиле маршрутизации контроллер и действие уже определены.
Вот вам и неплохой кандидат на роль именованного маршрута. Мы можем улучшить ситуацию, заведя маршрут auction_path:
auction.id) %>

Присваивание маршруту имени – это срезание пути; имя выводит нас
прямо на нужный маршрут без утомительного поиска, избавляя заодно от необходимости задавать длинные описания параметров, зашитых
в маршрут.

Говорит Кортенэ…
Не забывайте экранировать описания лотов!
Такие ссылки, как #{auction.item.description}, всегда следует заключать в метод h() во избежание атак с использованием кросссайтовых сценариев (XSS). Если, конечно, вы не реализовали
какой-нибудь хитроумный способ контроля входных данных.

Именованный маршрут выглядит так же, как обычный, – мы лишь заменяем слово connect именем маршрута:
map.auction "auctions/:id",
:controller => "auctions",
:action => "show"

В представлении теперь можно использовать более компактный вариант
link_to, а гиперссылка (для аукциона 3) содержит следующий URL:
http://localhost:3000/auctions/show/3

Как выбирать имена для маршрутов

111

Синтаксическая глазурь
Аргумент, передаваемый методу auction_path, можно еще сократить.
Если в качестве аргумента именованному маршруту нужно передать
идентификатор, достаточно указать лишь число, опустив ключ :id:

Но можно пойти еще дальше – достаточно передать объекты, и Rails
извлечет идентификатор автоматически:

Этот принцип распространяется и на другие метапараметры в строкеобразце именованного маршрута. Например, если имеется такой маршрут:
map.item 'auction/:auction_id/item/:id',
:controller => "items",
:action => "show"

то, вызвав метод link_to следующим образом:

вы получите следующий путь (который зависит от конкретного значения идентификатора):
/auction/5/item/11

Здесь мы дали Rails возможность вывести идентификаторы объектов
аукциона и лота. При условии что аргументы передаются в порядке
расположения их идентификаторов в образце, в сгенерированный путь
будут подставлены правильные значения.

Еще немного глазури?
Вовсе необязательно, чтобы генератор маршрутов вставлял в URL
именно значение идентификатора. Можно подменить значение, определив в модели метод to_param. Предположим, вы хотите, чтобы в URL
аукциона по продаже некоторого лота фигурировало название этого
лота. В файле модели item.rb переопределим метод to_param. В данном
случае сделаем это так, чтобы он возвращал «нормализованное» название (знаки препинания убраны, а между словами вставлены знаки подчеркивания):
def to_param
description.gsub(/\s/, "-").gsub([^\w-], '').downcase
end

Тогда вызов метода item_path(@item) вернет нечто подобное:
/auction/3/item/cello-bow

112

Глава 3. Маршрутизация

Разумеется, если в поле :id вы помещаете строку типа cello-bow, то
должны как-то научиться снова получать из нее объект. Приложения
для ведения блогов, в которых эта техника используется с целью создания «жетонов» (slugs) в перманентных ссылках, часто заводят в базе
данных отдельный столбец для хранения «нормализованной» версии
названия, выступающего как часть пути. В результате для восстановления исходного объекта можно сделать что-то типа:
Item.find_by_munged_description(params[:id])

И, конечно, в маршруте можно назвать этот параметр не :id, а как-то
более осмысленно!

Говорит Кортенэ…
Почему не следует употреблять в URL числовые идентификаторы?
Во-первых, потому что конкуренты могут увидеть, сколько вы создали аукционов. Во-вторых, если идентификаторы – последовательные числа, можно написать автоматизированного паука, который будет воровать ваш контент. В-третьих, это открывает дверь
в вашу базу данных. И наконец, слова просто приятнее выглядят.

Метод организации контекста with_options
Иногда полезно создать несколько именованных маршрутов, относящихся к одному и тому же контроллеру. Эту задачу можно решить
с помощью метода with_options объекта map.
Предположим, что имеются такие именованные маршруты:
map.help '/help', :controller => "main", :action => "help"
map.contact '/contact', :controller => "main", :action => "contact"
map.about '/about', :controller => "main", :action => "about"

Все три маршрута можно консолидировать следующим образом:
map.with_options :controller => "main" do |main|
main.help '/help', :action => "help"
main.contact '/contact', :action => "contact"
main.about '/about', :action => "about"
end

Три внутренних вызова создают именованные маршруты с ограниченным контекстом, в котором значением параметра :controller является
строка "main", поэтому трижды повторять это не нужно.
Отметим, что внутренние методы выполняются от имени объекта main,
а не map. После организации контекста вызовы методов вложенного
объекта main выполняют всю грязную работу.

113

Заключение

Говорит Кортенэ…
Квалифицированный программист Rails, измеряя производительность приложения под нагрузкой, обратит внимание, что
маршрутизация, распознавание маршрутов, а также методы
url_for, link_to и родственные им часто оказываются самой медленной частью цикла обработки запросов. (Примечание: это несущественно, пока количество просмотров страниц не достигнет
тысяч в час, поэтому не занимайтесь преждевременной оптимизацией.)
Распознавание маршрутов работает медленно, потому что на
время вычисления маршрута все остальное приостанавливается.
Чем больше маршрутов, тем медленнее все крутится. В некоторых проектах количество маршрутов исчисляется сотнями.
Генерация URL работает медленно, потому что часто в странице
встречается много обращений к link_to.
Что в этом случае делать разработчику? Первое, что следует предпринять, когда приложение кряхтит и стонет от непосильной нагрузки (вот ведь повезло кому-то!), – кэшировать сгенерированные URL или заменить их текстом. Каждый такой шаг позволяет
выиграть какие-то миллисекунды, но все они суммируются.

Заключение
Первая половина этой главы помогла вам разобраться в общих принципах маршрутизации, основанной на правилах map.connect, и понять, что у системы маршрутизации двоякое назначение:
• распознавание входящих запросов и отображение их на действия
контроллера с попутной инициализацией дополнительных переменных-приемников;
• распознавание параметров URL в методе link_to и родственных ему,
а также поиск соответствующего маршрута, по которому можно сгенерировать HTML-ссылки.
Знания об общей природе маршрутизации мы дополнили более продвинутыми приемами, например использованием регулярных выражений и маскированием маршрутов.
Наконец, перед тем как двигаться дальше, удостоверьтесь, что понимаете, как работают именованные маршруты и почему они упрощают
жизнь разработчика, позволяя сократить код представлений. В следующей главе мы будем определять группы взаимосвязанных именованных маршрутов, и вы поймете, что сейчас мы взобрались на вышку,
с которой удобно прыгать в пучины REST.

4
REST, ресурсы и Rails
Пока не появился REST, я (как и многие другие) по-настоящему
не понимал, куда помещать свое барахло.
Йонас Никлас, сообщение в списке рассылки по Ruby on Rails

В версии 1.2 в Rails была добавлена поддержка проектирования в соответствии с архитектурным стилем REST. Cпецификация REST (Represen­
ta­tional State Transfer – передача представляемых состояний) – сложная
тема из области теории информации, ее полное рассмотрение выходит
далеко за рамки этой главы1. Однако некоторые краеугольные положения мы все же осветим. В любом случае средства REST, предоставляемые Rails, могут быть вам полезны, даже если вы не эксперт и не горячий приверженец REST.
Основная причина состоит в том, что все разработчики сталкиваются
с задачей названия и организации ресурсов и действий в своих приложениях. Типичные операции для всех приложений с хранением в базе
данных прекрасно укладываются в парадигму REST, и очень скоро вы
в этом убедитесь.
1

Для тех, кто интересуется стилем Rest, каноническим текстом является диссертация Роя Филдинга, которую можно найти по адресу http://www.ics.uci.
edu/~fielding/pubs/dissertation/top.htm. Особенно интересны главы 5 и 6,
в которых REST рассматривается во взаимосвязи с HTTP. Кроме того, массу
информации и ссылки на дополнительные ресурсы можно найти на викисайте, посвященном REST, по адресу http://rest.blueoxen.net/cgi-bin/wiki.pl.

О REST в двух словах

115

О REST в двух словах
Рой Томас Филдинг (Roy T. Fielding), создатель REST, называет свое
детище сетевым «архитектурным стилем», точнее, стилем, проявляющимся в архитектуре World Wide Web. На самом деле Филдинг является не только создателем REST, но и одним из авторов самого протокола HTTP, – REST и Сеть очень тесно связаны друг с другом.
Филдинг определяет REST как ряд ограничений, налагаемых на взаимодействие между компонентами системы. По существу, вначале имеется просто множество компьютеров, способных общаться между собой, а затем мы постепенно налагаем ограничения, разрешающие одни
способы общения и запрещающие другие.
В число ограничений REST (среди прочих) входят:
• применение архитектуры клиент-сервер
• коммуникация без сохранения состояния
• явное извещение о возможности кэширования ответа
Сеть World Wide Web допускает коммуникацию, отвечающую требованиям REST. Но она также допускает и нарушения принципов REST –
ограничения не будут соблюдаться, если вы специально об этом не позаботитесь. Однако Филдинг – один из авторов протокола HTTP и, хотя
у него есть некоторые претензии к этому протоколу с точки зрения
REST (как и критические замечания по поводу широкого распространения практики, не согласующейся с принципами REST, например использования cookies), общее соответствие между REST и веб – не случайное совпадение.
REST проектировался с целью помочь вам предоставлять службы, причем не произвольным образом, а в согласии с идиомами и конструкциями, присущими HTTP. Если поищете, то легко найдете многочисленные дискуссии, в которых REST сравнивается, например, с SOAP.
Смысл аргументов в защиту REST сводится к тому, что HTTP уже позволяет предоставлять службы, поэтому дополнительный семантический уровень поверх него излишен. Просто надо уметь пользоваться тем,
что дает HTTP.
Одно из достоинств стиля REST заключается в том, что он хорошо масштабируется для больших систем, например Сети. Кроме того, он поощряет, даже требует, использовать стабильные долгоживущие идентификаторы ресурсов (URI). Компьютеры общаются между собой, посылая запросы и ответы, помеченные этими идентификаторами. Эти
запросы и ответы содержат также представления (в виде текста, XML,
графики и т. д.) ресурсов (высокоуровневое, концептуальное описание
содержимого). В идеале, запрашивая у компьютера XML-представление ресурса, скажем «Ромео и Джульетта», вы каждый раз указываете
в запросе один и тот же идентификатор и метаданные, описывающие,

116

Глава 4. REST, ресурсы и Rails

что требуется получить именно XML, и получаете один и тот же ответ.
Если возвращаются разные ответы, должна быть причина, например
запрашиваемый ресурс является изменяемым («Текущая интерпретация для студента №3994»).
Ниже мы еще вернемся к ресурсам и представлениям. А пока посмотрим, какое место в этой картине занимает Rails.

REST в Rails
Поддержка REST в Rails складывается из методов-помощников и дополнений к системе маршрутизации, спроектированных так, чтобы придать определенный стиль, логику и порядок контроллерам, а стало быть,
и восприятию приложения внешним миром. Это больше чем просто набор соглашений об именовании (хотя и это тоже). Чуть ниже мы поговорим о деталях, а пока отметим, что по большому счету преимущества от
использования REST в Rails можно разбить на две категории:
• для вас – удобство и автоматическое следование методикам, доказавшим свою состоятельность на практике;
• для всех остальных – согласованный с REST интерфейс к службам
вашего приложения.
Извлечь пользу из первого преимущества можно даже в том случае,
когда второе вас не волнует. На самом деле именно на этом аспекте мы
и сконцентрируем внимание: как поддержка REST в Rails может облегчить вам жизнь и помочь сделать код элегантнее.
Мы не хотим преуменьшать важность стиля REST как такового или
подвергать сомнению стремление предоставлять согласованные с REST
службы. Просто невозможно рассказать обо всем на свете, а этот раздел
книги посвящен маршрутизации, поэтому взглянем на REST именно
под этим углом зрения.
Заметим еще, что взаимоотношения между Rails и REST, хотя и плодотворные, не свободны от сложностей. Многие подходы, применяемые
в Rails, изначально не согласуются с предпосылками REST. Стиль
REST предполагает коммуникацию без сохранения состояния – каждый запрос должен содержать все необходимое получателю для выработки правильного ответа. Но практически все сколько-нибудь сложные программы для Rails нуждаются в сохранении состояния на сервере для отслеживания сеансов. И эта практика несовместима с идеологией REST. Cookies на стороне клиента – тоже используемые во многих
приложениях Rails – Филдинг отметает как не согласующуюся с REST
практику.
Распутывать все узлы и разрешать все дилеммы мы не станем. Повторюсь: наша цель – показать, как устроена поддержка REST, и распах-

Маршрутизация и CRUD

117

нуть двери для дальнейшего исследования и применения на практике –
включая изучение диссертации Филдинга и теоретических постулатов
REST. Мы не сможем рассмотреть все, но то, о чем мы будем говорить,
совместимо с более широкой трактовкой темы.
История взаимоотношений REST и Rails начинается с CRUD…

Маршрутизация и CRUD
Акроним CRUD (Create Read Update Delete – Создание Чтение Обновление Удаление) – это классическая сводка операций с базой данных.
Заодно это призывный клич разработчиков на платформе Rails. По­
скольку мы обращаемся к базам данных с помощью абстракций, то
склонны забывать, как на самом деле все просто. Проявляется это
в придумывании слишком уж креативных имен для действий контроллеров. Возникает искушение называть действия как-то вроде add_item,
replace_email_address и т. д. Но в этом нет никакой необходимости.
Да, контроллер не отображается на базу данных в отличие от модели.
Но жизнь будет проще, если называть действия в соответствии с операциями CRUD или настолько близко к ним, насколько это возможно.
Система маршрутизации не «заточена» под CRUD. Можно создать
маршрут, ведущий к любому действию, как бы оно ни называлось.
Выбор CRUD-имен – это вопрос дисциплины. Но… при использовании
средств поддержки REST, предлагаемых Rails, это происходит автоматически.
Поддержка REST в Rails подразумевает и стандартизацию имен дейст­
вий. В основе этой поддержки лежит техника автоматического создания групп именованных маршрутов, которые жестко запрограммированы для указания на конкретный предопределенный набор действий.
В этом есть своя логика. Присваивать действиям CRUD-имена – это хорошо. Использовать именованные маршруты – удобно и элегантно. Поэтому применение механизмов REST – короткий путь к проверенным
практикой подходам.
Слова «короткий путь» не передают, насколько мало от вас требуется
для получения большой отдачи. Стоит поместить такое предложение:
map.resources :auctions

в файл routes.rb, как вы уже создадите четыре именованных маршрута, которые фактически позволяют соединиться с семью действиями
контроллера, – как именно, будет описано в этой главе. И у этих дейст­
вий будут симпатичные CRUD-совместимые имена.
Слово «resources» в выражении map.resources заслуживает особого внимания.

118

Глава 4. REST, ресурсы и Rails

Ресурсы и представления
Стиль REST характеризует коммуникацию между компонентами системы (здесь компонентом может быть, скажем, веб-броузер или сервер)
как последовательность запросов, ответами на которые являются представления ресурсов.
В данном контексте ресурс – это «концептуальное отображение» (Филдинг). Сами ресурсы не привязаны ни к базе данных, ни к модели, ни
к контроллеру. Вот некоторые примеры ресурсов:
• текущее время дня
• история выдачи книги библиотекой
• полный текст романа «Крошка Доррит»
• карта города Остин
• инвентарная ведомость склада
Ресурс может быть одиночным или множественным, изменяемым (как
время дня) или фиксированным (как текст «Крошки Доррит»). По существу это высокоуровневая абстракция того, что вы хотите получить,
отправляя запрос.
Но получаете вы не сам ресурс, а его представление. Именно здесь
REST распадается на мириады циркулирующих в Сети типов контента
и фактически доставляемых данных. В любой момент времени для ресурса существует несколько представлений (в том числе 0). Так, ваш
сайт может предлагать как текстовую, так и аудиоверсию «Крошки
Доррит». Обе версии будут считаться одним и тем же ресурсом, на который указывает один и тот же идентификатор (URI). Нужный тип
контента – то или иное представление – задается в запросе дополнительно.

Ресурсы REST и Rails
Как почти все в Rails, поддержка REST-совместимых приложений
«пристрастна», то есть предлагается конкретный способ проектирования REST-интерфейса, и чем выше ваша готовность принять его, тем
больший урожай удобств вы пожнете. Данные приложений Rails хранятся в базе, поэтому подход Rails к REST заключается в том, чтобы
как можно теснее ассоциировать ресурс с моделью ActiveRecord или парой модель/контроллер.
Терминология используется довольно свободно, например, часто можно услышать выражение «ресурс Book». На самом деле, обычно подразумевается модель Book, контроллер book с набором CRUD-действий
и ряд именованных маршрутов, относящихся к этому контроллеру
(благодаря предложению map.resources :books). Но пусть даже модель
Book и соответствующий контроллер существуют, ресурсы в смысле

Ресурсы и представления

119

REST, которые видны внешнему миру, обитают на более высоком уровне абстракции – «Крошка Доррит», история выдачи и т. д.
Лучший способ понять, как устроена поддержка REST в Rails, – двигаться от известного к новому, в данном случае от общей идеи именованных маршрутов к их специализации для целей REST.

От именованных маршрутов к поддержке REST
В начале разговора об именованных маршрутах мы приводили примеры консолидации различных сущностей в имени маршрута. Создав
маршрут вида
map.auction 'auctions/:id',
:controller => "auction",
:action => "show"

вы получаете возможность воспользоваться удобными методами-помощниками в следующих ситуациях:

Этот маршрут гарантирует, что будет сгенерирован путь, активирующий действие show контроллера auctions. Такие именованные маршруты хороши краткостью и легкостью зрительного восприятия.
Ассоциировав метод auction_path с действием auction/show, мы сделали
все необходимое в терминах стандартных операций с базой данных.
А теперь взглянем на это с точки зрения CRUD. Именованный маршрут auction_path хорошо согласуется с именем show (буквой R в акрониме CRUD). А что, если нам нужны хорошие имена маршрутов для действий create, update и delete.
Имя auction_path мы уже использовали для действия show. Можно было
бы предложить имена auction_delete_path, auction_create_path… но они
выглядят как-то громоздко. В действительности нам хотелось бы, чтобы вызов auction_path означал разные вещи в зависимости от того, на
какое действие указывает URL.
Поэтому нужен способ отличить один вызов auction_path от другого.
Можно было бы различать единственное (auction_path) и множест­
венное (auctions_path) число. URL в единственном числе семантически
озна­чает, что мы хотим что-то сделать с одним существующим объектом аукциона. Если же операция производится над множеством аукционов, то больше подходит множественное число.
К числу множественных операций над аукционами относится и создание. Обычно действие create встречается в таком контексте:

Здесь употребляется множественное число, поскольку мы говорим не
«выполнить действие применительно к конкретному аукциону», а «при-

120

Глава 4. REST, ресурсы и Rails

менительно ко всему множеству аукционов выполнить действие создания». Да, мы создаем один аукцион, а не много. Но в момент обращения к именованному маршруту auctions_path мы имеем в виду все аукционы вообще.
Другой случай, когда имеет смысл маршрут с именем во множественном числе, – получение списка всех объектов определенного вида или
просто какое-то общее представление, не ограниченное отображением
одного объекта. Такое представление обычно реализуется действием
index. Подобные действия, как правило, загружают много данных в одну или несколько переменных, а соответствующее представление выводит их в виде списка или таблицы (возможно, не одной).
И в этом случае хорошо бы иметь возможность написать так:

Но тут идея о создании вариантов маршрута auction_path в единственном и множественном числе упирается в потолок: уже есть два места,
где требуется множественное число. Одно из них create, другое – index.
Однако выглядят они одинаково:
http://localhost:3000/auctions

Как система маршрутизации узнает, что в одном случае мы имели
в виду действие create, а в другом – index? Нужен еще какой-то флаг –
переменная, по которой можно было бы организовать ветвление.
К счастью, такая переменная есть.

И снова о глаголах HTTP
Формы отправляются методом POST. Действия index запрашиваются
методом GET. Следовательно, нам нужно, чтобы система маршрутизации понимала, что
/auctions в GET-запросе

и
/auctions в POST-запросе

это разные вещи. Кроме того, мы хотим, чтобы она генерировала один
и тот же URL – /auctions – но разными HTTP-методами в зависимости
от обстоятельств.
Именно это и делает механизм поддержки REST в Rails. Он позволяет
сказать, что маршрут /auctions должен вести в разные места в зависимости от метода HTTP-запроса. Он дает возможность определить маршруты с одинаковыми именами, учитывающие, какой глагол HTTP
употреблен. Короче говоря, глаголы HTTP используются в нем в качестве тех самых дополнительных данных, которые необходимы для лаконичного решения поставленной задачи.

121

Стандартные RESТ-совместимые действия контроллеров

Это достигается за счет применения специальной команды маршрутизации: map.resources. Вот как она выглядит для аукционов:
map.resources :auctions

Это все. Одна такая строка в файле routes.rb эквивалентна определению
четырех именованных маршрутов (как вы вскоре убедитесь). А в результате комбинирования четырех маршрутов с различными методами
HTTP-запросов вы получаете семь полезных – очень полезных – перестановок.

Стандартные RESТ-совместимые
действия контроллеров
Вызов map.resources :auctions означает заключение некоей сделки
с системой маршрутизации. Система отдает вам четыре именованных
маршрута. Они могут вести на одно из семи действий контроллера
в зависимости от метода HTTP-запроса. В обмен вы соглашаетесь использовать строго определенные имена действий контроллера: index,
create, show, update, destroy, new, edit.
Ей-богу, это неплохая сделка, так как система проделывает за вас большую работу, а навязываемые имена действий очень близки к CRUD.
В табл. 4.1 суммированы все действия. Она устроена, как таблица умножения, – на пересечении строки, содержащей именованный маршрут, и столбца с методом HTTP-запроса находится то, что вы получаете
от системы. В каждой клетке (кроме незаполненных) показан, во-первых, генерируемый маршрутом URL, а во-вторых, действие, вызываемое при распознавании этого маршрута (в таблице упоминается только
метод _url, но вы получаете и метод _path).
Таблица 4.1. REST-совместимые маршруты, а также помощники, пути
и результирующие действия контроллера
Метод-помощник

GET

client_url(@client)

/clients/1 show

clients_url

/clients index

edit_client_url
(@client)
new_client_url

/clients/1/edit
edit
/clients/new new

POST

PUT

DELETE

/clients/1
update

/clients/1
destroy

/clients
create

Для действий edit и new имена маршрутов уникальны, а в URL применяется специальный синтаксис. К этим особым случаям мы еще вернемся ниже.
Поскольку именованные маршруты комбинируются с HTTP-методами, необходимо знать, как при генерации URL задать метод запроса,

122

Глава 4. REST, ресурсы и Rails

чтобы маршруты clients_url для методов GET и POST не выполняли одно
и то же действие контроллера. Большая часть того, что надлежит сделать, можно свести к нескольким правилам:
1. По умолчанию подразумевается HTTP-метод GET.
2. При обращении к методам form_tag и form_for автоматически используется HTTP-метод POST.
3. При необходимости (а она возникает в основном для операций PUT
и DELETE) вы можете явно указать метод запроса в дополнение к URL,
сгенерированному именованным маршрутом.
Необходимость задавать операцию DELETE возникает, например, в ситуации, когда вы хотите с помощью ссылки активировать действие
destroy:
auction(@auction),
:method => :delete %>

В зависимости от использованного метода-помощника (типа form_for)
можно поместить название HTTP-метода во вложенный хеш:
auction(@auction),
:html => { :method => :put } do |f| %>

В этом примере маршрут с именем в единственном числе комбинируется с методом PUT, что приводит к вызову действия update (см. пересечение строки 2 со столбцом 4 в табл 4.1).

Хитрость для методов PUT и DELETE
Вообще говоря, веб-браузеры отправляют запросы только методами GET
и POST. Чтобы заставить их посылать запросы PUT и DELETE, Rails необходимо проявить некую «ловкость рук». Вам об этом беспокоиться не надо, но полезно знать, что происходит за кулисами.
Запрос методом PUT или DELETE в контексте REST в Rails – это на самом
деле POST-запрос со скрытым полем _method, которому присваивается
значение put или delete. Приложение Rails, обрабатывающее запрос,
замечает это и маршрутизирует запрос на действие update или destroy
соответственно.
Таким образом, можно сказать, что поддержка REST в Rails опережает
время. Компоненты REST, применяющие протокол HTTP, обязаны понимать все методы запроса. Но не понимают, поэтому Rails приходится
вмешаться. Разработчику, который пытается понять, как именованные маршруты отображаются на имена действий, необязательно задумываться об этом мелком мошенничестве. И хочется надеяться, что со
временем нужда в нем отпадет.

Стандартные RESТ-совместимые действия контроллеров

123

Одиночные и множественные
RESTсовместимые маршруты
Имена некоторых REST-совместимых маршрутов записываются в единст­
венном числе (одиночные маршруты), других – во множественном (множественные маршруты). Логика такова:
1. Маршруты для действий show, new, edit и destroy одиночные, так
как они применяются к конкретному ресурсу.
2. Остальные маршруты множественные. Они относятся к множествам взаимосвязанных ресурсов.
Одиночным REST-совместимым маршрутам требуется аргумент, так
как они должны знать идентификатор элемента множества, к которому применяется действие. Синтаксически допускается как простой
список аргументов:
item_url(@item) # show, update или destroy в зависимости от глагола HTTP

так и хеш:
item_url(:id => @item)

Не требуется (хотя и не возбраняется) вызывать метод id объекта @item,
так как Rails понимает, что вы хотите сделать именно это.

Специальные пары: new/create и edit/update
Как видно из табл. 4.1, действия new и edit подчиняются специальным
соглашениям о REST-совместимых именах. Причина связана с действиями create и update и с тем, как они связаны с действиями new и edit.
Обычно операции create и update вызываются путем отправки формы.
Это означает, что с каждой из них на самом деле ассоциировано два
действия – два запроса:
1. Действие, приводящее к отображению формы.
2. Действие, заключающееся в обработке данных отправленной формы.
С точки зрения REST-совместимой маршрутизации это означает, что
действие create тесно связано с предшествующим ему действием new,
а действие update – с действием edit. Действия new и edit играют роль
ассистентов; их единственное назначение – показать пользователю
форму, необходимую для создания или обновления ресурса.
Для включения этих двухшаговых сценариев в общую картину ресурсов требуется небольшой трюк. Форма для редактирования ресурса сама по себе ресурсом не является. Это скорее «предресурс». Форму для
создания нового ресурса можно рассматривать как некий вид ресурса,
если допустить, что «быть новым», то есть не существовать, – нечто
такое, что ресурс может сделать, не переставая быть ресурсом...

124

Глава 4. REST, ресурсы и Rails

Да, подпустил я философии. Но вот как все это реализовано в Rails для
REST.
Считается, что действие new создает новый одиночный ресурс (а не множество ресурсов). Однако, так как логически эта транзакция описывается глаголом GET, а GET для одиночного ресурса уже соответствует
действию show, то для new необходим маршрут с отдельным именем.
Вот почему мы вынуждены писать

чтобы получить ссылку на действие items/new.
Что касается действия edit, то оно не должно давать полноценный ресурс, а скорее быть некоей «разновидностью для редактирования» дейст­
вия show. Поэтому для него применяется тот же URL, что для show, но
с модификатором в виде суффикса /edit, что совместимо с форматом
URL для действия new:
/items/5/edit

Стоит отметить, что до выхода версии Rails 2.0 действие edit отделялось точкой с запятой: /items/5;edit. Это решение было продиктовано
скорее ограничениями системы маршрутизации, нежели более возвышенными мотивами. Однако подобная схема создавала больше проблем, чем решала1, и была исключена из «острия Rails» сразу после
выхода версии Rails 1.2.3.
Соответствующий именованный маршрут называется edit_item_url
(@item). Как и в случае new, имя маршрута для действия edit содержит
дополнительные слова, чтобы отличить его от маршрута к действию
show, предназначенного для получения существующего одиночного ресурса методом GET.

Одиночные маршруты к ресурсам
Помимо метода map.resources, существует одиночная (или «синглетная») форма маршрутизации ресурса: map.resource. Она используется
для представления ресурса, который в данном контексте существует
в единственном числе.
1

Точка с запятой не только выглядит странно, но и создает ряд более существенных проблем. Например, она очень мешает кэшированию. Пользователи броузера Safari не могли аутентифицировать URL, содержащие точку
с запятой. Кроме того, некоторые веб-серверы (и прежде всего Mongrel)
справедливо считают, что точки с запятой являются частью строки запроса, так как этот символ зарезервирован, чтобы обозначать начало параметров пути (относящихся к элементу пути между символами косой черты,
в отличие от параметров запроса, следующих за символом «?»).

Вложенные ресурсы

125

Одиночный маршрут к ресурсу, расположенный в начале списка маршрутов, может быть полезен, когда во всем приложении или, быть может, в сеансе пользователя существует только один ресурс такого типа.
Например, приложение для ведения адресных книг предоставляет
каждому зарегистрированному пользователю собственную адресную
книгу, поэтому можно было бы написать:
map.resource :address_book

В результате из всего множества маршрутов к ресурсу вы получите
только одиночные: address_book_url для GET/PUT, edit_address_book_url
для GET и update_address_book_url для PUT.
Отметим, что имя метода resource, аргумент этого метода и имена всех
именованных маршрутов записываются в единственном числе. Предполагается, что вы работаете в контексте, где имеет смысл говорить
«адресная книга» – одна и только одна, поскольку для текущего пользователя есть лишь одна адресная книга. Контекст не устанавливается
автоматически – вы должны аутентифицировать пользователя и извлечь его адресную книгу из базы данных (а равно и сохранить ее) явно. В этом отношении никакой «магии» или чтения мыслей не преду­
смотрено; этого всего лишь еще одна техника маршрутизации, которой
вы при желании можете воспользоваться.

Вложенные ресурсы
Предположим, что нужно выполнять операции над заявками: создание, редактирование и т. д. Вы знаете, что каждая заявка ассоциирована с некоторым аукционом. Это означает, что, выполняя операцию над
заявкой, вы на самом деле оперируете парой заявка/аукцион или, если
взглянуть под другим углом зрения, вложенной структурой аукцион/
заявка. Заявки всегда встречаются в конце пути, проходящего через
аукцион.
Таким образом, в данном случае необходим URL вида
/auctions/3/bids/5

Действия при получении запроса к такому URL зависят, разумеется, от
употребленного глагола HTTP. Но семантика самого URL такова: ресурс, который можно идентифицировать как заявку 5 на аукционе 3.
Почему бы просто не перейти на URL bids/5, минуя аукцион? На то
есть две причины. Во-первых, первоначальный URL более информативен. Согласен, он длиннее, но увеличение длины служит для того, чтобы сообщить дополнительную информацию о ресурсе. Во-вторых, механизм реализации REST-совместимых маршрутов в Rails при таком
URL дает вам непосредственный доступ к идентификатору аукциона
через params[:auction_id].

126

Глава 4. REST, ресурсы и Rails

Для создания маршрутов к вложенным ресурсам поместите в файл
routes.rb такие строки:
map.resources :auctions do |auction|
auction.resources :bids
end

Отметим, что внутренний метод resources вызывается для объекта auction, а не map. Об этом часто забывают.
Смысл данной конструкции состоит в том, чтобы сообщить объекту
map, что вам нужны REST-совместимые маршруты к ресурсам аукциона, то есть вы хотите получить auctions_url, edit_auction_url и все остальное. Кроме того, вам необходимы REST-совместимые маршруты
к заявкам: auction_bids_url, new_auction_bid_url и т. д.
Однако, применяя команду для получения вложенных ресурсов, вы
обещаете, что при любом использовании именованного маршрута к заявке будете указывать аукцион, в который она вложена. В коде приложения это выглядит как аргумент метода именованного маршрута:

Такой вызов позволяет системе маршрутизации добавить часть /auctions/3 перед /bids. А на принимающем конце – в данном случае в дейст­
вии bids/index, на которое этот URL указывает, – вы сможете найти
идентификатор аукциона @auction в элементе params[:auction_id] (это
множественный REST-совместимый маршрут для метода GET; если забыли, справьтесь с табл. 4.1).
Глубина вложенности может быть произвольна. Каждый уровень вложенности на единицу увеличивает количество аргументов, передаваемых вложенным маршрутам. Следовательно, для одиночных маршрутов (show, edit, destroy) требуются по меньшей мере два аргумента, как
показано в листинге 4.1.
Листинг 4.1. Передача двух параметров для идентификации вложенного
ресурса с помощью link_to
:delete %>

Это позволяет системе маршрутизации получить информацию (@auction.id и @bid.id), необходимую ей для генерации маршрута.
Если хотите, можете добиться того же результата, передавая аргументы в хеше, но обычно так не делают, потому что код получается
длиннее:
auction_bid_path(:auction => @auction, :bid => @bid)

Вложенные ресурсы

127

Явное задание :path_prefix
Добиться эффекта вложенности маршрутов можно также, явно указав
параметр :path_prefix при обращении к методу отображения ресурсов.
Вот как это можно сделать для заявок, вложенных в аукционы:
map.resources :auctions
map.resources :bids, :path_prefix => "auctions/:auction_id"

В данном случае вы говорите, что все URL заявок должны включать
статическую строку auctions и значение auction_id, то есть контекстную
информацию, необходимую для ассоциирования заявок с конкретным
аукционом.
Основное отличие этого подхода от настоящего вкладывания ресурсов
связано с именами генерируемых методов-помощников. Вложенные
ресурсы автоматически получают префикс имени, соответствующий
родительскому ресурсу (см. auction_bid_path в листинге 4.1.)
Скорее всего, техника вкладывания будет встречаться вам чаще, чем
явное задание :path_prefix, потому что обычно проще позволить системе маршрутизации самостоятельно вычислить префикс, исходя из пути вложения ресурсов. Плюс, как мы скоро увидим, при желании нетрудно избавиться от лишних префиксов.

Явное задание :name_prefix
Иногда некий ресурс требуется вложить в несколько других ресурсов.
Или в одном случае обращаться к ресурсу по вложенному маршруту,
а в другом – напрямую. Может даже возникнуть желание, чтобы помощники именованных маршрутов указывали на разные ресурсы в зависимости от контекста, в котором исполняются1. Все это возможно
с помощью префикса имени :name_prefix, поскольку он позволяет управ­
лять процедурой, генерирующей методы-помощники для именованных маршрутов.
Предположим, что вы хотите обращаться к заявкам не только через
аукционы, как в предыдущих примерах, но и указывая лишь номер
заявки. Иными словами, требуется, чтобы распознавались и генерировались маршруты обоих видов:
/auctions/2/bids/5 и /bids/5

Первое, что приходит в голову, – задать в качестве первого помощника
bid_path(@auction, @bid), а в качестве второго – bid_path(@bid). Кажется
1

Тревор Сквайрс (Trevor Squires) написал замечательный подключаемый модуль ResourceFu, позволяющий реализовать такую технику. Вы можете загрузить его со страницы http://agilewebdevelopment.com/plugins/resource_fu.

128

Глава 4. REST, ресурсы и Rails

логичным предположить, что если необходим маршрут к заявке, не
проходящий через объемлющий ее аукцион, можно просто опустить
параметр, определяющий аукцион.
Принимая во внимание, что система маршрутизации автоматически
задает префиксы имен, вы должны переопределить name_prefix для заявок, чтобы все работало, как задумано (листинг 4.2).
Листинг 4.2. Переопределение name_prefix во вложенном маршруте
map.resources :auctions do |auction|
auction.resources :bids, :name_prefix => nil
end

Я широко применял такую технику в реальных приложениях и хочу
заранее предупредить вас, что после исключения механизма префиксации имен отлаживать ошибки маршрутизации становится на порядок труднее. Но у вас может быть и другое мнение.
В качестве примера рассмотрим, что нужно сделать, захоти мы получать доступ к заявкам по другому маршруту – через того, кто из разместил, а не через аукцион (листинг 4.3).
Листинг 4.3. Переопределение name_prefix во вложенном маршруте
map.resources :auctions do |auction|
auction.resources :bids, :name_prefix => nil
end
map.resource :people do |people|
people.resources :bids, :name_prefix => nil # вы уверены?
end

Поразительно, но код в листинге 4.3 должен бы1 работать правильно
и генерировать следующих помощников:
bid_path(@auction, @bid) # /auctions/1/bids/1
bid_path(@person, @bid) # /people/1/bids/1

Но, если идти по этому пути, код контроллера и представления может
усложниться.
Сначала контроллер должен будет проверить, какой из элементов
params[:auction_id] и params[:person_id] существует, и загрузить соответствующий контекст. В шаблонах представлений, вероятно, придется выполнить аналогичную проверку, чтобы сформировать правильное
отображение. В худшем случае появится куча предложений if/else,
загромождающих код!
Решая заняться программированием подобного дуализма, вы, скорее
всего, идете по ложному пути. К счастью, мы можем явно указать,
1

Могу лишь сказать должен бы, поскольку маршрутизация исторически является наиболее изменчивой частью Rails, так что работа этого кода зависит
от конкретной версии. Я точно знаю, что в версии Rails 1.2.3 он не работает.

Вложенные ресурсы

129

какой контроллер следует ассоциировать с каждым из наших маршрутов.

Явное задание RESTсовместимых контроллеров
Мы пока еще не говорили о том, как REST-совместимые маршруты
отображаются на контроллеры. Из всего вышесказанного могло сложиться впечатление, что это происходит автоматически. На самом деле так оно и есть, и основой является имя ресурса.
Вернемся к нашему примеру и рассмотрим следующий вложенный
маршрут:
map.resources :auctions do |auction|
auction.resources :bids
end

Здесь участвуют два контроллера: AuctionsController и BidsController.
Можно явно указать, какой из них использовать, задействовав параметр:controller метода resources. Он позволяет присвоить ресурсу
произвольное имя (видимое пользователю), сохранив согласованность имени контроллера с различными стандартами именования,
например:
map.resources :my_auctions, :controller => :auctions do |auction|
auction.resources :my_bids, :controller => :bids
end

А теперь все вместе
Теперь, познакомившись с параметрами :name_prefix, :path_prefix,
и :controller, мы можем собрать все воедино и показать, когда точный
контроль над REST-совместимыми маршрутами может быть полезен.
Например, сделанное в листинге 4.3 можно усовершенствовать, воспользовавшись параметром :controller (листинг 4.4).
Листинг 4.4. Несколько вложенных ресурсов заявок
с явно заданным контроллером
map.resources :auctions do |auction|
auction.resources :bids, :name_prefix => nil,
:controller => :auction_bids
end
map.resource :people do |people|
people.resources :bids, :name_prefix => nil,
:controller => :person_bids
end

На практике классы AuctionBidsController и PersonBidsController, вероятно, будут расширять один и тот же родительский класс, как показа-

130

Глава 4. REST, ресурсы и Rails

но в листинге 4.5, и пользоваться фильтрами before для загрузки правильного контекста.
Листинг 4.5. Определение подклассов контроллеров
для работы с вложенными маршрутами
class BidsController < ApplicationController
before_filter :load_parent
before_filter :load_bid
protected
def load_parent
# переопределяется в подклассах
end
def load_bid
@bids = @parent.bids
end
end
class AuctionBidsController < BidsController
protected
def load_parent
@parent = @auction = Auction.find(params[:auction_id])
end
end
class PersonBidsController < BidsController
protected
def load_parent
@parent = @person = Person.find(params[:person_id])
end
end

Отметим, что обычно именованные параметры задаются в виде символов, но параметр :controller понимает и строки, поскольку это необходимо, когда класс контроллера находится в пространстве имен, как
в следующем примере, где задается административный маршрут для
аукционов:
map.resources :auctions,
:controller => 'admin/auctions', # Admin::AuctionsController
:name_prefix => 'admin_',
:path_prefix => 'admin'

Вложенные ресурсы

131

Замечания
Нужна ли вложенность? В случае одиночных маршрутов вложенность
обычно не дает ничего такого, что без нее получить нельзя. В конце концов любая заявка принадлежит какому-то аукциону. Это означает, что
доступ к bid.auction_id ничуть не сложнее, чем к params[:auction_id],
в предположении, что объект заявки у вас уже есть.
Более того, объект заявки не зависит от вложенности. Элемент params[:id]
получит значение 5, и соответствующую запись можно извлечь из базы
данных напрямую. Совсем необязательно знать, какому аукциону эта
заявка принадлежит.
Bid.find(params[:id])

Стандартное обоснование разумного применения вложенных ресурсов,
которое чаще всего приводит Дэвид, – простота контроля над разрешениями и контекстными ограничениями. Как правило, доступ к вложенному ресурсу должен быть разрешен только в контексте родительского ресурса, и проконтролировать это в программе несложно, если
помнить, что вложенный ресурс загружается с помощью ассоциации
ActiveRecord родителя (листинг 4.6).
Листинг 4.6. Загрузка вложенного ресурса с помощью ассоциации
has_many родителя
@auction = Auction.find(params[:auction_id])
@bid = @auction.bids.find(params[:id]) # предотвращает несоответствие между
# аукционом и заявкой

Если вы хотите добавить к аукциону заявку, то URL вложенного ресурса будет выглядеть так:
http://localhost:3000/auctions/5/bids/new

Аукцион идентифицируется в URL, в результате чего отпадает необходимость загромождать форму скрытыми полями, называть дейст­
вие add_bid, сохранять идентификатор пользователя в :id и прибегать
к другим не согласующимся с REST хитростям.

О глубокой вложенности
Джеймис Бак (Jamis Buck) – очень влиятельная фигура в сообществе
пользователей Rails, почти такая же влиятельная, как сам Дэвид.
В феврале 2007 года в своем блоге1 он поделился мыслью о том, что глубокая вложенность – это плохо, и предложил следующее эвристическое правило: «Уровень вложенности ресурса никогда не должен превышать единицу».
1

http://weblog.jamisbuck.org/2007/2/5/nesting-resources.

132

Глава 4. REST, ресурсы и Rails

Этот совет продиктован опытом и практическими соображениями. Методы-помощники для маршрутов, вложенных более чем на два уровня,
становятся слишком длинными и неуклюжими. При работе с ними
легко допустить ошибку, а понять, что не так, довольно сложно.
Предположим, что в нашем приложении с заявкой может быть связано
несколько комментариев. Можно было бы следующим образом определить маршрут, в котором комментарии вложены в заявки:
map.resources :auctions do |auctions|
auctions.resources :bids do |bids|
bids.resources :comments
end
end

Но тогда пришлось бы прибегать к различным параметрам, чтобы избежать появления помощника с именем auction_bid_comments_path (это
еще не так плохо, мне встречались куда более уродливые имена).
Вместо этого Джеймис предлагает поступать следующим образом:
map.resources :auctions do |auctions|
auctions.resources :bids
end
map.resources :bids do |bids|
bids.resources :comments
end
map.resources :comments

Обратите внимание, что каждый ресурс (за исключением аукциона)
определен дважды: один раз – в пространстве имен верхнего уровня,
а другой – в своем собственном контексте. Обоснование? Для работы
с отношением родитель-потомок вам в действительности нужны только два уровня. В результате URL становятся короче, а с методамипомощниками проще иметь дело.
auctions_path
auctions_path(1)
auction_bids_path(1)
bid_path(2)
bid_comments_path(3)
comment_path(4)

#
#
#
#
#
#

/auctions
/auctions/1
/auctions/1/bids
/bids/2
/bids/3/comments
/comments/4

Говорит Кортенэ…
Многие из нас не согласны с уважаемым Джеймисом. Хотите устроить потасовку на конференции по Rails? Задайте вопрос: «Кто
считает, что более одного уровня вложенности в маршруте – это
хорошо?»

Настройка REST-совместимых маршрутов

133

Лично я не всегда следую рекомендации Джеймиса в своих проектах,
но обратил внимание на одну вещь, относящуюся к ограничению глубины вложенности ресурсов: в этом случае можно оставлять префиксы
имен на своих местах, а не обрубать их с помощью :name_prefix => nil.
И поверьте мне, префиксы имен очень здорово упростят сопровождение вашего кода в будущем.

Настройка REST-совместимых маршрутов
REST-совместимые маршруты дают группу именованных маршрутов,
заточенных для вызова ряда весьма полезных и общеупотребительных
действий контроллеров – надмножества CRUD, о котором мы уже говорили. Но иногда хочется выполнить добавочную настройку, не отка­
зываясь от преимуществ, которые дает соглашение об именовании
REST-совместимых маршрутов и «таблица умножения», описывающая комбинации именованных маршрутов с методами HTTP-запросов.
На­пример, это было бы полезно при наличии нескольких вариантов
просмотра ресурса, которые можно назвать «показами». Вы не можете
(и не должны) применять действие show более чем для одного такого варианта. Лучше представлять это как разные взгляды на ресурс и создать URL для каждого такого взгляда.

Маршруты к дополнительным действиям
Пусть, например, мы хотим реализовать возможность отзыва заявки.
Основной вложенный маршрут для заявок выглядит так:
map.resources :auctions do |a|
a.resources :bids
end

Мы хотели бы иметь действие retract, которое показывает форму (и, быть
может, выполняет проверки допустимости отзыва). Действие retract –
не то же самое, что destroy; оно – скорее, предтеча destroy. В этом смысле данное действие аналогично действию edit, которое выводит форму
для последующего действия update. Проводя параллель с парой edit/
update, мы хотели бы, чтобы URL выглядел так:
/auctions/3/bids/5/retract

а метод-помощник назывался retract_bid_url. Достигается это путем задания дополнительного маршрута :member для bids, как показано в листинге 4.7.
Листинг 4.7. Добавление маршрута к дополнительному действию
map.resources :auctions do |a|
a.resources :bids, :member => { :retract => :get }
end

134

Глава 4. REST, ресурсы и Rails

Если затем следующим образом добавить ссылку на операцию отзыва
в представление:

то сгенерированный URL будет содержать модификатор /retract. Но
такая ссылка, вероятно, должна выводить форму отзыва, а не выполнять саму процедуру отзыва! Я это говорю, потому что, согласно базовым принципам HTTP, GET-запрос не должен изменять состояние сервера – для этого предназначены POST-запросы. Достаточно ли добавить
параметр :method в вызов link_to?
:post
%>

Не совсем. Напомню, что в листинге 4.7 мы определили маршрут к операции отзыва как :get, потому система маршрутизации не распознает
POST-запрос. Но решение есть – надо лишь определить маршрут так,
чтобы на него отображался любой глагол HTTP:
map.resources :auctions do |a|
a.resources :bids, :member => { :retract => :any }
end

Дополнительные маршруты к наборам
Описанной техникой можно воспользоваться для добавления маршрутов, которые концептуально применимы ко всему набору ресурсов:
map.resources :auctions, :collection => { :terminate => :any }

Этот пример дает метод terminate_auctions_path, который порождает
URL, отображаемый на действие terminate контроллера auctions (пример, пожалуй, несколько странный, но идея в том, что он позволяет
завершить сразу все аукционы).
Таким образом, вы можете, оставаясь в рамках совместимости с REST,
точно настраивать поведение маршрутизации в своем приложении
и включать особые случаи, не переставая рассуждать в терминах ресурсов.

Замечания
При обсуждении REST-совместимой маршрутизации в списке рассылки Rails1, Джош Сассер (Josh Susser) предложил инвертировать синтаксис записи нестандартных действий, чтобы ключом был глагол
HTTP, а значением – массив имен действий:
map.resources :comments,
:member => { :get => :reply,
:post => [:reply, :spawn, :split] }
1

Полностью с обсуждением можно познакомиться по адресу http://www.
ruby-forum.com/topic/75356.

Настройка REST-совместимых маршрутов

135

Среди других причин Джош отметил, что это здорово упростило бы написание так называемых возвратов (post-back), то есть действий контроллера двойного назначения, которые умеют обрабатывать GET и POSTзапросы в одном методе.
Дэвид отозвался негативно. Возразив против возвратов в принципе, он
сказал: «Я начинаю думать, что явное игнорирование [возвратов] в map.
resources – это запроектированная особенность».
Ниже в том же обсуждении, продолжая защищать API, Дэвид добавил:
«Если вы пишете так много дополнительных методов, что повторение
начинает надоедать, следует пересмотреть исходные позиции. Возможно, вы не так уж хорошо следуете REST, как могли бы» (курсив мой).
Ключевой является последняя фраза. Включение дополнительных
действий портит элегантность общего дизайна REST-совместимых
приложений, поскольку уводит в сторону от выявления всех ресурсов,
характерных для вашей предметной области.
Памятуя, что реальные приложения сложнее примеров в справочном
руководстве, посмотрим все же, нельзя ли смоделировать отзывы строго, с использованием ресурсов. Вместо того чтобы включать действие
retract в контроллер BidsController, может быть, стоит ввести отдельный ресурс «отзыв», ассоциированный с заявками, и написать для
его обработки контроллер RetractionController.
map.resources :bids do |bids|
bids.resource :retraction
end

Теперь RetractionController можно сделать ответственным за все операции, касающиеся отзывов, а не мешать эту функциональность с BidsController. Если вдуматься, отзыв заявок – достаточно сложное дело,
которое в конце концов, все равно обросло бы громоздкой логикой. Пожалуй, выделение для него отдельного контроллера можно назвать
надлежащим разделением обязанностей и даже правильным объектно-ориентированным подходом.
Не могу не продолжить рассказ об этом знаменательном обсуждении
в списке рассылки, потому что с ним связан бесценный момент в истории сообщества Rails, укрепивший нашу репутацию как «пристрастной шайки»!
Джош ответил: «Хочу уточнить… Вы считаете, что код, который трудно читать и утомительно писать, – это достоинство? Пожалуй, с позиций сравнения макро- и микрооптимизации я бы не стал с вами спорить, но полагаю, что это спорный способ побудить людей писать правильно. Если совместимость с REST сводится только к этому, то не надо
думать, что синтаксический уксус заставит народ поступать как надо.
Однако если вы хотели сказать, что организация хеша действий в виде
{:action => method, ...} желательна, так как гарантирует, что каждое

136

Глава 4. REST, ресурсы и Rails

действие будет использоваться ровно один раз, то в этом, конечно, есть
смысл» (курсив мой).
Дэвид действительно считал, что менее понятный и более трудоемкий
код в данном конкретном случае является преимуществом, и с энтузиазмом ухватился за термин синтаксический уксус. Спустя примерно
два месяца он поместил в свой блог одно из самых знаменитых рассуждений о концепции (ниже приводится выдержка):
В спорах о проектировании языков и сред уже давно прижился термин «синтаксическая глазурь». Речь идет о превращении идиом в соглашения, о пропаганде единого стиля, поскольку он обладает несомненными достоинствами: красотой, краткостью и простотой использования. Все мы любим синтаксическую глазурь и приветствуем
в ней все: вселяющие ужас пропасти, головокружительные вершины
и кремовую серединку. Именно это делает языки, подобные Ruby, такими сладкими по сравнению с более прямолинейными альтернативами.
Но мы нуждаемся не в одном лишь сахаре. Хороший дизайн не только
поощряет правильное использование, но и препятствует неправильному. Если мы можем украшать какой-то стиль или подход синтаксической глазурью, чтобы поспособствовать его использованию, то
почему бы не сдобрить кое-что синтаксическим уксусом, дабы воспрепятствовать неразумному применению. Это делается реже, но
оттого не становится менее важным…
http://www.loudthinking.com/arc/2006_10.html

Ресурсы, ассоциированные
только с контроллером
Слово «ресурс», будучи существительным, наводит на мысль о таблицах и записях в базе данных. Однако в REST ресурс не обязательно должен один в один отображаться на модель ActiveRecord. Ресурсы – это
высокоуровневые абстракции сущностей, доступных через веб-приложение. Операции базы данных – лишь один из способов сохранять
и извлекать данные, необходимые для генерации представлений ресурсов.
Ресурс в REST необязательно также напрямую отображать на контроллер, по крайней мере теоретически. В ходе обсуждения параметров
:path_prefix и :controller метода map.resources вы видели, что при желании можно предоставлять REST-службы, для которых публичные идентификаторы (URI) вообще не соответствуют именам контроллеров.
А веду я к тому, что иногда возникает необходимость создать набор
маршрутов к ресурсам и связанный с ними контроллер, которые не соответствуют никакой модели в приложении. Нет ничего плохого в пол-

Ресурсы, ассоциированные только с контроллером

137

ном комплекте ресурс/контроллер/модель, где все имена соответствуют друг другу. Но бывают случаи, когда представляемые ресурсы можно инкапсулировать в контроллер, но не в модель.
Для аукционного приложения примером может служить контроллер
сеансов. Предположим, что в файле routes.rb есть такая строка:
map.resource :session

Она отображает URL /session на контроллер SessionController как одиночный ресурс, тем не менее модели Session не существует (кстати, ресурс правильно определен как одиночный, потому что с точки зрения
пользователя существует только один сеанс).
Зачем идти по пути REST при аутентификации? Немного подумав, вы
осознаете, что сеансы пользователей можно создавать и уничтожать.
Сеанс создается, когда пользователь регистрируется, и уничтожается,
когда он выходит. Значит принятый в Rails REST-совместимый подход
сопоставления действия и представления new с действием create годится! Форму регистрации пользователя можно рассматривать как форму
создания сеанса, находящуюся в файле шаблона session/new.rhtml (листинг 4.8).
Листинг 4.8. REST-совместимая форма регистрации
Регистрация
session_path do |f| %>
Имя:
Пароль:

Когда эта форма отправляется, данные обрабатываются методом create
контроллера сеансов, который показан в листинге 4.9.
Листинг 4.9. REST-совместимое действие регистрации
def create
@user = User.find_by_login(params[:user][:login])
if @user and @user.authorize(params[:user][:password])
flash[:notice] = "Добро пожаловать, #{@user.first_name}!"
redirect_to home_url
else
flash[:notice] = "Неправильное имя или пароль."
redirect_to :action => "new"
end
end

Это действие ничего не пишет в базу данных, но имя create адекватно,
поскольку оно создает сеанс. Более того, если позже вы решите, что сеансы следует-таки хранить в базе данных, то уже будет готово подходящее место для обработки.

138

Глава 4. REST, ресурсы и Rails

Надо ясно понимать, что CRUD как философия именования действий
и CRUD как набор фактических операций с базой данных иногда могут
существовать независимо друг от друга и что средства обработки ресурсов в Rails полезно ассоциировать с контроллером, для которого нет
соответствующей модели. Создание сеанса – не самый убедительный
пример REST-совместимой практики, поскольку REST требует, чтобы
передача представлений ресурсов осуществлялась без сохранения состояния. Но это неплохая иллюстрация того, как и почему принимаются проектные решения, касающиеся маршрутов и ресурсов, но не
затрагивающие приложение в целом.
Присваивать действиям имена, согласованные с CRUD, – в общем случае правильная идея. Если вы выполняете много операций создания
и удаления, то регистрацию пользователя проще представлять себе как
создание сеанса, чем изобретать для нее отдельную семантическую категорию. Вместо того чтобы вводить новую концепцию «пользователь
регистрируется», просто считайте, что речь идет о частном случае старой концепции «создается сеанс».

Различные представления ресурсов
Одна из заповедей REST состоит в том, что компоненты построенной на
принципах REST системы обмениваются между собой представлениями ресурсов. Различие между ресурсами и их представлениями весьма
существенно.
Как клиент или потребитель служб REST, вы получаете от сервера не
сам ресурс, а его представление. Вы также передаете представления
серверу. Так, операция отправки формы посылает серверу представление ресурса, а вместе с ним запрос, например PUT, диктующий, что это
представление следует использовать как основу для обновления ресурса. Представления можно назвать единой валютой в сфере управления
ресурсами.

Метод respond_to
В Rails возможность возвращать различные представления основана
на использовании метода respond_to в контроллере, который, как вы
видели, позволяет формировать разные ответы в зависимости от желания пользователя. Кроме того, при создании маршрутов к ресурсам вы
автоматически получаете механизм распознавания URL, оканчивающихся точкой и параметром :format.
Предположим, например, что в файле маршрутов есть маршрут map.
resources :auctions, а логика контроллера AuctionsController выглядит
примерно так:

Набор действий в Rails для REST

139

def index
@auctions = Auction.find(:all)
respond_to do |format|
format.html
format.xml { render :xml => @auctions.to_xml }
end
end

Теперь появилась возможность соединиться с таким URL: http://localhost:3000/auctions.xml.
Система маршрутизации обеспечит выполнение действия index. Она
также распознает суффикс .xml в конце маршрута и пойдет по ветви
respond_to, которая возвращает XML-представление.
Разумеется, все это относится к этапу распознавания URL. А как быть,
если вы хотите сгенерировать URL, заканчивающийся суффиксом .xml?

Форматированные именованные маршруты
Система маршрутизации дает также варианты именованных маршрутов
к ресурсу с модификатором .:format. Пусть нужно получить ссылку на
XML-представление ресурса. Этого можно достичь с помощью варианта
REST-совместимого именованного маршрута с префиксом formatted_:

В результате генерируется следующая HTML-разметка:
XML-версия этого аукциона

При щелчке по этой ссылке срабатывает относящаяся к XML ветвь
блока respond_to в действии show контроллера auctions. Возвращаемая
XML-разметка может выглядеть в броузере не очень эстетично, но сам
именованный маршрут к вашим услугам.
Круг замкнулся: вы можете генерировать URL, соответствующие конкретному типу ответу, и обрабатывать запросы на получение различных типов ответа с помощью метода respond_to. А можно вместо этого
указать тип желаемого ответа с помощью заголовка Accept в запросе.
Таким образом, система маршрутизации и надстроенные над ней средст­
ва построения маршрутов к ресурсам дают набор мощных и лаконичных инструментов для дифференциации запросов и, следовательно,
для генерирования различных представлений.

Набор действий в Rails для REST
Встроенные в Rails средства поддержки REST сводятся, в конечном
итоге, к именованным маршрутам и действиям контроллеров, на которые они указывают. Чем больше вы работаете со средствами Rails для

140

Глава 4. REST, ресурсы и Rails

REST, тем лучше понимаете назначение каждого из семи REST-совместимых действий. Разумеется, в разных контроллерах (и приложениях) они используются по-разному. Тем не менее, поскольку количество действий конечно, а их роли довольно четко определены, у каждого
действия есть ряд более-менее постоянных свойств и присущих только
ему характеристик.
Ниже мы рассмотрим каждое из семи действий, приведя примеры
и комментарии. Мы уже встречались с ними ранее, особенно в главе 2
«Работа с контроллерами», но сейчас вы познакомитесь с предысторией, почувствуете характеристические особенности каждого действия
и поймете, какие вопросы возникают при выборе любого из них.

index
Обычно действие index дает представление множественной формы ресурса (или набора). Как правило, представление, порождаемое этим
действием, общедоступно и достаточно обще. Действие index предъявляет миру наиболее нейтральное представление.
Типичное действие index выглядит примерно так:
class AuctionsController < ApplicationController
def index
@auctions = Auction.find(:all)
end
...
end

Шаблон представления отображает общедоступные сведения о каждом
аукционе со ссылками на детальную информацию о самом аукционе
и профиле продавца.
Хотя index лучше всего считать открытым действием, иногда возникают ситуации, когда необходимо отобразить представление набора, недоступное широкой публике. Например, у пользователя должна быть
возможность посмотреть список всех своих заявок, но при этом видеть
чужие списки запрещено.
В этом случае наилучшая стратегия состоит в том, чтобы «закрыть
дверь» как можно позже. Вам на помощь придет REST-совместимая
маршрутизация.
Пусть нужно сделать так, чтобы каждый пользователь мог видеть историю своих заявок. Можно было бы профильтровать результат работы действия index контроллера bids с учетом текущего пользователя
(@user). Проблема, однако, в том, что тем самым мы исключаем использование этого действия для более широкой аудитории. Что если нужно

Набор действий в Rails для REST

141

получить открытое представление текущего набора заявок с наивысшей ценой предложения? А, быть может, даже переадресовать на представление index аукционов? Идея в том, чтобы сохранять максимально
открытый доступ настолько долго, насколько это возможно.
Сделать это можно двумя способами. Один из них – проверить, зарегистрировался ли пользователь, и соответственно решить, что показывать.
Но такой подход здесь не пройдет. Во-первых, зарегистрировавшийся
пользователь может захотеть увидеть более общедоступное представление. Во-вторых, чем больше зависимостей от состояния на стороне сервера мы сможем устранить или консолидировать, тем лучше.
Поэтому будем рассматривать два списка заявок не как открытую и закрытую версию одного и того же ресурса, а как два разных ресурса. Это
различие можно инкапсулировать прямо в маршрутах:
map.resources :auctions do |auctions|
auctions.resources :bids, :collection => { :manage => :get }
end

Теперь контроллер заявок bids можно организовать так, что доступ будет изящно разбит на уровни, задействуя при необходимости фильтры
и устранив ветвление в самих действиях:
class BidsController < ApplicationController
before_filter :load_auction
before_filter :check_authorization, :only => :manage
def index
@bids = Bid.find(:all)
end
def manage
@bids = @auction.bids
end
...
protected
def load_auction
@auction = Auction.find(params[:auction_id])
end
def check_authorization
@auction.authorized?(current_user)
end
end

Мы четко разделили ресурсы /bids и /bids/manage, а также роли, которые они играют в приложении.

142

Глава 4. REST, ресурсы и Rails

Говорит Кортенэ…
Некоторые разработчики полагают, что использование фильтров для загрузки данных – преступление против всего хорошего
и чистого. Если ваш коллега или начальник пребывает в этом убеждении, включите поиск в действие, устроенное примерно так:
def show
@auction = Auction.find(params[:id])
unless auction.authorized?(current_user)
... # доступ запрещен
end
end

Альтернативный способ – добавить метод в класс User, поскольку
за авторизацию должен отвечать объект, представляющий пользователя:
class User < ActiveRecord::Base
def find_authorized_auction(auction_id)
auction = Auction.find(auction_id)
return auction.authorized?(self) && auction
end
end

И вызовите его из действия контроллера AuctionController:
def show
@auction = current_user.find_authorized_auction
(params[:id]) else
raise ActiveRecord::RecordNotFound
end
end

Можно даже добавить метод в модель Auction, поскольку именно
эта модель управляет доступом к данным.
def self.find_authorized(id, user)
auction = find(id)
return auction.authorized?(user) && auction
end

С точки зрения именованных маршрутов, мы теперь имеем ресурсы
bids_url и manage_bids_url. Таким образом, мы сохранили общедоступную, лишенную состояния грань ресурса /bids и инкапсулировали зависящее от состояния поведение в отдельный подресурс /bids/manage.
Не пугайтесь, если такой образ мыслей с первого раза не показался вам
естественным, – это нормально при освоении REST.
Если бы я занимал в отношении REST догматическую позицию, то счел
бы странным и даже отвратительным включать в обсуждение касающихся REST приемов саму идею инкапсуляции поведения, зависяще-

Набор действий в Rails для REST

143

го от состояния, поскольку REST-совместимые запросы по определению не должны зависеть от состояния. Однако мы показали, что имеющиеся в Rails средства поддержки REST могут, так сказать, постепенно
убавляться в ситуациях, когда необходимо отойти от строго следующего принципам REST интерфейса.

show
REST-совместимое действие show относится к одиночному ресурсу.
Обычно оно интерпретируется как представление информации об одном объекте, одном элементе набора. Как и index, действие show активируется при получении GET-запроса.
Типичное, можно сказать классическое, действие show выглядит примерно так:
class AuctionController < ApplicationController
def show
@auction = Auction.find(params[:id])
end
end

Конечно, действие show может полагаться на фильтры before как на
способ, позволяющий не загружать показываемый ресурс явно в самом
действии. Возможно, вам необходимо отличать (скажем, исходя из
маршрута) общедоступные профили от профиля текущего пользователя, который может допускать модификацию и содержать дополнительную информацию.
Как и в случае действия index, полезно делать действие show максимально
открытым, а административные и привилегированные представления
переносить либо в отдельный контроллер, либо в отдельное действие.

destroy
Доступ к действиям destroy обычно предполагает наличие административных привилегий, хотя, конечно, все зависит от того, что именно вы
удаляете. Для защиты действия destroy можно написать код, представленный в листинге 4.10.
Листинг 4.10. Защита действия destroy
class UsersController < ApplicationController
before_filter :admin_required, :only => :destroy

Типичное действие destroy могло бы выглядеть следующим образом
(предполагая, что пользователь @user уже загружен в фильтре before):
def destroy
@user.destroy
flash[:notice] = "Пользователь удален!"
redirect_to users_url
end

144

Глава 4. REST, ресурсы и Rails

Такой подход можно отразить в простом административном интерфейсе:
Пользователи

:delete) if
current_user.admin? %>

Ссылка delete присутствует, только если текущий пользователь является администратором.
На самом деле самое интересное в последовательности REST-совместимого удаления в Rails происходит в представлении, которое содержит ссылки to на действие. Ниже приведена HTML-разметка одной итерации цикла. Предупреждение: она длиннее, чем можно было бы ожидать.
Emma Knight Peel
Удалить)

Почему так много кода – да еще на JavaScript! – для двух маленьких
ссылочек? Первая ссылка обрабатывается быстро – она просто ведет
к действию show для данного пользователя. А причина, по которой вторая ссылка получилась такой длинной, состоит в том, что отправка методом DELETE потенциально опасна. Rails хочет максимально затруднить подлог таких ссылок и сделать все, чтобы эти действия нельзя
было выполнить случайно, например в результате обхода вашего сайта
пауком или роботом. Поэтому, когда вы задаете метод DELETE, в HTMLдокумент встраивается целый JavaScript-сценарий, который обертывает ссылку в форму. Поскольку роботы не отправляют форм, это обеспечивает коду дополнительный уровень защиты.

new и create
Вы уже видели, что в Rails для REST действия new и create идут рука об
руку. «Новый ресурс» – это просто виртуальная сущность, которую
еще нужно создать. Соответственно, действие new обычно выводит форму, а действие create создает новую запись исходя из данных формы.
Пусть требуется, чтобы пользователь мог создавать (то есть открывать)
аукцион. Тогда вам понадобится:
1. Действие new для отображения формы.
2. Действие create, которое создаст новый объект Auction из данных,
введенных в форму, и затем выведет представление (результат действия show) этого аукциона.

Набор действий в Rails для REST

145

У действия new работы немного. На самом деле ему вообще ничего не
надо делать. Как и всякое пустое действие, его можно опустить. При
этом Rails все равно поймет, что вы хотите выполнить рендеринг представления new.html.erb.
Шаблон представления new.html.erb мог бы выглядеть, как показано
в листинге 4.11. Обратите внимание, что некоторые поля ввода отнесены к пространству имен :item (благодаря методу-помощнику fields_for),
а другие – к пространству имен :auction (из-за метода-помощника form_
for). Объясняется это тем, что лот и аукцион в действительности создаются в тандеме.
Листинг 4.11. Форма для создания нового аукциона
Создать новый аукцион

auctions_path do |f| %>

Описание лота:
Производитель лота:
Материал лота:
Год выпуска лота:

Резервировать:
Шаг торгов:
Начальная цена:
Время окончания:

Действие формы выражено с помощью именованного маршрута auctions
в сочетании с тем фактом, что для формы автоматически генерируется
POST-запрос.
После отправки заполненной формы наступает время главного события: действия create. В отличие от new, у этого действия есть работа:
def create
@auction = current_user.auctions.build(params[:auction])
@item = @auction.build_item(params[:item])
if @auction.save
flash[:notice] = "Аукцион открыт!"
redirect_to auction_url(@auction)
else
render :action => "new"
end
end

Наличие пространств имен "auction" и "item" для полей ввода позволяет нам воспользоваться обоими с помощью хеша params, чтобы создать
новый объект Auction из ассоциации текущего пользователя с аукционами и одновременно объект Item методом build_item. Это удобный спо-

146

Глава 4. REST, ресурсы и Rails

соб работы сразу с двумя ассоциированными объектами. Если по какой-то причине метод @auction.save завершится с ошибкой, ассоциированный объект не будет создан, поэтому беспокоиться об очистке нет
нужды.
Если же операция сохранения завершается успешно, будут созданы
и аукцион, и лот.

edit и update
Подобно операциям new и create, операции edit и update выступают
в паре: edit отображает форму, а update обрабатывает введенные в нее
данные.
Формы для редактирования и ввода новой записи очень похожи (можно поместить большую часть кода в частичный шаблон и включить его
в обе формы; оставляем это в качестве упражнения для читателя). Вот
как мог бы выглядеть шаблон edit.html.erb для редактирования лота:
Редактировать лот
item_path(@item),
:html => { :method => :put } do |item| %>
Описание:
Производитель:
Материал:
Год выпуска:

Основное отличие от формы для задания нового лота (листинг 4.11) заключается в используемом именованном маршруте и в том, что для
действия update следует задать метод запроса PUT. Это послужит диспетчеру указанием на необходимость вызвать метод обновления.

Заключение
В этой главе мы рассмотрели непростую тему, касающуюся применения принципов REST к проектированию приложений Rails, в основном с точки зрения системы маршрутизации и действий контроллера.
Мы узнали, что основой Rails для REST является метод map.resources
в файле маршрутов, а также о многочисленных параметрах, позволяющих структурировать приложение именно так, как должно. Как выяснилось, в ряде мест Дэвид и команда разработчиков ядра Rails разлили
синтаксический уксус, чтобы помешать нам пойти по ложному пути.
Один из сложных аспектов написания и сопровождения серьезных
приложений Rails – понимание системы маршрутизации и умение находить ошибки, которые вы, без сомнения, будете допускать в ходе
повседневной работы. Эта тема настолько важна для любого разработчика на платформе Rails, что мы посвятили ей целую главу.

5
Размышления о маршрутизации в Rails
Вы находитесь в лабиринте, состоящем из похожих
друг на друга извилистых коридоров.
Adventure (компьютерная игра,
популярная в конце 1970-х годов)

В данном контексте «размышления» означает не «раздумья», а исследование, тестирование и поиск причин ошибок. В системе маршрутизации
есть немало мест, над которыми можно поразмыслить подобным образом,
а также удобные подключаемые модули, которые могут помочь в этом.
Мы не собираемся рассматривать все существующие приемы и подключаемые модули, но познакомим вас с некоторыми важными инструментами, позволяющими убедиться, что ваши маршруты делают именно то,
на что вы рассчитывали, а в противном случае понять, в чем ошибка.
Мы также заглянем в исходный код системы маршрутизации, в том
числе и средств, ориентированных на поддержку REST-совместимой
маршрутизации.

Исследование маршрутов
в консоли приложения
Описав полный круг, мы возвращаемся к тому, с чего начали эту книгу, когда говорили о диспетчере, – на этот раз мы воспользуемся консолью приложения для исследования внутренних механизмов работы

148

Глава 5. Размышления о маршрутизации в Rails

уже знакомой нам системы маршрутизации. Это поможет в поиске
причин ошибок и позволит лучше понять саму систему.

Распечатка маршрутов
Начнем с перечисления всех имеющихся маршрутов. Для этого необходимо получить текущий объект RouteSet:
$ ruby script/console
Loading development environment.
>> rs = ActionController::Routing::Routes

В ответ будет выведено довольно много информации – распечатка всех
определенных в системе маршрутов. Но можно представить эту распечатку в более удобном для восприятия виде:
>> puts rs.routes

В результате перечень маршрутов будет выведен в виде таблицы:
GET
GET
POST
POST
GET
GET
GET
GET

/bids/
{:controller=>"bids", :action=>"index"}
/bids.:format/
{:controller=>"bids", :action=>"index"}
/bids/
{:controller=>"bids", :action=>"create"}
/bids.:format/
{:controller=>"bids", :action=>"create"}
/bids/new/
{:controller=>"bids", :action=>"new"}
/bids/new.:format/ {:controller=>"bids", :action=>"new"}
/bids/:id;edit/
{:controller=>"bids", :action=>"edit"}
/bids/:id.:format;edit {:controller=>"bids", :action=>"edit"}

# и т. д.

Возможно, столько информации вам не нужно, но представление ее
в подобном виде помогает разобраться. Вы получаете зримое подтверждение факта, что у каждого маршрута есть метод запроса, образец URL
и параметры, определяющие пару контроллер/действие.
В таком же формате можно получить и список именованных маршрутов. Но в этом случае имеет смысл немного подправить формат. При переборе вы получаете имя и назначение каждого маршрута. Данную информацию можно использовать для форматирования в виде таблицы:
rs.named_routes.each {|name, r| printf("%-30s %s\n", name, r) }; nil

nil в конце необходимо, чтобы программа irb не выводила реальное
возвращаемое значение при каждом обращении к each, поскольку это
выдвинуло бы интересную информацию за пределы экрана.
Результат выглядит примерно так (слегка «причесан» для представления на печатной странице):
history

ANY

new_us
GET
new_auction GET

/history/:id/ {:controller=>"auctions",
:action=>"history"}
/users/new/
{:controller=>"users", :action=>"new"}
/auctions/new/ {:controller=>"auctions",

Исследование маршрутов в консоли приложения

149

:action=>"new"}
# и т. д.

Идея в том, что можно вывести на консоль разнообразную информацию о маршрутизации, отформатировав ее по собственному разумению. Ну а что насчет «необработанной» информации? Исходная распечатка также содержала несколько важных элементов:
#"bids", :action=>"create"},
@to_s="POST /bids.:format/ {:controller=>\"bids\",
:action=>\"create\"}",
@significant_keys=[:format, :controller, :action],
@conditions={:method=>:post},
@segments=[#,
#,
#,
#,
#]>

Анатомия объекта Route
Самый лучший способ понять, что здесь происходит, – взглянуть на
представление маршрута в формате YAML (Yet Another Markup
Language). Ниже приведен результат работы операции to_yaml, снабженный комментариями. Объем информации велик, но, изучив ее, вы
узнаете много нового. Можно сказать, рентгеном просветите способ
конструирования маршрута.
# Все это – объект Route
-- !ruby/object:actionController::Routing::Route
# Этот маршрут распознает только PUT-запросы.
conditions:
:method: :put
# Главная цепочка событий в процедуре распознавания и точки для подключения
# механизма сопоставления в процедуре генерации.
requirements:
:controller: bids
:action: update
# Сегменты. Это формальное определение строки-образца.
# Учитывается все, включая разделители (символы косой черты).

150

Глава 5. Размышления о маршрутизации в Rails
# Отметим, что каждый сегмент – это экземпляр того или иного класса:
# DividerSegment, StaticSegment или DynamicSegment.
# Читая дальше, вы сможете реконструировать возможные значения образца.
# Обратите внимание на автоматически вставленное поле regexp, которое
# ограничивает множество допустимых значений сегмента :id.
segments:
- !ruby/object:actionController::Routing::DividerSegment
is_optional: false
raw: true
value: /
- !ruby/object:actionController::Routing::StaticSegment
is_optional: false
value: auctions
- !ruby/object:actionController::Routing::DividerSegment
is_optional: false
raw: true
value: /
- !ruby/object:actionController::Routing::DynamicSegment
is_optional: false
key: :auction_id
- !ruby/object:actionController::Routing::DividerSegment
is_optional: false
raw: true
value: /
- !ruby/object:actionController::Routing::StaticSegment
is_optional: false
value: bids
- !ruby/object:actionController::Routing::DividerSegment
is_optional: false
raw: true
value: /
- !ruby/object:actionController::Routing::DynamicSegment
is_optional: false
key: :id
regexp: !ruby/regexp /[^\/;.,?]+/
- !ruby/object:actionController::Routing::DividerSegment
is_optional: true
raw: true
value: /
significant_keys:
- :auction_id
- :id
- :controller
- :action
# (Это должно находиться в одной строке; разбито на две только для
# удобства форматирования.)
to_s: PUT /auctions/:auction_id/bids/:id/
{:controller=>"bids" :action=>"update"}

Исследование маршрутов в консоли приложения

151

Хранение сегментов в виде набора позволяет системе маршрутизации
выполнять распознавание и генерацию, поскольку сегменты можно перебирать как для сопоставления с поступившим URL, так и для вывода
URL (в последнем случае сегменты используются в качестве трафарета).
Конечно, об устройстве механизма работы с маршрутами можно узнать
еще больше, заглянув в исходный код. Очень далеко мы заходить не
будем, но познакомимся с файлами routing.rb и resources.rb в дереве
ActionController. Там вы найдете определения классов Routing, RouteSet,
различных классов Segment и многое другое. Если хотите подробнее узнать, как это все работает, обратитесь к серии статей в блоге Джеймиса
Бака, одного из членов команды разработчиков ядра Rails1.
Но этим использование консоли не ограничивается – вы можете непосредственно выполнять операции распознавания и генерации URL.

Распознавание и генерация с консоли
Чтобы вручную выполнить с консоли распознавание и генерацию, сначала зададим в качестве контекста текущего сеанса объект RouteSet (если вы никогда не встречались с таким приемом, предоставляем случай
познакомиться с интересным применением IRB):
$ ./script/console
Loading development environment.
>> irb ActionController::Routing::Routes
>>

Вызывая команду irb в текущем сеансе работы с IRB, мы говорим, что
объектом по умолчанию – self – будет выступать набор маршрутов. Это
позволит меньше печатать в дальнейшем при вводе команд.
Чтобы узнать, какой маршрут генерируется при заданных параметрах, достаточно передать эти параметры методу generate. Ниже приведено несколько примеров с комментариями.
Начнем с вложенных маршрутов к ресурсам-заявкам. Действие create
генерирует URL набора; поле :id в нем отсутствует. Но для организации вложенности имеется поле :auction_id.
>> generate(:controller => "bids", :auction_id => 3, :action =>
"create")
=> "/auctions/3/bids"

Далее следует два маршрута к заявкам bids, вложенных в users. В обоих случаях (retract и manage) указания подходящего имени действия
достаточно для включения в путь URL дополнительного сегмента.
>> generate(:controller => "bids", :user_id => 3, :id => 4, :action =>
"retract")
1

http://weblog.jamisbuck.org/2006/10/4/under-the-hood-route-recognition-in-rails.

152

Глава 5. Размышления о маршрутизации в Rails
=> "/users/3/bids/4/retract"
>> generate(:controller => "bids", :user_id => 3, :action => "manage")
=> "/users/3/bids/manage"

Не забыли про действие history контроллера auctions, которое выводит
историю заявок? Вот как сгенерировать URL для него:
>> generate(:controller => "auctions", :action => "history", :id => 3)
=> "/history/3"

В следующих двух примерах иллюстрируется маршрут item_year, которому в качестве параметра нужно передать год, записанный четырьмя
цифрами. Отметим, что генерация выполняется неправильно, если год
не соответствует образцу, – значение года добавляется в виде строки
запроса, а не включается в URL в виде сегмента пути:
>> generate(:controller => "items", :action => "item_year", :year =>
1939)
=> "/item_year/1939"
>> generate(:controller => "items", :action => "item_year", :year =>
19393)
=> "/items/item_year?year=19393"

Можно поступить и наоборот, то есть начать с путей и посмотреть, как
система распознавания маршрутов преобразует их в контроллер, действие и параметры.
Вот что происходит для маршрута верхнего уровня, определенного
в файле routes.rb:
>> recognize_path("/")
=> {:controller=>"auctions", :action=>"index"}

Аналогичный результат получается для маршрута к ресурсу auctions,
который записан во множественном числе и отправлен методом GET:
>> recognize_path("/auctions", :method => :get)
=> {:controller=>"auctions", :action=>"index"}

Для запроса методом POST результат будет иным – он маршрутизируется к действию create:
>> recognize_path("/auctions", :method => :post)
=> {:controller=>"auctions", :action=>"create"}

Та же логика применима к множественному POST-запросу во вложенном маршруте:
>> recognize_path("/auctions/3/bids", :method => :post)
=> {:controller=>"bids", :action=>"create", :auction_id=>"3"}

Нестандартные действия тоже распознаются и преобразуются в нужный контроллер, действие и параметры:
>> recognize_path("/users/3/bids/1/retract", :method => :get)
=> {:controller=>"bids", :user_id=>"3", :action=>"retract", :id=>"1"}

Тестирование маршрутов

153

Маршрут к истории заявок ведет на контроллер auctions:
>> recognize_path("/history/3")
=> {:controller=>"auctions", :action=>"history", :id=>"3"}

Маршрут item_year распознает только пути с четырехзначными числами в позиции :year. Во втором из показанных ниже примеров система
сообщает об ошибке – подходящего маршрута не существует.
>> recognize_path("/item_year/1939")
=> {:controller=>"items", :action=>"item_year", :year=>"1939"}
>> recognize_path("/item_year/19393")
ActionController::RoutingError: no route found to match
"/item_year/19393" with {}

Консоль и именованные маршруты
С консоли можно выполнять и именованные маршруты. Проще всего
это сделать, включив модуль ActionController::UrlWriter и задав произвольное значение для принимаемого по умолчанию хоста (просто чтобы подавить ошибки):
>>
=>
>>
=>

include ActionController::UrlWriter
Object
default_url_options[:host] = "example.com"
"example.com"

Теперь можно вызвать именованный маршрут и посмотреть, что вернет система, то есть какой URL будет сгенерирован:
>>
=>
>>
=>
>>
=>

auction_url(1)
"http://example.com/auctions/1"
formatted_auction_url(1,"xml")
"http://example.com/auctions/1.xml"
formatted_auctions_url("xml")
"http://example.com/auctions.xml"

Как всегда, консоль приложения помогает и учиться, и отлаживать
программу. Если в одном окне открыть файл routes.rb, а в другом консоль, то вы сможете очень быстро разобраться во взаимосвязи механизмов распознавания и генерации маршрутов. К сожалению, консоль не
умеет автоматически перезагружать таблицу маршрутов. Даже метод
reload!, похоже, не заставляет ее перечитать файл.
Для обучения и быстрого тестирования консоль хороша, но существуют инструменты для более систематического подхода к тестированию
маршрутов.

Тестирование маршрутов
Система предоставляет следующие средства для тестирования маршрутов:

154

Глава 5. Размышления о маршрутизации в Rails

• assert_generates
• assert_recognizes
• assert_routing
Третий метод – assert_routing – представляет собой комбинацию первых двух. Вы передаете ему путь и параметры, а он проверяет, чтобы
в результате распознавания пути эти параметры действительно получали указанные значения, и наоборот – чтобы из указанных параметров генерировался заданный путь. Тема тестирования и задания маршрутов подробно рассматривается в главах 17 «Тестирование» и 18 «RSpec
on Rails».
Вы сами решаете, сколько и каких тестов написать для своего приложения. В идеале комплект тестов должен включать по меньшей мере,
все комбинации, встречающиеся в приложении. Если хотите посмотреть на достаточно полный набор тестов маршрутизации, загляните
в файл routing.rb в подкаталоге test установленной библиотеки ActionPack. При последнем подсчете в нем была 1881 строка. Эти строки предназначены для тестирования самой среды, так что от вас не требуется
(и не рекомендуется!) дублировать тесты в своем приложении. Однако
их изучение (как, впрочем, и других файлов для тестирования Rails)
может навести на полезные мысли и уж точно послужит иллюстрацией
процедуры разработки, управляемой тестами.
Замечание о синтаксисе аргументов
Не забывайте действующее в Ruby правило, касающееся исполь­
зования хешей в списках аргументов: если последний аргумент
в списке – хеш, то фигурные скобки можно опускать.
Поэтому так писать можно:
assert_generates(user_retract_bid_path(3,1),
:controller => "bids",
:action => "retract",
:id => "1", :user_id => "3")

Если хеш встречается не в последней позиции, то фигурные скобки
обязательны. Следовательно, так писать должно:
assert_recognizes({:controller => "auctions",
:action => "show",
:id => auction.id.to_s },
auction_path(auction))

Здесь последним аргументом является auction_path(auction), поэтому фигурные скобки вокруг хеша необходимы.
Если вы получаете загадочные сообщения о синтаксических ошибках в списке аргументов, проверьте, не нарушено ли это правило.

Подключаемый модуль Routing Navigator

155

Подключаемый модуль Routing Navigator
Рик Олсон (Rick Olson), один из разработчиков ядра Rails, написал
подключаемый модуль Routing Navigator, который позволяет прямо
в броузере получить ту же информацию, что при исследовании маршрутов с консоли, только еще лучше.
Для установки модуля Routing Navigator, находясь в каталоге верхнего уровня приложения Rails, выполните следующую команду:
./script/plugin install
http://svn.techno-weenie.net/projects/plugins/routing_navigator/

Теперь нужно сообщить одному или нескольким контроллерам, что
они должны показывать искомую информацию о маршрутизации. Например, можно добавить такую строку:
routing_navigator :on

в начало определения класса контроллера в файле auction_controller.
rb (куда вы поместили фильтры before и другие методы класса). Разумеется, это следует делать только на этапе разработки – оставлять информацию о маршрутизации в промышленно эксплуатируемом приложении не стоит.
При щелчке по кнопке Recognize (Распознать) или Generate (Генерировать) вы увидите поля, в которые можно ввести путь (в случае распознавания) или параметры (в случае генерации), а затем выполнить соответствующую операцию. Идея та же, что при работе с консолью, но
оформление более элегантное.
Еще одна кнопка называется Routing Navigator. Щелкнув по ней, вы попадете на новую страницу со списком всех маршрутов (как обычных,
так и именованных), которые определены в вашем приложении. Над
списком маршрутов расположены уже описанные выше кнопки, позволяющие ввести путь или параметры и выполнить распознавание или
генерацию.
Список всех имеющихся маршрутов может оказаться весьма длинным.
Но его можно отфильтровать, воспользовавшись еще одним полем – YAML
to filter routes by requirements (YAML для фильтрации маршрутов по требованию). Например, если ввести в него строку controller: bids и щелкнуть
по кнопке Filter, то в нижней части появится список маршрутов, которые относятся к контроллеру bids.
Модуль Routing Navigator – это великолепный инструмент для отладки, поэтому имеет смысл потратить некоторое время на его изучение.

156

Глава 5. Размышления о маршрутизации в Rails

Заключение
Вот и подошло к концу наше путешествие в мир маршрутизации Rails,
как с поддержкой REST, так и без оной. Разрабатывая приложения
для Rails, вы, конечно, выберете наиболее приемлемые для себя идиомы и приемы работы; а, если понадобятся примеры, к вашим услугам
огромный массив уже написанного кода. Если не забывать о фундаментальных принципах, то умение будет возрастать, что не замедлит сказаться на элегантности и логичности ваших программ.
Счастливо выбрать маршрут!

6
Работа с ActiveRecord
Объект, обертывающий строку таблицы или представления
базы данных, инкапсулирует доступ к базе и добавляет
к данным логику предметной области.
Мартин Фаулер,
«Архитектура корпоративных программных приложений»

Паттерн ActiveRecord, выявленный Мартином Фаулером в основополагающей книге Patterns of Enterprise Architecture1, отображает один
класс предметной области на одну таблицу базы данных, а один экземп­
ляр класса – на одну строку таблицы. Хотя этот простой подход применим и не во всех случаях, он обеспечивает удобную среду для доступа
к базе данных и сохранения в ней объектов.
Среда ActiveRecord в Rails включает механизмы для представления моделей и их взаимосвязей, операций CRUD (Create, Read, Update, Delete),
сложных поисков, контроля данных, обратных вызовов и многого другого. Она опирается на принцип «примата соглашения над конфигурацией», поэтому проще всего ее применять, когда уже на этапе создания
схемы новой базы данных вы следуете определенным соглашениям.
Однако ActiveRecord предоставляет и средства конфигурирования, позволяющие адаптировать его к унаследованным базам данных, в которых соглашения Rails не применялись.
1

Мартин Фаулер «Архитектура корпоративных программных приложений»,
Вильямс, 2007 год.

158

Глава 6. Работа с ActiveRecord

В основном докладе на конференции, посвященной рождению Rails,
в 2006 году, Мартин Фаулер сказал, что в Ruby on Rails паттерн Active
Record внедрен настолько глубоко, насколько никто не мог и предполагать. На этом примере показано, чего можно добиться, если всецело
посвятить себя достижению идеала, в качестве которого в Rails выступает простота.

Основы
Для полноты начнем с изложения самых основ работы ActiveRecord.
Первое, что нужно сделать при создании нового класса модели, – объявить его как подкласс ActiveRecord::Base, применив синтаксис расширения Ruby:
class Client < ActiveRecord::Base
end

По принятому в ActiveRecord соглашению класс Client отображается
на таблицу clients. О том, как Rails понимает, что такое множественное число, см. раздел «Приведение к множественному числу» ниже.
По тому же соглашению, ActiveRecord ожидает, что первичным ключом таблицы будет колонка с именем id. Она должна иметь целочисленный тип, а сервер должен автоматически инкрементировать ключ
при создании новой записи. Отметим, что в самом классе нет никаких
упоминаний об имени таблицы, а также об именах и типах данных колонок.
Каждый экземпляр класса ActiveRecord обеспечивает доступ к данным одной строки соответствующей таблицы в объектно-ориентированном стиле. Колонки строки представляются в виде атрибутов объекта; при этом применяются простейшие преобразования типов (то
есть типу varchar соответствует строка Ruby, типам даты/времени –
даты Ruby и т. д.). Проверка наличия значения по умолчанию не производится. Типы атрибутов выводятся из определения колонок в таблице, ассоциированной с классом. Добавление, удаление и изменение
самих атрибутов или их типов реализуется изменением описания таблицы в схеме базы данных.
Если сервер Rails запущен в режиме разработки, то изменения в схеме
базы данных отражаются на объектах ActiveRecord немедленно, и это
видно в веб-броузере. Если же изменения внесены в схему в режиме
работы с консолью Rails, то они автоматически не подхватываются, но
это можно сделать вручную, набрав на консоли команду reload!.
Путь Rails состоит в том, чтобы генерировать, а не писать трафаретный
код. Поэтому вам почти никогда не придется ни создавать файл для
своего класса модели, ни вводить его объявление. Гораздо проще воспользоваться для этой цели встроенным в Rails генератором моделей.

159

Основы

Например, позволим генератору моделей создать класс Client и по­
смотрим, какие в результате появятся файлы:
$ script/generate model client
exists app/models/
exists test/unit/
exists test/fixtures/
create app/models/client.rb
create test/unit/client_test.rb
create test/fixtures/clients.yml
exists db/migrate
create db/migrate/002_create_clients.rb

Файл, в котором находится новый класс модели, называется client.rb:
class Client < ActiveRecord::Base
end

Просто и красиво. Посмотрим, что еще было создано. Файл client_test.rb
содержит заготовку для автономных тестов:
require File.dirname(__FILE__) + '/../test_helper'
class ClientTest < Test::Unit::TestCase
fixtures :clients
# Заменить настоящими тестами.
def test_truth
assert true
end
end

Комментарий предлагает заменить метод test_truth настоящими тестами. Но пока мы просто знакомимся со сгенерированным кодом, поэтому пойдем дальше. Отметим, что класс ClientTest ссылается на файл
фикстуры (fixture) clients.yml:
# О фикстурах см. http://ar.rubyonrails.org/classes/Fixtures.html
one:
id: 1
two:
id: 2

Какие-то идентификаторы… Автономные тесты и фикстуры рассматриваются в главе 17 «Тестирование».
И наконец, имеется файл миграции с именем 002_create_clients.rb:
class CreateClients < ActiveRecord::Migration
def self.up
create_table :clients do |t|
# t.column :name, :string
end
end

160

Глава 6. Работа с ActiveRecord
def self.down
drop_table :clients
end
end

Механизм миграций в Rails позволяет создавать и развивать схему базы данных, без него вы не получили бы никаких моделей ActiveRecord
(точнее, они были бы очень скучными). Коли так, рассмотрим миграции более подробно.

Говорит Кортенэ…
ActiveRecord – прекрасный пример «Золотого пути» Rails. Это
означает, что, оставаясь в рамках наложенных ограничений,
можно зайти очень далеко. Но стоит свернуть в сторону, и вы,
скорее всего, завязнете в грязи. Золотой путь подразумевает соблюдение ряда соглашений, в частности, присваивание таблицам
имен во множественном числе (users).
Разработчики, недавно открывшие для себя Rails, а также приверженцы конкурирующих веб-платформ ругаются, что их заставляют именовать таблицы определенным образом, на уровне
базы данных нет никаких ограничений, обработка внешних ключей абсолютно неправильна, в системах масштаба предприятия
первичные ключи должны быть составными, и т. д. и т. п.
Но перестаньте хныкать – все это не более чем умолчания, которые можно переопределить в одной строке кода или с помощью
подключаемого модуля.

Миграции
Никуда не уйти от того факта, что со временем схема базы данных эволюционирует. Добавляются новые таблицы, изменяются имена колонок, что-то удаляется – в общем, вы понимаете, о чем я. Если разработчики не готовы строго соблюдать соглашения и придерживаться определенной дисциплины, то синхронизация схемы базы данных с кодом
приложений традиционно становится очень трудоемкой задачей.
Миграции в Rails помогают развивать схему базы данных приложения
(ее еще называют DDL-описанием1) без уничтожения и нового создания базы после каждого изменения. А это означает, что вы не теряете
данные, появившиеся в процессе разработки. Быть может, иногда это
1

DDL (Data Definition Language) – язык определения данных. – Примеч. перев.

Миграции

161

и не так важно, но обычно очень удобно. При выполнении миграции
достаточно описать изменения, необходимые для перехода от одной
версии схемы к другой – следующей или предыдущей.

Создание миграций
Rails предлагает генератор для создания миграций. Вот текст справки
по нему1:
$ script/generate migration
Порядок вызова: script/generate migration MigrationName [флаги]
Информация о Rails:
-v, --version
Вывести номер версии Rails и завершиться.
-h, --help
Вывести это сообщение и завершиться.
Общие параметры:
-p, --pretend
Выполнять без внесения изменений.
-f, --force
Перезаписывать существующие файлы.
-s, --skip
Пропускать существующие файлы.
-q, --quiet
Подавить нормальную печать.
-t, --backtrace
Отладка: выводить трассировку стека в случае ошибок.
-c, --svn
Модифицировать файлы в системе subversion.

(Примечание: команда svn должна находиться по одному

из просматриваемых путей.)
Описание:
Генератор миграций создает заглушку для новой миграции базы данных.
В качестве аргумента передается имя миграции. Имя может быть задано
в ВерблюжьейНотации или с_подчерками.
Генератор создает класс миграции в каталоге db/migrate, указывая в начале
имени порядковый номер.
Пример:
./script/generate migration AddSslFlag
Если уже существуют 4 миграции, то для миграция AddSslFlag будет создан
файл db/migrate/005_add_ssl_flag.rb.

Как видите, от вас требуется только задать понятное имя миграции
в нотации CamelCase2, а генератор сделает все остальное. Напрямую
мы вызываем генератор только при необходимости изменить атрибуты
колонок в существующей таблице.
1

Оригинальный генератор печатает справку на английском языке. Для
удобства читателя она переведена. – Примеч. перев.

2

Нотация CamelCase или camelCase (вариативность написания заглавных
букв существенна) получила название ВерблюжьейНотации. Имена идентификаторов, состоящие из нескольких слов, записываются так, что слова
не разделяются никакими знаками, но каждое слово начинается с заглавной буквы, например dateOfBirth или DateOfBirth. При этом первое слово
может начинаться с заглавной или строчной буквы – в зависимости от соглашения. Визуально имеется ряд «горбов», как у верблюда. Отсюда и английское название. – Примеч. перев.

162

Глава 6. Работа с ActiveRecord

Как уже отмечалось выше, другие генераторы, в частности генератор
моделей, тоже создают сценарии миграции, если только не указан флаг
--skip-migration.

Именование миграций
Последовательность миграций определяется простой схемой нумерации, отраженной в именах файлов; генератор миграций формирует порядковые номера автоматически.
По соглашению, имя файла начинается с трехзначного номера версии
(дополненного слева нулями), за которым следует имя класса миграции, отделенное знаком подчерка. (Примечание: имя файла обязано
точно соответствовать имени класса, иначе процедура миграции закончится с ошибкой.)
Генератор миграций определяет порядковый номер следующей миграции, справляясь со специальной таблицей в базе данных, за которую
отвечает Rails. Она называется schema_info и имеет очень простую
структуру:
mysql> desc schema_info;
+—————————+—————————+——————+————-+————————-+——————-+
| Field | Type
| Null | Key | Default | Extra |
+—————————+—————————+——————+————-+————————-+——————-+
| version | int(11) | YES |
| NULL
|
|
+—————————+—————————+——————+————-+————————-+——————-+
1 row in set (0.00 sec)

Таблица содержит всего одну колонку и одну строку, в которой хранится текущий номер миграции приложения.
Содержательная часть имени миграции оставлена на ваше усмотрение,
но многие известные мне разработчики Rails стараются отражать в нем
операцию над схемой (в простых случаях) или хотя бы намекнуть, для
чего миграция предназначена (в более сложных).

Подвохи миграций
Если вы пишете свои программы для Rails в одиночку, то у схемы порядковой нумерации имен никаких подвохов нет, так что можете смело
пропустить этот раздел. Проблемы начинаются, когда над одним проектом работают несколько программистов, особенно большие коллективы. Речь идет не о проблемах миграции API самой среды Rails – именно
сопровождение изменяющейся схемы базы данных представляет собой
сложную и пока не до конца решенную задачу.
Вот что пишет о проблемах миграции, с которыми приходилось сталкиваться, мой старый приятель по компании ThoughtWorks Джей
Филдс (Jay Fields), не раз возглавлявший большие коллективы разработчиков на платформе Rails, в своем блоге:

163

Миграции

Миграции – это прекрасно, но за них приходится расплачиваться.
При работе в большом коллективе (а моя теперешняя команда состоит из 14 человек и продолжает расти) случаются конфликты
миграций. Проблему можно отчасти решить с помощью договоренностей, но нет сомнений, что миграции могут стать узким местом.
Кроме того, сам процесс создания миграции в большой команде может протекать болезненно. Прежде чем создавать новую миграцию,
вы должны убедиться, что коллеги сохранили свои миграции в системе управления версиями. Далее наилучшее развитие событий – создать миграцию, которая ничего не изменяет, и сразу же поставить
ее на учет. Это гарантирует, что вы не помешаете создавать миграции другим членам команды, однако не всегда можно ограничиться
одним лишь добавлением таблицы. Если миграция изменяет структуру базы данных, то часто некоторые тесты перестают работать. Очевидно, не следует ставить миграцию на учет, пока все
тесты не заработают нормально, но на это может потребоваться
время. В течение всего этого времени больше никто не сможет вносить изменения в базу данных1.

По-другому та же проблема проявляется, когда требуется внести изменения сразу в несколько ветвей программы. Это вообще оказывается
кошмаром (дополнительную информацию на эту тему см. в комментариях к процитированной записи в блоге Джея).
К сожалению, у данной проблемы нет простого решения, разве что спроектировать базу от начала до конца еще до реализации приложения. Но
на этом пути куча своих проблем (настолько много, что мы даже затрагивать их не будем). Могу лишь по собственному опыту сказать, что заранее продумать пусть грубую, но достаточно полную схему базы данных, а уж потом нырять с головой в кодирование весьма полезно.
Также весьма желательно известить коллег о том, что вы собираетесь
приступить к созданию новой миграции, чтобы потом два человека не

Говорит Себастьян…
Мы применяем подключаемый к svn сценарий (идею подал Конор Хант), который контролирует добавление новых миграций
в репозиторий и предотвращает появление одинаковых номеров.
Еще один подход – назначить человека, отвечающего за миграции. Тогда разработчики могли бы локально создавать и тестировать миграции, а потом отправлять их по электронной почте
«координатору», который проверит результат и присвоит правильные номера.

1

http://jayfields.blogspot.com/2006/12/rails-migrations-with-large-teampart.html.

164

Глава 6. Работа с ActiveRecord

Говорит Кортенэ…
Выгоды от хранения схемы базы данных в системе управления
версиями намного перевешивают трудности, которые возникают, когда члены команды спонтанно изменяют схему. Хранение
всех версий кода снимает неприятный вопрос: «Кто добавил это
поле?».
Как обычно, на эту тему написано несколько подключаемых к Rails
модулей. Один из них написал я сам и назвал IndependentMigrations.
Проще говоря, он позволяет иметь несколько миграций с одним и тем же номером. Другие модули допускают идентификацию миграций по временному штампу. Подробнее о моем модуле
и альтернативах можно прочитать по адресу http://blog.caboo.se/
articles/2007/3/27/independent-migrations-plugin.

попытались поставить на учет миграцию с одним и тем же номером,
что приведет к печальным последствиям.

Migration API
Но вернемся к самому Migration API. Вот как будет выглядеть созданный ранее файл 002_create_clients.rb после добавления определений
четырех колонок в таблицу clients:
class CreateClients < ActiveRecord::Migration
def self.up
create_table :clients do |t|
t.column :name, :string
t.column :code, :string
t.column :created_at, :datetime
t.column :updated_at, :datetime
end
end
def self.down
drop_table :clients
end
end

Как видно из этого примера, директивы миграции находятся в определениях двух методов класса: self.up и self.down. Если перейти в каталог проекта и набрать команду rake db:migrate, будет создана таблица
clients. По ходу миграции Rails печатает информативные сообщения,
чтобы было видно, что происходит:
$ rake db:migrate
(in /Users/obie/prorails/time_and_expenses)

Миграции

165

== 2 CreateClients: migrating
==========================================
-- create_table(:clients)
-> 0.0448s
== 2 CreateClients: migrated (0.0450s)
=================================

Обычно выполняется только код метода up, но, если вы захотите откатиться к предыдущей версии схемы, то метод down опишет, что нужно
сделать, чтобы отменить действия, произведенные в методе up.
Для отката необходимо выполнить то же самое задание migrate, но передать в качестве параметра номер версии, на которую необходимо откатиться: rake db:migrate VERSION=1.

create_table(name, options)
Методу create_table необходимы по меньшей мере имя таблицы и блок,
содержащий определения колонок. Почему мы задаем идентификаторы символами, а не просто в виде строк? Работать будет и то, и другое,
но для ввода символа нужно на одно нажатие меньше1.
В методе create_table сделано серьезное и обычно оказывающееся истинным предположение: необходим автоинкрементный целочисленный первичный ключ. Именно поэтому вы не видите его объявления
в списке колонок. Если это допущение не выполняется, придется передать методу create_table некоторые параметры в виде хеша.
Например, как определить простую связующую таблицу, в которой
есть два внешних ключа, но ни одного первичного? Просто задайте параметр :id равным false – в виде булева значения, а не символа! Тогда
миграция не будет автоматически генерировать первичный ключ:
create_table :ingredients_recipes, :id => false do |t|
t.column :ingredient_id, :integer
t.column :recipe_id, :integer
end

Если же вы хотите, чтобы колонка, содержащая первичный ключ, называлась не id, передайте в параметре :id какой-нибудь символ. Пусть,
например, корпоративный стандарт требует, чтобы первичные ключи
именовались с учетом объемлющей таблицы: tablename_id. Тогда ранее приведенный пример нужно записать так:
create_table :clients, :id => :clients_id do |t|
t.column :name, :string
t.column :code, :string
t.column :created_at, :datetime

1

Если вы находите, что взаимозаменяемость символов и строк в Rails несколько раздражает, то не одиноки.

166

Глава 6. Работа с ActiveRecord
t.column :updated_at, :datetime
end

Параметр :force => true говорит миграции, что нужно предварительно
удалить определяемую таблицу, если она существует. Но будьте осторожны, поскольку при запуске в режиме эксплуатации это может
привести к потере данных (чего вы, возможно, не хотели). Насколько
я знаю, параметр :force наиболее полезен, когда нужно привести базу
данных в известное состояние, но при повседневной работе он редко
бывает нужен.
Параметр :options позволяет включить дополнительные инструкции
в SQL-предложение CREATE и полезен для учета специфики конкретной СУБД. В зависимости от используемой СУБД можно задать, например, кодировку, схему сортировки, комментарии, минимальный
и максимальный размер и многие другие свойства.
Параметр :temporary => true сообщает, что нужно создать таблицу, существующую только во время выполнения миграции. В сложных случаях это может оказаться полезным для переноса больших наборов данных из одной таблицы в другую, но вообще-то применяется нечасто.

Говорит Себастьян…
Малоизвестно, что можно удалять файлы из каталогов миграции
(сохраняя самые свежие), чтобы размер каталога db/migrate оставался на приемлемом уровне. Можно, скажем, переместить старые миграции в каталог db/archived_migrations или сделать еще
что-то в этом роде.
Если вы хотите быть абсолютно уверены, что ваш код допускает
развертывание с нуля, можете заменить миграцию с наименьшим номером миграцией «создать заново все», основанной на
текущем содержимом файла schema.rb.

Определение колонок
Добавить в таблицу колонки можно либо с помощью метода column внутри блока, ассоциированного с предложением create_table, либо методом
add_column. Второй метод отличается от первого только тем, что принимает в качестве первого аргумента имя таблицы, куда добавляется колонка.
create_table :clients do |t|
t.column :name, :string
end
add_column :clients, :code, :string
add_column :clients, :created_at, :datetime

167

Миграции

Первый (или второй) параметр – имя колонки, а второй (или третий) –
ее тип. В стандарте SQL92 определены фундаментальные типы данных,
но в каждой конкретной СУБД имеются свойственные только ей расширения стандарта.
Если вы знакомы с типами данных в СУБД, то предыдущий пример
мог вызвать недоумение: почему колонка имеет тип string, хотя в базах данных такого типа нет, а есть типы char и varchar?

Отображение типов колонок
Причина, по которой для колонки базы данных объявлен тип string,
заключается в том, что миграции в Rails по идее должны быть независимыми от СУБД. Вот почему можно (и я это делал) вести разработку
на СУБД Postgres, а развертывать систему на Oracle.
Полное обсуждение вопроса о том, как выбирать правильные типы данных, выходит за рамки этой книги. Но полезно иметь под рукой справку от отображении обобщенных типов в миграциях на конкретные типы для различных СУБД. В табл. 6.1 такое отображение приведено для
СУБД, которые наиболее часто встречаются в приложениях Rails.
Таблица 6.1. Отображение типов данных для СУБД, которые наиболее
часто встречаются в приложениях Rails
Тип миграции
Класс Ruby
:binary
String
:boolean
Boolean
:date
Date
:datetime
Time
:decimal
BigDecimal
:float
Float
:integer
Fixnum
:string
String
:text
String
:time
Time
:timestamp
Time

MySQL

Postgres

SQLite

Oracle

blob

bytea

blob

blob

tinyint(1)

boolean

Boolean

number(1)

date

date

date

date

datetime

timestamp

datetime

date

decimal

decimal

decimal

decimal

float

float

float

number

int(11)

integer

integer

number(38)

varchar(255)

character
varying(255)

varchar(255)

varchar(255)

text

clob(32768)

text

clob

time

time

time

date

datetime

timestamp

datetime

date

168

Глава 6. Работа с ActiveRecord

Для каждого класса-адаптера соединения существует хеш native_database_types, устанавливающий описанное в табл. 6.1 отображение. Если
вас заинтересуют отображения для других СУБД, можете открыть код
соответствующего адаптера и найти в нем вышеупомянутый хеш. Так,
в классе SQLServerAdapter из файла sqlserver_adapter.rb хеш native_database_types выглядит следующим образом:
def native_database_types
{
:primary_key => "int NOT NULL IDENTITY(1, 1) PRIMARY KEY",
:string
=> { :name => "varchar", :limit => 255 },
:text
=> { :name => "text" },
:integer
=> { :name => "int" },
:float
=> { :name => "float", :limit => 8 },
:decimal
=> { :name => "decimal" },
:datetime
=> { :name => "datetime" },
:timestamp => { :name => "datetime" },
:time
=> { :name => "datetime" },
:date
=> { :name => "datetime" },
:binary
=> { :name => "image"},
:boolean
=> { :name => "bit"}
}
end

Дополнительные характеристики колонок
Во многих случаях одного лишь указания типа данных недостаточно.
Все объявления колонок принимают еще и следующие параметры:
:default => value

Задает значение по умолчанию, которое записывается в данную колонку вновь созданной строки. Явно указывать null необязательно, достаточно просто опустить этот параметр.
:limit => size

Задает размер для колонок типа string, text, binary и integer. Семантика
зависит от конкретного типа данных. В общем случае ограничение на
строковые типы относится к числу символов, а для других типов речь
идет о количестве байтов, выделяемых в базе для хранения значения.
:null => true

Делает колонку обязательной, добавляя ограничение not null, которое
проверяется на уровне СУБД.

Количество знаков в десятичной записи
Для колонок, объявленных как :decimal, можно задавать следующие
характеристики:

Миграции

169

:precision => number

Здесь precision (точность) – общее число цифр в десятичной записи
числа.
:scale => number

Здесь scale (масштаб) – число цифр справа от десятичного знака. Например, для числа 123,45 точность равна 5, а масштаб – 2. Очевидно,
масштаб не может быть больше точности.

Примечание
Для десятичных типов велика опасность потери данных при переносе с одной СУБД на
другую Например, поскольку в Oracle и SQL Server принимаемая по умолчанию точность
различается, в процессе переноса может происходить отбрасывание знаков и, как
следствие, изменение числового значения. Поэтому разумно всегда задавать точность
и масштаб явно.

Подводные камни при выборе типов колонок
Выбор типа колонки не всегда очевиден и зависит как от используемой
СУБД, так и от требований, предъявляемых приложением:
• :binary. В зависимости от способа использования хранение в базе
двоичных данных может сильно снизить производительность. Rails
загружает объекты из базы данных целиком, поэтому присутствие
больших двоичных атрибутов в часто употребляемых моделях заметно увеличивает нагрузку на сервер базы данных;
• :boolean. Булевы значения в разных СУБД хранятся по-разному.
Иногда для представления true и false используются целые значения 1 и 0, а иногда – символы T и F. Rails прекрасно справляется
с задачей отображения таких значений на «родные» для Ruby объекты true и false, поэтому задумываться о реальной схеме хранения
не нужно. Прямое присваивание атрибутам значений 1 или F, в конкретном случае, может быть, и сработает, но такая практика считается антипаттерном;
• :date, :datetime и :time. Сохранение дат в СУБД, где нет встроенного
типа даты, например в Microsoft SQL Server, может стать проблемой. Rails отображает тип datetime на класс Ruby Time, который не
позволяет представить даты ранее 1 января 1970 года. Но ведь имеющийся в Ruby класс DateTime умеет работать с более ранними датами, так почему же он не используется в Rails? Дело в том, что класс
Time реализован на C и потому работает очень быстро, тогда как
DateTime написан на чистом Ruby и, следовательно, медленнее.
Чтобы заставить ActiveRecord отображать тип даты на DateTime вместо
Time, поместите код из листинга 6.1 в какой-нибудь файл, находящийся в каталоге lib/ и затребуйте его из сценария config/environment.rb
с помощью require.

170

Глава 6. Работа с ActiveRecord

Листинг 6.1. Отображение даты на тип DateTime вместо Time
require 'date'
# Это необходимо сделать, потому что класс Time не поддерживает
# даты ранее 1970 года…
class ActiveRecord::ConnectionAdapters::Column
def self.string_to_time(string)
return string unless string.is_a?(String)
time_array = ParseDate.parsedate(string)[0..5]
begin
Time.send(Base.default_timezone, *time_array)
rescue
DateTime.new(*time_array) rescue nil
end
end
end

• :decimal. В старых версиях Rails (до 1.2) тип :decimal с фиксированной точкой не поддерживался, поэтому во многих ранних приложениях Rails некорректно использовался тип :float. Числа с плавающей точкой по природе своей неточны, поэтому для большинства
бизнес-приложений следует выбирать тип :decimal, а не :float;
• :float. Не пользуйтесь типом :float для хранения денежных сумм1
и вообще любых данных, для которых необходима фиксированная
точность. Поскольку числа с плавающей точкой дают хорошую аппроксимацию, простое хранение данных в таком формате, наверное, приемлемо. Проблемы начинаются, когда вы пытаетесь выполнять над числами математические действия или операции сравнения, поскольку внести таким образом ошибку в приложение до
смешного просто, а найти ее ох как тяжело;
• :integer и :string. Есть не так уж много неприятностей, с которыми
можно столкнуться при использовании целых и строковых типов.
Это основные кирпичики любого приложения, и многие разработчики опускают задание размера, получая по умолчанию 11 цифр
и 255 знаков соответственно.
Следует помнить, что при попытке сохранить значение, которое не помещается в отведенную для него колонку (для строк – по умолчанию
255 знаков), вы не получите никакого уведомления об ошибке. Строка
будет просто молча обрезана. Убеждайтесь, что длина введенных пользователем данных не превосходит максимально допустимой.
• :text. Есть сообщения о том, что текстовые поля снижают производительность запросов настолько, что в сильно нагруженных приложениях это может превратиться в проблему. Если вам абсолютно
необходимы текстовые данные в приложениях, для которых быстродействие критично, помещайте их в отдельную таблицу;
1

По адресу http://dist.leetsoft.com/api/money/ размещен рекомендуемый
класс Money с открытым исходным текстом.

Методы в стиле макросов

171

• :timestamp. В версии Rails 1.2 при создании новых записей
ActiveRecord может не очень хорошо работать, когда значение колонки по умолчанию генерируется функцией, как для данных типа
timestamp в Postgres. Проблема в том, что Rails не исключает такие
колонки из предложения insert, как следовало бы, а задает для них
«значение» null, что может приводить к игнорированию значения
по умолчанию.

Нестандартные типы данных
Если в вашем приложении необходимы типы данных, специфичные
для конкретной СУБД (например, тип :double, обеспечивающий более
высокую точность, чем :float), включите в файл config/environment.rb
директиву config.active_record.schema_format = :sql, чтобы заставить Rails
сохранять информацию о схеме в «родном» для данной СУБД формате
DDL, а не в виде кросс-платформенного кода на Ruby, записываемого
в файл schema.rb.

«Магические» колонки с временными штампами
К колонкам типа timestamp Rails применяет магию, если они названы
определенным образом. Active Record автоматически снабжает операции создания временным штампом, если в таблице есть колонка с именем created_at или created_on. То же самое относится к операциям обновления, если в таблице есть колонка с именем updated_at или updated_on.
Отметим, что в файле миграции тип колонок created_at и updated_at
должен быть задан как datetime, а не timestamp.
Автоматическую проштамповку можно глобально отключить, задав
в файле config/environment.rb следующую переменную:
ActiveRecord::Base.record_timestamps = false

За счет наследования данный код отключает временные штампы для всех
моделей, но можно сделать это и избирательно в конкретной модели, если
установить переменную record_timestamps в false только для нее. По умолчанию временные штампы выражены в местном поясном времени, но,
если задать переменную ActiveRecord::Base.default_timezone = :utc, будет
использовано время UTC.

Методы в стиле макросов
Большинство существенных классов, которые вы пишете, программируя приложение для Rails, сконфигурированы для вызова в стиле
макросов (в определенных кругах это также называется предметноориентированным языком, или DSL – domain-specific language). Основная идея заключается в том, что в начале класса размещается максимально понятный блок кода, конфигурация которого сразу видна.

172

Глава 6. Работа с ActiveRecord

Для размещения вызовов в стиле макросов именно в начале файла есть
веская причина. Эти методы декларативно сообщают Rails, как управлять экземплярами, выполнять контроль данных, делать обратные вызовы и взаимодействовать с другими моделями. Часто в таких методах
используется метапрограммирование, то есть на этапе выполнения
они добавляют в класс то или иное поведение в форме дополнительных
переменных экземпляра или методов.

Объявление отношений
Рассмотрим, например, класс Client, в котором объявлены некоторые
отношения. Не пугайтесь, если смысл этих объявлений вам не ясен;
мы подробно поговорим об этом в главе 7 «Ассоциации в ActiveRecord».
Сейчас я хочу лишь показать, что имею в виду, говоря о стиле макросов:
class Client < ActiveRecord::Base
has_many :billing_codes
has_many :billable_weeks
has_many :timesheets, :through => :billable_weeks
end

Благодаря трем объявлениям has_many класс Client получает по меньшей мере три новых атрибута – прокси-объекты, позволяющие интерактивно манипулировать ассоциированными наборами.
Я припоминаю, как когда-то в первый раз обучал своего друга – опытного программиста на Java – основам Ruby и Rails. Несколько минут
он пребывал в сильном замешательстве, а потом я буквально увидел,
как в его голове зажглась лампочка, и он провозгласил: «О! Так это же
методы!».
Ну конечно, это самые обычные вызовы методов в контексте объекта
класса. Мы опустили скобки, чтобы подчеркнуть декларативность. Это
не более чем вопрос стиля, но лично мне скобки в таком фрагменте кажутся неуместными:
class Client < ActiveRecord::Base
has_many(:billing_codes)
has_many(:billable_weeks)
has_many(:timesheets, :through => :billable_weeks)
end

Когда интерпретатор Ruby загружает файл client.rb, он выполняет методы has_many, которые, еще раз подчеркну, определены как методы
класса ActiveRecord::Base. Они выполняются в контексте класса Client
и добавляют в него атрибуты, которые в дальнейшем становятся доступны экземплярам класса Client. Новичку такая модель программирования может показаться странной, но очень скоро она становится
«вторым я» любого программиста Rails.

Методы в стиле макросов

173

Примат соглашения над конфигурацией
Примат соглашения над конфигурацией – один из руководящих принципов Rails. Если вы следуете принятым в Rails соглашениям, то почти ничего не придется явно конфигурировать. И здесь мы наблюдаем
разительный контраст с тем, сколько приходится конфигурировать
для запуска даже простейшего приложения в других технологиях.
Дело не в том, что всякое новое приложение Rails уже создается с готовой умалчиваемой конфигурацией, отражающей применяемые соглашения. Нет – соглашения уже впечатаны в среду, жестко зашиты в ее
поведение, и это-то подразумеваемое по умолчанию поведение вы и переопределяете с помощью явного конфигурирования, когда возникает
необходимость.
Стоит также отметить, что, как правило, конфигурирование производится очень близко к тому, что конфигурируется. Объявления ассоциаций, проверок и обратных вызовов располагаются в начале большин­
ства моделей ActiveRecord.
Подозреваю, что многие из нас впервые занялись конфигурированием
(в противовес соглашению), чтобы изменить соответствие между именем класса и таблицы базы данных, поскольку по умолчанию Rails
предполагает, что имя таблицы образуется как множественное число
от имени класса. И, поскольку такое соглашение застает врасплох многих начинающих разработчиков Rails, рассмотрим эту тему, перед тем
как двигаться дальше.

Приведение к множественному числу
В Rails есть класс Inflector, в обязанность которого входит преобразование строк (слов) из единственного числа в множественное, имен
классов – в имена таблиц, имен классов с указанием модуля – в имена
без модуля, имен классов – во внешние ключи и т. д. (некоторые операции имеют довольно смешные имена, например dasherize).
Принимаемые по умолчанию окончания для формирования единственного и множественного числа неисчисляемых имен существительных
хранятся в файле inflections.rb в каталоге установки Rails. Как правило, класс Inflector успешно находит имя таблицы, образуемое приведением имени класса к множественному числу, но иногда случаются
оплошности. Для многих новых пользователей Rails это становится
первым камнем преткновения, но причин для паники нет. Можно заранее проверить, как Inflector будет реагировать на те или иные слова.
Для этого понадобится лишь консоль Rails, которая, кстати, является
одним из лучших инструментов при работе с Rails.
Чтобы запустить консоль, выполните из командной строки сценарий
script/console, который находится в каталоге вашего проекта.

174

Глава 6. Работа с ActiveRecord
$ script/console
>> Inflector.pluralize "project"
=> "projects"
>> Inflector.pluralize "virus"
=> "viri"
>> Inflector.pluralize "pensum"
=> "pensums"

Как видите, Inflector достаточно умен – в качестве множественного
числа от virus он правильно выбрал viri. Но, если вы знаете латынь,
то, наверное, обратили внимание, что pensum во множественном числе на самом деле пишется как pensa. Понятно, что инфлектор латыни
не обучен.
Однако вы можете расширить познания инфлектора одним из трех
способов:
• добавить новое правило-образец
• описать исключение
• объявить, что некое слово не имеет множественного числа
Лучше всего делать это в файле config/environment.rb, где уже имеется прокомментированный пример:
Inflector.inflections do |inflect|
inflect.plural /^(.*)um$/i, '\1a'
inflect.singular /^(.*)a/i, '\1um'
inflect.irregular 'album', 'albums'
inflect.uncountable %w( valium )
end

Кстати, в версии Rails 1.2 инфлектор принимает слова, уже записанные во множественном числе, и… ничего с ними не делает, что, наверное, самое правильное. Прежние версии Rails вели себя в этом отношении не так разумно.
>>
=>
>>
=>

"territories".pluralize
"territories"
"queries".pluralize
"queries"

Если хотите посмотреть на длинный список существительных, которые Inflector правильно приводит к множественному числу, загляните
в файл activesupport/test/inflector_test.rb. Я нашел в нем немало интересного, например:
"datum" => "data",
"medium" => "media",
"analysis" => "analyses"

Методы в стиле макросов

175

Надо ли сообщать разработчикам ядра об ошибках
в работе Inflector
Майкл Козярский (Michael Koziarski), один из разработчиков ядра
Rails, пишет, что сообщать о проблемах с классом Inflector не следует:
«Инфлектор практически заморожен; до выхода версии 1.0 мы добавляли в него много правил для исправления ошибок, чем привели
в ярость тех, кто называл свои таблицы согласно старым вариантам.
Если необходимо, добавляйте исключения в файл environment.rb самостоятельно».

Задание имен вручную
Разобравшись с инфлектором, вернемся к конфигурированию классов
моделей ActiveRecord. Методы set_table_name и set_primary_key позволяют обойти соглашения Rails и явно задать имя таблицы и имя колонки, содержащей первичный ключ.
Предположим, к примеру (и только к примеру!), что я вынужден использовать для именования таблиц какое-то мерзкое соглашение, отличающееся от принятого в ActiveRecord. Тогда я мог бы поступить
следующим образом:
class Client < ActiveRecord::Base
set_table_name "CLIENT"
set_primary_key "CLIENT_ID"
end

Методы set_table_name и set_primary_key позволяют выбрать для таблиц и первичных ключей произвольные имена, но тогда вы должны
явно указать их в классе модели. Для одной модели это всего лишь
пара лишних строчек, но в большом приложении оказывается ненужным усложнением, поэтому не делайте этого без острой необходимости.
Если вы не можете сами диктовать соглашения о выборе имен, например, когда схемами управляет отдельная группа администрирования
базы данных, то выбора, наверное, нет. Но при наличии свободы действий лучше придерживаться соглашений Rails. Возможно, это покажется непривычным, зато позволит сэкономить время и избежать ненужных сложностей.

Унаследованные схемы именования
Если вы работаете с унаследованными схемами, может возникнуть искушение вставлять метод set_table_name повсюду, нужен он или не нужен. Но прежде чем у вас появится такая привычка, познакомьтесь

176

Глава 6. Работа с ActiveRecord

еще с некоторыми возможностями, которые позволят избежать повторений и облегчить себе жизнь.
Чтобы полностью отключить механизм приведения имен таблиц к множественному числу, добавьте в конец файла config/environment.rb
следующую строку:
ActiveRecord::Base.pluralize_table_names = false

В классе ActiveRecord::Base есть и другие полезные атрибуты, позволяющие сконфигурировать Rails для работы с унаследованными схемами
именования:
• primary_key_prefix_type. Акцессор для задания префикса, который
добавляется в начало имени любого первичного ключа. Если задан
параметр :table_name, то ActiveRecord будет считать, что первичный ключ называется tableid, а не id. Если же задан параметр
:table_name_with_underscore, то предполагается, что первичный ключ
называется table_id;
• table_name_prefix. Иногда к имени таблицы добавляют имя базы данных. Установите этот атрибут, чтобы не включать префикс в имена
всех классов модели вручную;
• table_name_suffix. Помимо префикса, можно добавить к именам
всех таблиц еще и суффикс;
• underscore_table_names. Установите в false, если не хотите, чтобы
ActiveRecord вставляла подчерки между отдельными частями составного имени таблицы.

Определение атрибутов
Список атрибутов, ассоциированных с классов модели ActiveRecord,
явно не кодируется. На этапе выполнения модель ActiveRecord получает схему базы данных непосредственно от сервера. Добавление, удаление и изменение атрибутов или их типов производится путем манипулирования самой базой данных – с помощью команд SQL или графических инструментов. Но в идеале для этого следует применять миграции ActiveRecord.
Практическое следствие паттерна ActiveRecord состоит в том, что вы
должны определить структуру таблицы базы данных и убедиться, что
эта таблица существует, перед тем как начинать работу с моделью.
У некоторых программистов такая философия проектирования может
вызывать трудности, особенно если они привыкли к проектированию
«сверху вниз».
Без сомнения, путь Rails подразумевает, что классы модели имеют тесную связь со схемой базой данных. Но, с другой стороны, помните, что
модели могут быть обычными классами Ruby, необязательно расширя-

Определение атрибутов

177

ющими ActiveRecord::Base. В частности, очень часто классы моделей,
не имеющие отношения к ActiveRecord, применяются с целью инкапсуляции данных и логики для уровня представления.

Значения атрибутов по умолчанию
Миграции позволяют задавать значения атрибутов по умолчанию путем передачи параметра :default методу column, но, как правило, это
следует делать на уровне модели, а не на уровне базы данных. Значения по умолчанию – часть логики предметной области, поэтому их место – рядом со всей прочей предметной логикой приложения, то есть на
уровне модели.
Типичный пример – модель должна возвращать строку "n/a" вместо nil
(или пустой строки), если атрибуту еще не присвоено значение. Этот
пример достаточно прост, поэтому может служить отправной точкой
для разговора о том, как атрибуты возникают на этапе выполнения.
Для начала соорудим простенький тест, описывающий желаемое поведение:
class SpecificationTest < Test::Unit::TestCase
def test_default_string_for_tolerance_should_be_na
spec = Specification.new
assert_equal 'n/a', spec.tolerance
end
end

Этот тест, как и следовало ожидать, не проходит. ActiveRecord не предоставляет в модели никаких методов класса для декларативного определения значений по умолчанию. Похоже, придется явно создать акцессор для данного атрибута, который будет возвращать значение по
умолчанию.
Обычно с акцессорами атрибутов ActiveRecord разбирается самостоятельно, но в данном случае нам предстоит вмешаться и подставить свой
метод чтения. Для этого достаточно определить метод с таким же именем, как у атрибута, и воспользоваться оператором or, котовый вернет
альтернативу, если @tolerance равно nil:
class Specification < ActiveRecord::Base
def tolerance
@tolerance or 'n/a'
end
end

Теперь тест проходит. Замечательно. Все сделали? Не совсем. Надо еще
протестировать случай, при котором должно возвращаться истинное
значение @tolerance. Добавим спецификацию теста с непустым значением @tolerance и изменим имена методов тестирования, сделав их более содержательными.

178

Глава 6. Работа с ActiveRecord
class SpecificationTest < Test::Unit::TestCase
def test_default_string_for_tolerance_should_return_na_when_nil
spec = Specification.new
assert_equal 'n/a', spec.tolerance
end
def test_tolerance_value_should_be_returned_when_not_nil
spec = Specification.new(:tolerance => '0.01mm')
assert_equal '0.01mm', spec.tolerance
end
end

Опаньки! Второй тест не проходит. Похоже, в любом случае возвращается строка "n/a". Это означает, что в атрибут @tolerance ничего не было
записано. Должны ли мы знать о том, что запись в атрибут не производится? Это деталь реализации ActiveRecord или нет?
Тот факт, что в Rails переменные экземпляра наподобие @tolerance не
используются для хранения атрибутов модели, – действительно деталь
реализации. Но в экземплярах моделей есть два метода write_attribute
и read_attribute, которые ActiveRecord предлагает для переопределения акцессоров, подразумеваемых по умолчанию, а это как раз то, что
мы пытаемся сделать. Давайте исправим класс Specification:
class Specification < ActiveRecord::Base
def tolerance
read_attribute(:tolerance) or 'n/a'
end
end

Теперь тест проходит. А как насчет простого примера использования
write_attribute?
class SillyFortuneCookie < ActiveRecord::Base
def message=(txt)
write_attribute(:message, txt + ' in bed')
end
end

Оба примера можно было бы написать и по-другому, воспользовавшись
более короткой формой чтения и записи атрибутов с помощью квадратных скобок:
class Specification < ActiveRecord::Base
def tolerance
self[:tolerance] or 'n/a'
end
end
class SillyFortuneCookie < ActiveRecord::Base
def message=(txt)
self[:message] = txt + ' in bed'
end
end

CRUD: создание, чтение, обновление, удаление

179

Сериализованные атрибуты
Одна из самых «крутых» (на мой взгляд) особенностей ActiveRecord –
возможность помечать колонки типа text как сериализованные. Любой объект (точнее, граф объектов), записываемый в такой атрибут,
будет храниться в базе данных в формате YAML – стандартном для
Ruby формате сериализации.

Говорит Себастьян…
Максимальный размер колонок типа TEXT составляет 64K. Если
сериализованный атрибут оказывается длиннее, не миновать
многочисленных ошибок.
С другой стороны, если ваши сериализованные атрибуты оказываются настолько длинными, то стоит еще раз подумать, что вы
хотите сделать. Как минимум перенесите такие атрибуты в отдельную таблицу и выберите для них более длинный тип данных,
если СУБД позволяет.

CRUD: создание, чтение, обновление, удаление
Акроним CRUD обозначает четыре стандартные операции любой
СУБД.
У него несколько негативная окраска, поскольку в английском языке
слово crud означает «ненужное барахло». Однако в кругах, связанных
с Rails, использование слова CRUD всецело одобряется. Как мы увидим в следующих главах, проектирование функциональности приложений в виде набора CRUD-операций считается самым правильным
подходом!

Создание новых экземпляров ActiveRecord
Самый прямолинейный способ создать новый экземпляр модели
ActiveRecord – воспользоваться обычным механизмом конструирования в Ruby – методом класса new. Вновь созданные объекты могут быть
пустыми (если опустить параметры) или с уже установленными, но
еще не сохраненными атрибутами. Достаточно передать конструктору
хеш, в котором имена ключей соответствуют именам колонок в ассоциированной таблице. В обоих случаях допустимые ключи определяются
именами колонок в таблице, поэтому нельзя задать атрибут, которому
не соответствуют никакая колонка.
В только что созданном, но еще не сохраненном объекте ActiveRecord
имеется атрибут @new_record, который можно опросить методом new_
record?:

180

Глава 6. Работа с ActiveRecord
>> c = Client.new
=> #nil,
"code"=>nil}>
>> c.new_record?
=> true

Конструкторы ActiveRecord принимают необязательный блок и используют его для дополнительной инициализации. Этот блок выполняется, когда в экземпляре уже установлены значения всех переданных конструктору атрибутов:
>> c = Client.new do |client|
?> client.name = "Nile River Co."
>> client.code = "NRC"
>> end
=> #"Nile
River Co.", "code"=>"NRC"}>

В ActiveRecord имеется также удобный метод класса create, который создает новый экземпляр, записывает его в базу данных и возвращает – все
в одной операции:
>> c = Client.create(:name => "Nile River, Co.", :code => "NRC")
=> #"Nile River,
Co.", "updated_at"=>Mon Jun 04 22:24:27 UTC 2007, "code"=>"NRC",
"id"=>1, "created_at"=>Mon Jun 04 22:24:27 UTC 2007}>

Метод create не принимает блок. Должен бы, поскольку это самое естественное место для инициализации объекта перед сохранением, но,
увы, не принимает.

Чтение объектов ActiveRecord
Считывать данные из базы в экземпляр объекта ActiveRecord очень
легко и удобно. Основной механизм – метод find, который скрывает
операцию SQL SELECT от разработчика.

Метод find
Искать существующий объект по первичному ключу очень просто. Пожалуй, это одна из первых вещей, которые мы узнаем о Rails, только
начиная изучать эту среду. Достаточно вызвать метод find, указав
ключ искомого экземпляра. Но помните: если такой экземпляр отсут­
ствует, возникнет исключение RecordNotFound.
>> first_project = Project.find(1)
>> boom_client = Client.find(99)
ActiveRecord::RecordNotFound: Couldn't find Client with ID=99
from
/vendor/rails/activerecord/lib/active_record/base.rb:1028:in

CRUD: создание, чтение, обновление, удаление

181

'find_one'
from
/vendor/rails/activerecord/lib/active_record/base.rb:1011:in
'find_from_ids'
from
/vendor/rails/activerecord/lib/active_record/base.rb:416:in 'find'
from (irb):

Метод find понимает также два специальных символа Ruby: :first
и :all:
>> all_clients = Client.find(:all)
=> [#"Paper Jam Printers",
"code"=>"PJP", "id"=>"1"}>, #"Goodness Steaks", "code"=>"GOOD_STEAKS",
"id"=>"2"}>]
>> first_client = Client.find(:first)
=> #"Paper Jam Printers",
"code"=>"PJP", "id"=>"1"}>

Мне странно, что не существует параметра :last, но получить последнюю запись несложно, воспользовавшись параметром :order:
>> all_clients = Client.find(:first, :order => 'id desc')
=> #"Paper Jam Printers",
"code"=>"PJP", "id"=>"1"}>

Кстати, для методов Ruby совершенно естественно возвращать значения разных типов в зависимости от параметров, что и иллюстрирует
предыдущий пример. В зависимости от того, как вызван метод find, вы
получаете либо единственный объект ActiveRecord, либо массив таких
объектов.
Наконец, метод find понимает также массив ключей и возбуждает исключение RecordNotFound, если не может найти хотя бы один из них:
>> first_couple_of_clients = Client.find(1, 2)
[#"Paper Jam Printers",
"code"=>"PJP", "id"=>"1"}>, #
"Goodness Steaks", "code"=>"GOOD_STEAKS", "id"=>"2"}>]
>> first_few_clients = Client.find(1, 2, 3)
ActiveRecord::RecordNotFound: Couldn't find all Clients with IDs
(1,2,3)
from /vendor/rails/activerecord/lib/active_record/base.rb:1042:in
'find_some'
from /vendor/rails/activerecord/lib/active_record/base.rb:1014:in
'find_from_ids'
from /vendor/rails/activerecord/lib/active_record/base.rb:416:in
'find'
from (irb):9

182

Глава 6. Работа с ActiveRecord

Чтение и запись атрибутов
Выбрав из базы данных экземпляр модели, вы можете получить доступ
к колонкам несколькими способами. Самый простой (и понятный) –
воспользоваться оператором «точка» для доступа к атрибуту:
>>
=>
>>
=>

first_client.name
"Paper Jam Printers"
first_client.code
"PJP"

Полезно знать о закрытом методе read_attribute, которого мы вскользь
коснулись выше. Он удобен, если нужно переопределить акцессор атрибута, подразумеваемый по умолчанию. Для иллюстрации, не выходя из консоли Rails, я заново открою класс и переопределю акцессор
name, чтобы он инвертировал прочитанное из базы значение:
>>
>>
>>
>>
>>
=>
>>
=>

class Client
def name
read_attribute(:name).reverse
end
end
nil
first_client.name
"sretnirP maJ repaP"

Мне не составит труда продемонстрировать, почему в этом случае необходимо переопределять read_attribute:
>> class Client
>> def name
>>
self.name.reverse
>> end
>> end
=> nil
>> first_client.name
SystemStackError: stack level too deep
from (irb):21:in 'name'
from (irb):21:in 'name'
from (irb):24

Как и следовало ожидать, в дополнение к методу read_attribute существует метод write_attribute, который позволяет изменить значение атрибута:
project = Project.new
project.write_attribute(:name, "A New Project")

Переопределять методы установки атрибутов для задания нестандартного поведения так же просто, как методы чтения:
class Project
# Описанию проекта нельзя присваивать пустую строку
def description=(new_value)

CRUD: создание, чтение, обновление, удаление

183

self[:description] = new_value unless new_value.blank?
end
end

Предыдущий пример иллюстрирует простой способ контроля данных –
перед тем как разрешить присваивание, проверяется, пусто ли новое
значение. Ниже вы познакомитесь с более удобными подходами к решению этой задачи.

Нотация хеша
Еще один способ доступа к атрибутам заключается в использовании
оператора «квадратные скобки», который позволяет обращаться к атрибутам так, как будто это обычный хеш:
>>
=>
>>
=>

first_client['name']
"Paper Jam Printers"
first_client[:name]
"Paper Jam Printers"

Строка или символ
Многие методы в Rails принимают в качестве параметров как
символы, так и строки, и это может вносить путаницу. Что правильнее?
Общее правило состоит в том, чтобы использовать символы, когда строка выступает в роли имени, и строки, когда речь идет
о значении. Пожалуй, символы правильнее употреблять в каче­ст­
ве ключей хеша и для иных подобных целей.
Здравый смысл подсказывает, что нужно выбрать какое-то одно
соглашение и следовать ему во всем приложении, но большинст­
во разработчиков для Rails всюду, где можно, употребляют символы.

Метод attributes
Существует также метод attributes, возвращающий хеш, где каждый
атрибут представлен своим именем и значением, которое возвращает
read_attribute. Если вы переопределяете методы чтения и записи атрибутов, следует помнить, что метод attributes не вызывает переопределенные версии акцессоров чтения, тогда как метод attributes= (позволяющий выполнять множественное присваивание) вызывает пере­
определенные версии акцессоров записи.
>> first_client.attributes
=> {"name"=>"Paper Jam Printers", "code"=>"PJP", "id"=>1}

184

Глава 6. Работа с ActiveRecord

Возможность получить сразу весь хеш атрибутов бывает полезна, когда требуется перебрать их или передать весь набор другой функции.
Отметим, что хеш, который возвращает метод attributes, не является
ссылкой на внутреннюю структуру в объекте ActiveRecord. Это копия,
поэтому изменения не отразятся на объекте, от которого копия получена.
>>
=>
>>
=>
>>
=>

atts = first_client.attributes
{"name"=>"Paper Jam Printers", "code"=>"PJP", "id"=>1}
atts["name"] = "Def Jam Printers"
"Def Jam Printers"
first_client.attributes
{"name"=>"Paper Jam Printers", "code"=>"PJP", "id"=>1}

Для внесения групповых изменений в атрибуты объекта ActiveRecord
можно передать хеш методу attributes.

Доступ к атрибутам и манипулирование ими
до приведения типов
Адаптеры соединений в ActiveRecord извлекают результаты в виде
строк, а Rails при необходимости преобразует их в другие типы данных, исходя из типа колонки таблицы. Например, целые типы преобразуются в экземпляры класса Fixnum и т. д.
Даже при работе с новым экземпляром объекта ActiveRecord, кон­
структору которого были переданы строки, при попытке доступа к соответствующим атрибутам их значения будут приведены к нужному
типу.
Но иногда хочется прочитать (или изменить) непреобразованные значения атрибутов – это позволяют создать акцессоры _before_
type_cast, которые формируются в модели автоматически.
Пусть, например, требуется получать денежные суммы в виде строк,
введенных конечными пользователями. Если вы инкапсулировали такие значения в класс для представления денежных сумм (кстати, настоятельно рекомендую), придется иметь дело с докучливыми знаками
доллара и запятыми. В предположении, что в модели Timesheet определен атрибут rate типа :decimal, следующий код уберет ненужные символы, перед тем как выполнять приведение типа для операции сохранения:
class Timesheet < ActiveRecord::Base
before_save :fix_rate
def fix_rate
rate_before_type_cast.tr!('$,','')
end
end

CRUD: создание, чтение, обновление, удаление

185

Перезагрузка
Метод reload выполняет запрос к базе данных и переустанавливает атрибуты объекта ActiveRecord. Ему передается необязательный аргумент options, так что можно, например, написать record.reload(:lock =>
true), чтобы запись перечитывалась под защитой исключительной блокировки (см. раздел «Блокировка базы данных» ниже в этой главе).

Динамический поиск по атрибутам
Поскольку одна из самых распространенных операций в приложениях, работающих с базой данных, – простой поиск по одной или нескольким колонкам, в Rails есть эффективный способ решить эту задачу, не
прибегая к параметру conditions метода find. Работает он благодаря
применению имеющегося в Ruby обратного вызова method_missing, который выполняется, когда запрошенный метод еще не определен.
Имена методов динамического поиска начинаются с префиксов find_
by_ или find_all_by_, обозначающих, что вы хотите получить одно значение или массив соответственно. Семантика аналогична вызову метода find с параметром :first или :all.
>> City.find_by_name("Hackensack")
=> # "Hackensack", "latitude" =>
"40.8858330000", "id" => "15942", "longitude" => "-74.0438890000",
"state" => "NJ" }>
>> City.find_all_by_name("Atlanta").collect(&:state)
=> ["GA", "MI", "TX"]

Допускается также задавать в имени поискового метода несколько атрибутов, разделяя их союзом and, так что возможно имя Person.find_
by_user_name_and_password или даже Payment.find_by_purchaser_and_state_
and_country.
Достоинство динамических методов поиска в том, что запись получается короче и проще для восприятия. Вместо Person.find(:first, ["user_
name = ? AND password = ?", user_name, password]) попробуйте написать
просто Person.find_by_user_name_and_password(user_name, password):
>> City.find_by_name_and_state("Atlanta", "TX")
=> # "Atlanta", "latitude" =>
"33.1136110000", "id" => "25269", "longitude" => "-94.1641670000",
"state" => "TX"}>

Можно даже вызывать динамический метод с параметрами, как обычный метод. Payment.find_all_by_amount – не что иное, как Payment.find_all_
by_amount(amount, options). А полный интерфейс метода Person.find_by_user_name выглядит как Person.find_by_user_name(user_name, options). Поэтому
вызывают его так: Payment.find_all_by_amount(50, :order => "created_on").

186

Глава 6. Работа с ActiveRecord

Тот же самый динамический стиль можно применять для создания
объекта, если последний еще не существует. Такой динамический метод называется find_or_create_by_ и возвращает найденный объект, если он существует; в противном случае создает объект, а потом возвращает его. Если же вы хотите вернуть новую запись без предварительного сохранения, воспользуйтесь методом find_or_initialize_by_.

Специальные SQLзапросы
Метод класса find_by_sql принимает SQL-запрос на выборку и возвращает массив объектов ActiveRecord, соответствующих найденным
строкам. Вот набросок примера, но использовать его в реальных приложениях ни в коем случае нельзя:
>> Client.find_by_sql("select * from clients")
=> [#"Nile River, Co.",
"updated_at"=>"2007-06-04 22:24:27", "code"=>"NRC", "id"=>"1",
"created_at"=>"2007-06-04 22:24:27"}>, #"Amazon, Co.", "updated_at"=>"2007-06-04
22:26:22",
"code"=>"AMZ", "id"=>"2", "created_at"=>"2007-06-04 22:26:22"}>]

Еще и еще раз подчеркиваю: пользоваться методом find_by_sql следует, только когда без него не обойтись! Прежде всего таким способом вы
снижаете степень переносимости между различными СУБД – при использовании стандартных операций поиска, предоставляемых Active­
Record, Rails автоматически учитывает различия между СУБД.
Кроме того, в ActiveRecord уже встроена весьма развитая функциональность для абстрагирования предложений SELECT, и изобретать ее
заново было бы неразумно. Есть много случаев, когда кажется, что без
find_by_sql не обойтись, однако на самом деле это не так. Типичная ситуация – запрос с предикатом LIKE:
>> Client.find_by_sql("select * from clients where code like 'A%'")
=> [#"Amazon, Inc.", ...}>]

Но оказывается, что оператор LIKE легко включить в параметр conditions:
>> param = "A"
>> Client.find(:all, :conditions => ["code like ?", "#{param}%"])
=> [#"Amazon, Inc...}>] #
Правильно!

Rails незаметно для вас обезвреживает1 ваш SQL-запрос при условии,
что он параметризован. ActiveRecord выполняет SQL-запросы с помощью метода connection.select_all, затем обходит результирующий мас1

Обезвреживание предотвращает атаки внедрением SQL. Дополнительную
информацию о таких атаках применительно к Rails см. в статье по адресу
http://www.rorsecurity.info/2007/05/19/sql-injection/.

CRUD: создание, чтение, обновление, удаление

187

сив хешей и для каждой строки вызывает метод initialize. Так выглядел бы предыдущий запрос, будь он непараметризован:
>> param = "A"
>> Client.find(:all, :conditions => ["code like '#{param}%'"])
=> [#"Amazon, Inc...}>] #
Только не это!

Обратите внимание на отсутствующий знак вопроса, играющий роль
подставляемого символа. Никогда не забывайте, что интерполяция
поступивших от пользователя значений в любое предложение SQL –
крайне опасное дело! Подумайте, что произойдет, если злонамеренный
пользователь инициирует это небезопасное обращение к find с таким
значением в param:
"Amazon'; DELETE FROM users;'

Как это ни печально, очень немногие хорошо понимают, что такое
внедрение SQL. В данном случае лучшим другом вам будет Google.

Кэш запросов
По умолчанию Rails пытается оптимизировать производительность,
включая простой кэш запросов: хеши, хранящиеся в памяти текущего
потока, – по одному на каждое соединение с базой данных (в большинстве приложений Rails будет всего один такой хеш).
Если кэш запросов включен, то при каждом вызове find (или любой
другой операции выборки) результирующий набор сохраняется в хеше, причем в качестве ключа выступает предложение SQL. Если то же
самое предложение встретится еще раз, то для порождения нового набора объектов модели будет использован кэшированный результирующий набор без повторного обращения к базе данных.
Кэширование запросов можно включить вручную, обернув операции
в блок cache, как в следующем примере:
User.cache do
puts User.find(:first)
puts User.find(:first)
puts User.find(:first)
end

Заглянув в файл development.log, вы найдете такие записи:
Person Load (0.000821) SELECT * FROM people LIMIT 1
CACHE (0.000000) SELECT * FROM people LIMIT 1
CACHE (0.000000) SELECT * FROM people LIMIT 1

К базе данных было только одно обращение. Проведя такой же эксперимент на консоли без блока cache, вы увидите, что будут запротоколированы три разных события Person Load.

188

Глава 6. Работа с ActiveRecord

Операции сохранения и удаления приводят к очистке кэша, чтобы не
распространялись экземпляры с уже недействительным состоянием.
При необходимости вы можете вручную очистить кэш запросов, вызвав метод класса clear_query_cache.

Подключаемый модуль ActiveRecord Context
Рик Олсон вычленил из своего популярного приложения Lighthouse
этот подключаемый модуль, позволяющий инициализировать
кэш запросов набором объектов, который заведомо понадобится.
Это очень полезное дополнение к встроенной в ActiveRecord поддержке кэширования.
Дополнительную информацию см. на странице http://activereload.
net/2007/5/23/spend-less-time-in-thedatabase-and-more-timeoutdoors.

Протоколирование
В файле протокола отмечается, когда данные читались из кэша запросов, а не из базы. Поищите строки, начинающиеся со слова CACHE, а не
Model Load.
Place Load (0.000420) SELECT * FROM places WHERE (places.'id' =
15749)
CACHE (0.000000) SELECT * FROM places WHERE (places.'id' = 15749)
CACHE (0.000000) SELECT * FROM places WHERE (places.'id' = 15749)

Кэширование запросов по умолчанию в контроллерах
Из соображений производительности механизм кэширования запросов
ActiveRecord по умолчанию включается при обработке действий контроллеров. Модуль SqlCache, определенный в файле caching.rb библиотеки ActionController, примешан к классу ActionController::Base и обертывает метод perform_action с помощью alias_method_chain:
module SqlCache
def self.included(base) #:nodoc:
base.alias_method_chain :perform_action, :caching
end
def perform_action_with_caching
ActiveRecord::Base.cache do
perform_action_without_caching
end
end
end

CRUD: создание, чтение, обновление, удаление

189

Ограничения
Кэш запросов ActiveRecord намеренно сделан чрезвычайно простым.
Поскольку в качестве ключей хеша буквально используются предложения SQL, с помощью которых были выбраны данные, то невозможно
опознать различные вызовы find, отличающиеся формулировкой, но
семантически эквивалентные и дающие одинаковые результаты.
Например, предложения select foo from bar where id = 1 и select foo
from bar where id = 1 limit 1 считаются разными запросами и занимают
две записи в хеше. Подключаемый модуль active_record_context Рика
Олсона – пример более интеллектуальной реализации кэша, поскольку результаты индексируются первичными ключами, а не текстами
предложений SQL.

Обновление
Простейший способ манипулирования значениями атрибутов заключается в том, чтобы трактовать объект ActiveRecord как обычный объект Ruby, то есть выполнять присваивание напрямую с помощью метода myprop=(some_value).
Есть также целый ряд других способов обновления объектов Active­Re­
cord, о них и пойдет речь в этом разделе. Посмотрите, как используется
метод update класса ActiveRecord::Base:
class ProjectController < ApplicationController
def update
Project.update(params[:id], params[:project])
redirect_to :action=>'settings', :id => project.id
end
def mass_update
Project.update(params[:projects].keys, params[:projects].values])
redirect_to :action=>'index'
end
end

Первый вариант update принимает числовой идентификатор и хеш значений атрибутов, а второй – список идентификаторов и список значений. Второй вариант полезен при обработке формы, содержащей несколько допускающих обновление строк.
Метод класса update сначала вызывает процедуру проверки и не сохраняет запись, если проверка не проходит. Однако объект он возвращает
вне зависимости от того, проверены данные успешно или нет. Следовательно, если вы хотите узнать результат проверки, то должны после
обращения к update вызвать метод valid?:
class ProjectController < ApplicationController
def update
@project = Project.update(params[:id], params[:project])

190

Глава 6. Работа с ActiveRecord
if @project.valid? # а надо ли выполнять контроль еще раз?
redirect_to :action=>'settings', :id => project.id
else
render :action => 'edit'
end
end
end

Проблема в том, что в этом случае метод valid? вызывается дважды,
поскольку один раз его уже вызывал метод update. Быть может, более
правильно было бы воспользоваться методом экземпляра update_
attributes:
class ProjectController < ApplicationController
def update
@project = Project.find(params[:id]
if @project.update_attributes(params[:project])
redirect_to :action=>'settings', :id => project.id
else
render :action => 'edit'
end
end
end

И, конечно, если вы хоть немного программировали для Rails, то сразу
распознаете здесь идиому, поскольку она применяется в генерируемом
коде обстраивания (scaffolding). Метод update_attributes принимает
хеш со значениями атрибутов и возвращает true или false в зависимо­
сти от того, завершилась ли операция сохранения успешно или нет,
что, в свою очередь, определяется успешностью проверки.

Обновление с условием
В ActiveRecord есть еще один метод, полезный для обновления сразу
нескольких записей: update_all. Он тесно связан с предложением SQL
update…where. Метод update_all принимает два параметра: часть set предложения SQL и условия, включаемые в часть where. Возвращается количество обновленных записей1.
Мне кажется, что это один из методов, которые более уместны в контексте сценария, а не в методе контроллера, но у вас может быть иное
мнение. Вот пример, показывающий, как я передал бы ответственность
за все проекты Rails в системе новому менеджеру проектов:
Project.update_all("manager = 'Ron Campbell'", "technology =
'Rails'")
1

Библиотека Microsoft ADO не сообщает, сколько было обновлено записей,
поэтому метод update_all не работает с адаптером для SQL Server.

CRUD: создание, чтение, обновление, удаление

191

Обновление конкретного экземпляра
Самый простой способ обновить объект ActiveRecord состоит в том, чтобы изменить его атрибуты напрямую, а потом вызвать метод save. Стоит
отметить, что метод save либо вставляет запись в базу данных, либо –
если запись с таким первичным ключом уже есть – обновляет ее:
project = Project.find(1)
project.manager = 'Brett M.'
assert_equal true, project.save

Метод save возвращает true, если сохранение завершилось успешно,
и false в противном случае. Существует также метод save!, который
в случае ошибки возбуждает исключение. Каким из них пользоваться,
зависит от того, хотите ли вы обрабатывать ошибки немедленно или
поручить это какому-то другому методу выше по цепочке вызовов.
В общем-то, это вопрос стиля, хотя методы сохранения и обновления
без восклицательного знака, то есть возвращающие булево значение,
чаще используются в действиях контроллеров, где фигурируют в качестве условия, проверяемого в предложении if:
class StoryController < ApplicationController
def points
@story = Story.find(params[:id])
if @story.update_attribute(:points, params[:value])
render :text => "#{@story.name} updated"
else
render :text => "Error updating story points"
end
end
end

Обновление конкретных атрибутов
Методы экземпляра update_attribute и update_attributes принимают либо
одну пару ключ/значение, либо хеш атрибутов соответственно. В указанные атрибуты записываются переданные значения, и новые данные
сохраняются в базе – все в рамках одной операции.
Метод update_attribute обновляет единственный атрибут и сохраняет
запись. Обновления, выполняемые этим методом, не подвергаются
проверке! Другими словами, данный метод позволяет сохранить модель в базе данных, даже если состояние объекта некорректно. По заявлению разработчиков ядра Rails, это сделано намеренно. Внутри метод эквивалентен присваиванию model.attribute = some_value, за которым следует model.save(false).

192

Глава 6. Работа с ActiveRecord

Говорит Кортенэ…
Если в модели определены ассоциации, то ActiveRecord автоматически создает вспомогательные методы для массового присваивания. Иными словами, если в модели Project имеется ассоциация has_many :users, то появится метод записи атрибутов user_ids,
который будет вызван из update_attributes.
Это удобно, если вы обновляете ассоциации с помощью флажков в интерфейсе пользователя, поскольку достаточно назвать
флажки project[user_ids][] – и все остальное Rails проделает самостоятельно.
В некоторых случаях небезопасно разрешать пользователю устанавливать ассоциации таким способом. Подумайте, не стоит ли
прибегнуть к методу attr_accessible, чтобы предотвратить массовое присваивание, когда есть шанс, что какой-нибудь злоумышленник попробует воспользоваться вашим приложением некорректно.
Напротив, метод update_attributes выполняет все проверки, поэтому
часто используется в действиях по обновлению, где в качестве параметра ему передается хеш params, содержащий новые значения.

Вспомогательные методы обновления
Rails предлагает ряд вспомогательных методов обновления вида increment (увеличить на единицу), decrement (уменьшить на единицу) и toggle
(изменить состояние на противоположное), которые выполняют соответствующие действия для числовых и булевых атрибутов. У каждого
из них имеется вариант с восклицательным знаком (например, toggle!),
который после модификации атрибута вызывает еще и метод save.

Контроль доступа к атрибутам
Конструкторы и методы обновления, принимающие хеши для выполнения массового присваивания значений атрибутам, уязвимы для хакерских атак, если используются в сочетании с хешами параметров,
доступными в методах контроллера.
Если в классе ActiveRecord есть атрибуты, которые вы хотели бы защитить от случайного или массового присваивания, воспользуйтесь
следующими методами класса, позволяющими контролировать доступ
к атрибутам.
Метод attr_accessible принимает список атрибутов, для которых разрешено массовое присваивание. Это наиболее осторожный способ защиты от массового присваивания.

CRUD: создание, чтение, обновление, удаление

193

Если же вы предпочитаете сначала разрешить все и вводить ограничения по мере необходимости, то к вашим услугам метод attr_protected.
Атрибуты, переданные этому методу, будут защищены от массового
присваивания. Попытка присвоить им значения просто игнорируется.
Чтобы изменить значение такого атрибута, необходимо вызвать метод
прямого присваивания, как показано в примере ниже:
class Customer < ActiveRecord::Base
attr_protected :credit_rating
end
customer = Customer.new(:name => "Abe", :credit_rating => "Excellent")
customer.credit_rating # => nil
customer.attributes = { "credit_rating" => "Excellent" }
customer.credit_rating # => nil
# а теперь разрешенный способ задать ставку кредита
customer.credit_rating = "Average"
customer.credit_rating # => "Average"

Удаление и уничтожение
Наконец, есть два способа удалить запись из базы данных. Если уже
имеется экземпляр модели, можно уничтожить его методом destroy:
>> bad_timesheet = Timesheet.find(1)
>> bad_timesheet.destroy
=> #"2006-11-21
05:40:27", "id"=>"1", "user_id"=>"1", "submitted"=>nil, "created_at"=>
"2006-11-21 05:40:27"}>

Метод destroy удаляет данные из базы и замораживает экземпляр (делает доступным только для чтения), чтобы нельзя было сохранить его
еще раз:
>> bad_timesheet.save
TypeError: can't modify frozen hash
from activerecord/lib/active_record/base.rb:1965:in '[]='

Альтернативно можно вызывать методы destroy и delete как методы
класса, передавая один или несколько идентификаторов записей, подлежащих удалению. Оба варианта принимают либо единственный
идентификатор, либо массив:
Timesheet.delete(1)
Timesheet.destroy([2, 3])

Схема именования может показаться несогласованной, однако это не
так. Метод delete непосредственно выполняет предложение SQL, не загружая экземпляры предварительно (так быстрее). Метод destroy сначала загружает экземпляр объекта ActiveRecord, а потом вызывает

194

Глава 6. Работа с ActiveRecord

для него метод экземпляра destroy. Семантическое различие трудноуловимо, но становится понятным, когда задаются обратные вызовы
before_destroy или возникают зависимые ассоциации, то есть дочерние
объекты, которых нужно автоматически удалить вместе с родителем.

Блокировка базы данных
Термином блокировка обозначается техника, позволяющая предотвратить обновление одних и тех же записей несколькими одновременно
работающими пользователями. При загрузке строк таблицы в модель
ActiveRecord по умолчанию вообще не применяет блокировку. Если
в некотором приложении Rails в любой момент времени обновлять данные может только один пользователь, то беспокоиться о блокировках
не нужно.
Если же есть шанс, что чтение и обновление данных могут одновременно выполнять несколько пользователей, то вы обязаны озаботиться
конкурентностью. Спросите себя, какие коллизии или гонки (race
conditions) могут иметь место, если два пользователя попытаются обновить модель в один и тот же момент?
К учету конкурентности в приложениях, работающих с базой данных,
есть несколько подходов. В ActiveRecord встроена поддержка двух из
них: оптимистической и пессимистической блокировки. Есть и другие варианты, например блокирование таблиц целиком. У каждого
подхода много сильных и слабых сторон, поэтому для максимально надежной работы приложения имеет смысл их комбинировать.

Оптимистическая блокировка
Оптимистическая стратегия блокировки заключается в обнаружении
и разрешении конфликтов по мере их возникновения. Обычно ее рекомендуют применять в тех случаях, когда коллизии случаются нечасто.
При оптимистической блокировке записи базы данных вообще не блокируются, так что название только сбивает с толку.
Оптимистическая блокировка – довольно распространенная стратегия, поскольку многие приложения проектируются так, что любой
пользователь изменяет лишь данные, которые концептуально принадлежат только ему. Поэтому конкуренция за обновление одной и той
же записи маловероятна. Идея оптимистической блокировки в том,
что, коль скоро коллизии редки, то обрабатывать их нужно лишь в случае реального возникновения.
Если вы контролируете схему базы данных, то реализовать оптимистическую блокировку совсем просто. Достаточно добавить в таблицу целочисленную колонку с именем lock_version и значением по умолчанию 0:

Блокировка базы данных

195

class AddLockVersionToTimesheets < ActiveRecord::Migration
def self.up
add_column :timesheets, :lock_version, :integer, :default => 0
end
def self.down
remove_column :timesheets, :lock_version
end
end

Само наличие такой колонки изменяет поведение ActiveRecord. Если
некоторая запись загружена в два экземпляра модели и сохранена
с разными значениями атрибутов, то успешно завершится обновление
первого экземпляра, а при обновлении второго будет возбуждено исключение ActiveRecord::StaleObjectError.
Для иллюстрации оптимистической блокировки напишем простой автономный тест:
class TimesheetTest < Test::Unit::TestCase
fixtures :timesheets, :users
def test_optimistic_locking_behavior
first_instance = Timesheet.find(1)
second_instance = Timesheet.find(1)
first_instance.approver = users(:approver)
second_instance.approver = users(:approver2)
assert first_instance.save, "Успешно сохранен первый экземпляр"
assert_raises ActiveRecord::StaleObjectError do
second_instance.save
end
end
end

Тест проходит, потому что при вызове save для второго экземпляра
мы ожидаем исключения ActiveRecord::StaleObjectError. Отметим,
что метод save (без восклицательного знака) возвращает false и не возбуждает исключений, если сохранение не выполнено из-за ошибки
контроля данных. Другие проблемы, например блокировка записи,
могут приводить к исключению. Если вы хотите, чтобы колонка, содержащая номер версии, называлась не lock_version, а как-то иначе,
измените эту настройку с помощью метода set_locking_column. Чтобы
это изменение действовало глобально, добавьте в файл environment.rb
такую строку:
ActiveRecord::Base.set_locking_column 'alternate_lock_version'

Как и другие настройки ActiveRecord, эту можно задать на уровне
модели, если включить в класс модели такое объявление:

196

Глава 6. Работа с ActiveRecord
class Timesheet < ActiveRecord::Base
set_locking_column 'alternate_lock_version'
end

Обработка исключения StaleObjectError
Добавив оптимистическую блокировку, вы, конечно, не захотите остановиться на этом, поскольку иначе пользователь, оказавшийся проигравшей стороной в разрешении коллизии, просто увидит на экране сообщение об ошибке. Надо постараться обработать исключение StaleObjectError с наименьшими потерями.
Если обновляемые данные очень важны, вы, возможно, захотите по­
тратить время на то, чтобы смастерить дружелюбную к пользователю
систему, каким-то образом сохраняющую изменения, которые тот пытался внести. Если же данные легко ввести заново, то как минимум сообщите пользователю, что обновление не состоялось. Ниже приведен
код контроллера, в котором реализован такой подход:
def update
begin
@timesheet = Timesheet.find(params[:id])
@timesheet.update_attributes(params[:timesheet])
# куда-нибудь переадресовать
rescue ActiveRecord::StaleObjectError
flash[:error] = "Табель был модифицирован, пока вы его редактировали."
redirect_to :action => 'edit', :id => @timesheet
end
end

У оптимистической блокировки есть ряд преимуществ. Для нее не
нужны специальные механизмы СУБД, и реализуется она сравнительно просто. Как видно из примера, для обработки исключения Stale­
ObjectError потребовалось очень немного кода.
Недостатки связаны, главным образом, с тем, что операции обновления занимают чуть больше времени, так как необходимо проверить
версию блокировки. Кроме того, пользователи могут быть недовольны,
так как узнают об ошибке только после отправки данных, терять которые было бы крайне нежелательно.

Пессимистическая блокировка
Для пессимистической блокировки требуется специальная поддержка
со стороны СУБД (впрочем, в наиболее распространенных СУБД она
есть). На время операции обновления блокируются некоторые строки
в таблицах. Это не дает другим пользователям читать записи, которые
будут обновлены, и тем самым предотвращает потенциальную возможность работы с устаревшими данными.
Пессимистическая блокировка появилась в Rails относительно недавно
и работает в сочетании с транзакциями, как показано в примере ниже:

Дополнительные средства поиска

197

Timesheet.transaction do
t = Timesheet.find(1, :lock=> true)
t.approved = true
t.save!
end

Можно также вызвать метод lock! для существующего экземпляра модели, а он уже внутри вызовет reload(:lock => true). Вряд ли стоит это
делать после изменения атрибутов экземпляра, поскольку при перезагрузке изменения будут потеряны.
Пессимистическая блокировка производится на уровне базы данных.
В сгенерированное предложение SELECT ActiveRecord добавит модификатор FOR UPDATE (или его аналог), в результате чего всем остальным соединениям будет заблокирован доступ к строкам, возвращенным этим предложением. Блокировка снимается после фиксации
транзакции. Теоретически возможны ситуации (скажем, Rails «грохается» в середине транзакции?!), когда блокировка не будет снята
до тех пор, пока соединение не завершится или не будет закрыто по
тайм-ауту.

Замечание
Веб-приложения лучше масштабируются при оптимистической блокировке, поскольку, как мы уже говорили, в этом случае вообще никакие записи не блокируются. Однако приходится добавлять логику обработки ошибок. Пессимистическую блокировку реализовать несколько проще, но при этом могут возникать ситуации, когда один процесс
Rails вынужден ждать, пока остальные освободят блокировки, то есть
не сможет обслуживать никакие поступающие запросы. Напомним,
что процессы Rails однопоточные.
На мой взгляд, пессимистическая блокировка не должна быть такой
опасной, как на некоторых других платформах, поскольку Rails не дер­
жит транзакции дольше, чем на протяжении обработки одного HTTPзапроса. Собственно, я почти уверен, что в архитектуре, где ничего не
разделяется, такое вообще невозможно.
Опасаться нужно ситуации, когда многие пользователи конкурируют
за доступ к одной записи, для обновления которой нужно ощутимое
время. Лучше всего, чтобы транзакции с пессимистической блокировкой были короткими и исполнялись быстро.

Дополнительные средства поиска
При первом знакомстве с методом find мы рассматривали только поиск
по первичному ключу и параметры :first и :all. Но этим доступные
возможности отнюдь не исчерпываются.

198

Глава 6. Работа с ActiveRecord

Условия
Очень часто возникает необходимость отфильтровать результирующий
набор, возвращенный операцией поиска (которая сводится к предложению SQL SELECT), добавив условия (в часть WHERE). ActiveRecord
позволяет сделать это различными способами с помощью хеша параметров, который может быть передан методу find.
Условия задаются в параметре :conditions в виде строки, массива или
хеша, представляющего часть WHERE предложения SQL. Массив следует
использовать, когда исходные данные поступают из внешнего мира,
например из веб-формы, и перед записью в базу должны быть обезврежены. Небезопасные данные, поступающие извне, называются подо­
зрительными (tainted).
Условия можно задавать в виде простой строки, когда данные не вызывают подозрений. Наконец, хеш работает примерно так же, как массив, с тем отличием, что допустимо только сравнение на равенство. Если это вас устраивает (то есть условие не содержит, например, оператора LIKE), то я рекомендую пользоваться хешем, так как это безопасный
и, пожалуй, наиболее удобный для восприятия способ.
В документации по Rails API есть много примеров, иллюстрирующих
параметр :conditions:
class User < ActiveRecord::Base
def self.authenticate_unsafely(login, password)
find(:first,
:conditions => "login='#{login}' AND password='#{password}'")
end
def self.authenticate_safely(login, password)
find(:first,
:conditions => ["login= ? AND password= ?", login, password])
end
def self.authenticate_safely_simply(login, password)
find(:first,
:conditions => {:login => login, :password => password})
end
end

Метод authenticate_unsafely вставляет параметры прямо в запрос и потому уязвим к атакам с внедрением SQL, если имя и пароль пользователя берутся непосредственно из HTTP-запроса. Злоумышленник может просто включить SQL-запрос в строку, которая должна была бы
содержать имя или пароль.
Методы authenticate_safely и authenticate_safely_simply обезвреживают имя и пароль перед вставкой в текст запроса, гарантируя тем самым, что противник не сможет экранировать запрос и войти в систему
в обход аутентификации (или сделать еще что-нибудь похуже).

Дополнительные средства поиска

199

При использовании нескольких полей в условии бывает трудно понять,
к чему относится, скажем, четвертый или пятый вопросительный знак.
В таких случаях можно прибегнуть к именованным связанным переменным. Для этого нужно заменить вопросительные знаки символами,
а в хеше задать значения для соответствующих символам ключей.
В документации есть хороший пример на эту тему (для краткости мы
его немного изменили):
Company.find(:first, [
" name = :name AND division = :div AND created_at > :date",
{:name => "37signals", :div => "First", :date => '2005-01-01' }
])

Во время краткого обсуждения последней формы представления в IRCчате Робби Рассел (Robby Russell) предложил мне такой фрагмент:
:conditions => ['subject LIKE :foo OR body LIKE :foo', {:foo =>
'woah'}]
Иными словами, при использовании именованных связанных переменных (вместо вопросительных знаков) к одной и той же переменной
можно привязываться несколько раз. Здорово!
Простые условия в виде хеша также встречаются часто и бывают полезны:
:conditions => {:login => login, :password => password})

В результате генерируются условия, содержащие только сравнение на
равенство, и оператор SQL AND. Если вам нужно что-то, кроме AND, придется воспользоваться другими формами.

Булевы условия
Очень важно проявлять осторожность при задании условий, содержащих булевы значения. Различные СУБД по-разному представляют их
в колонках. В некоторых имеется встроенный булев тип данных, а в других применяется тот или иной символ, часто «1» / «0» или «T» / «F»
(и даже «Y» / «N»).
Rails незаметно для вас производит необходимые преобразования, если условия заданы в виде массива или хеша, а в качестве значения параметра задана булева величина в смысле Ruby:
Timesheet.find(:all, :conditions => ['submitted=?', true])

Упорядочение результатов поиска
Значением параметра :order является фрагмент SQL, определяющий
сортировку по колонкам:
Timesheet.find(:all, :order => 'created_at desc')

В SQL по умолчанию предполагается сортировка по возрастанию, если
спецификация asc/desc опущена.

200

Глава 6. Работа с ActiveRecord

Говорит Уилсон…
Стандарт SQL не определяет никакой сортировки, если в запросе
отсутствует часть order by. Некоторых разработчиков это застает
врасплох, поскольку они считают, что по умолчанию предполагается сортировка ORDER BY id ASС.

Сортировка в случайном порядке
Rails не проверяет значение параметра :order, следовательно, вы можете передать любую строку, которую понимает СУБД, а не только пары
колонка/порядок сортировки. Например, это может быть полезно,
когда нужно выбрать случайную запись:
# MySQL
Timesheet.find(:first, :order => 'RAND()')
# Postgres
Timesheet.find(:first, :order => 'RANDOM()')
# Microsoft SQL Server
Timesheet.find(:first, :order => 'NEWID()')
# Oracle
Timesheet.find(:first, :order => 'dbms_random.value')

Отметим, что сортировка больших наборов данных в случайном порядке может работать очень медленно, особенно в случае MySQL.

Параметры limit и offset
Значением параметра :limit должно быть целое число, указывающее
максимальное число отбираемых запросом строк. Параметр :offset
указывает номер первой из возвращаемых строк результирующего набора, при этом самая первая строка имеет номер 1. В сочетании эти два
параметра используются для разбиения результирующего набора на
страницы.
Например, следующее обращение к методу find вернет вторую страницу списка табелей, содержащую 10 записей:
Timesheet.find(:all, :limit => 10, :offset => 11)

В зависимости от особенностей модели данных в приложении может
иметь смысл всегда налагать некоторое ограничение на максимальное
количество объектов ActiveRecord, отбираемых одним запросом. Если
позволить пользователю выполнять запросы, загружающие в Rails тысячи объектов ActiveRecord, то крах неминуем.

Дополнительные средства поиска

201

Параметр select
По умолчанию параметр :select принимает значение «*», то есть соответствует предложению SELECT * FROM. Но это можно изменить, если, например, вы хотите выполнить соединение, но не включать в результат
колонки, по которым соединение производилось. Или включить в результирующий набор вычисляемые колонки:
>> b = BillableWeek.find(:first, :select => "monday_hours +
tuesday_hours + wednesday_hours as three_day_total")
=> #"24"}>

Применяя параметр :select, как показано в предыдущем примере,
имейте в виду, что колонки, не заданные в запросе, – неважно, явно
или с помощью «*», – не попадают в результирующие объекты! Так,
если в том же примере обратиться к атрибуту monday_hours объекта b,
результат будет неожиданным:
>> b.monday_hours
NoMethodError: undefined method 'monday_hours' for
#"24"}>
from activerecord/lib/active_record/base.rb:1850:in
'method_missing'
from (irb):38

Чтобы получить сами колонки, а также вычисляемую колонку, добавьте «*» в параметр :select:
:select => '*, monday_hours + tuesday_hours + wednesday_hours as
three_day_total'

Параметр from
Параметр :from определяет часть генерируемого предложения SQL,
в которой перечисляются имена таблиц. Если необходимо указать дополнительные таблицы для соединения или обратиться к представлению базы данных, можете задать значение явно.
Следующий пример взят из приложения, в котором используются признаки (теги):
def find_tagged_with(list)
find(:all,
:select => "#{table_name}.*",
:from => "#{table_name}, tags, taggings",
:conditions =>
["#{table_name}.#{primary_key}=taggings.taggable_id
and taggings.taggable_type = ?
and taggings.tag_id = tags.id and tags.name IN (?)",
name, Tag.parse(list)])
end

202

Глава 6. Работа с ActiveRecord

Если вам интересно, почему вместо непосредственного задания имени
таблицы интерполируется переменная table_name, то скажу, что этот код
подмешан в конечный класс из модулей Ruby. Данная тема подробно обсуждается в главе 9 «Дополнительные возможности ActiveRecord».

Группировка
Параметр :group задает имя колонки, по которой следует сгруппировать результаты, и отображается на часть GROUP BY предложения SQL.
Вообще говоря, параметр :group используется в сочетании с :select, так
как при вычислении в SELECT агрегатных функций SQL требует, чтобы
все колонки, по которым агрегирование не производится, были перечислены в части GROUP BY.
>> users = Account.find(:all,
:select => 'name, SUM(cash) as money',
:group => 'name')
=> [#"Joe", "money"=>"3500"}>,
#"Jane", "money"=>"9245"}>]

Имейте в виду, что эти дополнительные колонки возвращаются в виде
строк – ActiveRecord не пытается выполнить для них приведение типов. Для преобразования в числовые типы вы должны явно обратиться
к методу to_i или to_f.
>> users.first.money > 1_000_000
ArgumentError: comparison of String with Fixnum failed
from (irb):8:in '>'

Параметры блокировки
Задание параметра :lock => true для операции поиска в контексте
транзакции устанавливает исключительную блокировку на отбираемые строки. Эта тема рассматривалась выше в разделе «Блокировка
базы данных».

Соединение и включение ассоциаций
Параметр :joins полезен, когда вы выполняете группировку (GROUP BY)
и агрегирование данных из других таблиц, но не хотите загружать сами ассоциированные объекты.
Buyer.find(:all,
:select => 'buyers.id, count(carts.id) as cart_count',
:joins => 'left join carts on carts.buyer_id=buyers.id',
:group => 'buyers.id')

Однако чаще всего параметры :joins и :include применяются для попутной выборки (eager-fetch) дополнительных объектов в одном предложении SELECT. Эту тему мы рассмотрим в главе 7.

Соединение с несколькими базами данных в разных моделях

203

Параметр readonly
Если задать параметр :readonly => true, то все возвращенные объекты
помечаются как доступные только для чтения. Изменить их атрибуты
вы можете, а сохранить в базе – нет.
>> c = Comment.find(:first, :readonly => true)
=> #
>> c.body = "Keep it clean!"
=> "Keep it clean!"
>> c.save
ActiveRecord::ReadOnlyRecord: ActiveRecord::ReadOnlyRecord
from /vendor/rails/activerecord/lib/active_record/base.rb:1958

Соединение с несколькими базами данных
в разных моделях
Обычно для создания соединения применяется метод ActiveRecord::Base.
establish_connection, а для его получения – метод ActiveRecord::Base.
connection. Все производные от ActiveRecord::Base классы будут работать по этому соединению. Но что если в каких-то моделях необходимо
воспользоваться другим соединением? ActiveRecord позволяет задавать соединение на уровне класса.
Предположим, что имеется подкласс ActiveRecord::Base с именем Le­
gacyProject, для которого данные хранятся не в той же базе, что для
всего приложения Rails, а в какой-то другой. Для начала опишите
свойства этой базы данных в отдельном разделе файла database.yml. Затем вызовите метод LegacyProject.establish_connection, чтобы для класса LegacyProject и всех его подклассов использовалось альтернативное
соединение.
Кстати, чтобы этот пример работал, необходимо выполнить в контек­
сте класса предложение self.abstract_class = true. В противном случае
Rails будет считать, что в подклассах LegacyProject используется наследование с одной таблицей (single-table inheritance – STI), о котором
речь пойдет в главе 9.
class LegacyProject < ActiveRecord::Base
establish_connection :legacy_database
self.abstract_class = true
...
end

Метод establish_connection принимает строку (или символ), указывающую на конфигурационный раздел в файле database.yml. Можно вместо
этого передать параметры в хеше-литерале, хотя такое включение конфигурационных данных прямо в модель вместо database.yml может
лишь привести к ненужной путанице.

204

Глава 6. Работа с ActiveRecord
class TempProject < ActiveRecord::Base
establish_connection(:adapter => 'sqlite3', :database =>
':memory:')
...
end

Rails хранит соединения с базами данных в пуле соединений внутри
экземпляра класса ActiveRecord::Base. Пул соединений – это просто
объект Hash, индексированный классами ActiveRecord. Когда во время
выполнения возникает необходимость установить соединение, метод
retrieve_connection просматривает иерархию классов, пока не найдет
подходящее соединение.

Прямое использование соединений
с базой данных
Есть возможность напрямую использовать соединения ActiveRecord
с базой данных. Иногда это полезно при написании специализированных сценариев или для тестирования на скорую руку. Доступ к соединению дает атрибут connection класса ActiveRecord. Если во всех ваших
моделях используется одно и то же соединение, получайте этот атрибут от класса ActiveRecord::Base.
Самая простая операция, которую можно выполнить при наличии соединения, – вызов метода execute из модуля DatabaseStatements (подробно рассматривается в следующем разделе). Например, в листинге 6.2
показан метод, который одно за другим выполняет предложения SQL,
записанные в некотором файле.
Листинг 6.2. Поочередное выполнение SQL-команд из файла на одном
соединении ActiveRecord
def execute_sql_file(path)
File.read(path).split(';').each do |sql|
begin
ActiveRecord::Base.connection.execute(#{sql}\n") unless
sql.blank?
rescue ActiveRecord::StatementInvalid
$stderr.puts "предупреждение: #{$!}"
end
end
end

Модуль DatabaseStatements
Модуль ActiveRecord::ConnectionAdapters::DatabaseStatements примешивает к объекту соединения ряд полезных методов, позволяющих работать с базой данных напрямую, не используя модели ActiveRecord.

Прямое использование соединений с базой данных

205

Я сознательно не включил в рассмотрение некоторые методы из этого
модуля (например, add_limit! и add_lock), поскольку они нужны самой
среде Rails для динамического построения SQL-предложений, и я не
думаю, что разработчикам приложений от них много пользы.
begin_db_transaction()

Вручную начинает транзакцию в базе данных (и отключает принятый
в ActiveRecord по умолчанию механизм автоматической фиксации).
commit_db_transaction()

Фиксирует транзакцию (и снова включает механизм автоматической
фиксации).
delete(sql_statement)

Выполняет заданное SQL-предложение DELETE и возвращает количе­ство удаленных строк.
execute(sql_statement)

Выполняет заданное SQL-предложение в контексте текущего соединения. В модуле DatabaseStatements этот метод является абстрактным
и переопределяется в реализации адаптера к конкретной СУБД. Поэтому тип возвращаемого объекта, представляющего результирующий
набор, зависит от использованного адаптера.
insert(sql_statement)

Выполняет SQL-предложение INSERT и возвращает значение последнего
автоматически сгенерированного идентификатора для той таблицы,
в которую произведена вставка.
reset_sequence!(table, column, sequence = nil)

Используется только для Oracle и Postgres; записывает с поименованную последовательность максимальное значение, найденное в колонке
column таблицы table.
rollback_db_transaction()

Откатывает текущую транзакцию (и включает механизм автоматической фиксации). Вызывается автоматически, если блок транзакции
возбуждает исключение, или возвращает false.
select_all(sql_statement)

Возвращает массив хешей, в которых ключами служат имена колонок,
а значениями – прочитанные из них значения.
ActiveRecord::Base.connection.select_all("select name from businesses
order by rand() limit 5")
=> [{"name"=>"Hopkins Painting"}, {"name"=>"Whelan & Scherr"},
{"name"=>"American Top Security Svc"}, {"name"=>"Life Style Homes"},
{"name"=>"378 Liquor Wine & Beer"}]
select_one(sql_statement)

206

Глава 6. Работа с ActiveRecord

Аналогичен select_all, но возвращает только первую строку результирующего набора в виде объекта Hash, в котором ключами служат имена колонок, а значениями – прочитанные из них значения. Отметим,
что этот метод автоматически не добавляет в заданное вами SQL-предложение модификатор limit, поэтому, если набор данных велик, не забудьте включить его самостоятельно.
>> ActiveRecord::Base.connection.select_one("select name from
businesses
order by rand() limit 1")
=> {"name"=>"New York New York Salon"}
select_value(sql_statement)

Работает, как select_one, но возвращает единственное значение: то, что
находится в первой колонке первой строки результирующего набора.
>> ActiveRecord::Base.connection.select_value("select * from
businesses
order by rand() limit 1")
=> "Cimino's Pizza"
select_values(sql_statement)

Работает, как select_value, но возвращает массив значений из первой
колонки каждой строки результирующего набора.
>> ActiveRecord::Base.connection.select_values("select * from
businesses
order by rand() limit 5")
=> ["Ottersberg Christine E Dds", "Bally Total Fitness", "Behboodikah,
Mahnaz Md", "Preferred Personnel Solutions", "Thoroughbred Carpets"]
update(sql_statement)

Выполняет заданное SQL-предложение UPDATE и возвращает количество
измененных строк. В этом отношении не отличается от метода delete.

Другие методы объекта connection
Полный перечень методов объекта connection, возвращаемого экзем­
пляром адаптера с конкретной СУБД, довольно длинный. В реализациях большинства адаптеров Rails определены специализированные варианты этих методов, и это разумно, так как все СУБД обрабатывают
SQL-запросы немного по-разному, а различия между синтаксисом
нестандартных команд, например извлечения метаданных, очень велики.
Заглянув в файл abstract_adapter.rb, вы найдете реализации всех методов, предлагаемые по умолчанию:
...

Прямое использование соединений с базой данных

207

# Возвращает понятное человеку имя адаптера. Записывайте имя в разных
# регистрах; кто захочет, сможет воспользоваться методом downcase.
def adapter_name
'Abstract'
end
# Поддерживает ли этот адаптер миграции? Зависит от СУБД, поэтому
# абстрактный адаптер всегда возвращает +false+.
def supports_migrations?
false
end
# Поддерживает ли этот адаптер использование DISTINCT в COUNT? +true+
# для всех адаптеров, кроме sqlite.
def supports_count_distinct?
true
end
...

В следующих описаниях и примерах я обращаюсь к объекту соединения для приложения time_and_expenses с консоли Rails, а ссылка на
объект connection для удобства присвоена переменной conn.
active?

Показывает, является ли соединение активным и готовым для выполнения запросов.
adapter_name

Возвращает понятное человеку имя адаптера:
>> conn.adapter_name
=> "SQLite"

disconnect! и reconnect!
Закрывает активное соединение или закрывает и открывает вместо него новое соответственно.
raw_connection

Предоставляет доступ к настоящему соединению с базой данных. Полезен, когда нужно выполнить нестандартное предложение или воспользоваться теми средствами реализованного в Ruby драйвера базы
данных, которые ActiveRecord не раскрывает (при попытке написать
пример для этого метода я с легкостью «повалил» консоль Rails – исключения, возникающие при работе с raw_connection, практически не
обрабатываются).
supports_count_distinct?

Показывает, поддерживает ли адаптер использование DISTINCT в агрегатной функции COUNT. Это так (возвращается true) для всех адаптеров,

208

Глава 6. Работа с ActiveRecord

кроме SQLite, а для этой СУБД приходится искать обходные пути выполнения подобных запросов.
supports_migrations?

Показывает, поддерживает ли адаптер миграции.
tables

Возвращает список всех таблиц, определенных в схеме базы данных.
Включены также таблицы, которые обычно не раскрываются с помощью моделей ActiveRecord, в частности schema_info и sessions.
>> conn.tables
=> ["schema_info", "users", "timesheets", "expense_reports",
"billable_weeks", "clients", "billing_codes", "sessions"]
verify!(timeout)

Отложенная проверка соединения; метод active? Вызывается, только
если он не вызывался в течение timeout секунд.

Другие конфигурационные параметры
Помимо параметров, говорящих ActiveRecord, как обрабатывать имена таблиц и первичных ключей, существует еще ряд настроек, управляющих различными функциями. Все они задаются в файле config/
environment.rb.
Параметр ActiveRecord::Base.colorize_logging говорит Rails, нужно ли
использовать ANSI-коды для раскрашивания сообщений, записываемых в протокол адаптером соединения ActiveRecord. Раскраска (доступная всюду, кроме Windows), заметно упрощает восприятие протоколов, но, если вы пользуетесь такими программами, как syslog, могут
возникнуть проблемы. По умолчанию равен true. Измените на false,
если просматриваете протоколы в программах, не понимающих ANSIкоды цветов.
Вот фрагмент протокола с ANSI-кодами:
^[[4;36;1mSQL (0.000000)^[[0m ^[[0;1mMysql::Error: Unknown table
'expense_reports': DROP TABLE expense_reports^[[0m
^[[4;35;1mSQL (0.003266)^[[0m ^[[0mCREATE TABLE expense_reports
('id'
int(11) DEFAULT NULL auto_increment PRIMARY KEY, 'user_id' int(11))
ENGINE=InnoDB^[[0m

ActiveRecord::Base.default_timezone говорит Rails, надо ли использовать
Time.local (значение :local) или Time.utc (значение :utc) при интерпретации даты и времени, полученных из базы данных. По умолчанию
:local.

209

Заключение

Говорит Уилсон…
Почти никто из моих знакомых не знает, как просматривать
раскрашенные протоколы в программах постраничного вывода.
В случае программы less флаг -R включает режим вывода на экран «необработанных» управляющих символов.

ActiveRecord::Base.allow_concurrency определяет, надо ли создавать соединение с базой данных в каждом потоке или можно использовать одно соединение для всех потоков. По умолчанию равен false и, принимая во внимание количество предупреждений и страшных историй1,
сопровождающих любое упоминание этого параметра в Сети, будет разумно не трогать его. Известно, что при задании true количество соединений с базой резко возрастает.
ActiveRecord::Base.generate_read_methods определяет, нужно ли пытаться ускорить доступ за счет генерации оптимизированных методов чтения, чтобы избежать накладных обращений к методу method_missing
при доступе к атрибутам по имени. По умолчанию равен true.
ActiveRecord::Base.schema_format задает формат вывода схемы базы данных для некоторых заданий rake. Значение :sql означает, что схема
выводится в виде последовательности SQL-предложений, которые могут зависеть от конкретной СУБД. Помните о возможных несовместимостях при выводе в этом режиме, когда работаете с разными базами
данных на этапе разработки и тестирования.
По умолчанию принимается значение :ruby, и тогда схема выводится
в виде файла в формате ActiveRecord::Schema, который можно загрузить
в любую базу данных, поддерживающую миграции.

Заключение
В этой главе мы рассмотрели основы работы с ActiveRecord, включенным в Ruby on Rails для создания классов моделей, привязанных к базе данных. Мы узнали, что философия ActiveRecord основана на примате соглашения над конфигурацией, который является важнейшим
принципом пути Rails. Но в то же время можно вручную задать параметры, которые отменяют действующие соглашения.
1

Читайте сообщение Зеда Шоу в списке рассылки о сервере Mongrel по адресу
http://permalink.gmane.org/gmane.comp.lang.ruby.mongrel.general/245, в котором объясняются опасности, связанные с параметром allow_concurrency.

210

Глава 6. Работа с ActiveRecord

Мы также познакомились с методами класса ActiveRecord::Base, которому наследуют все сохраняемые модели в Rails. Этот класс включает все
необходимое для выполнения основных CRUD-операций: создания,
чтения, обновления и удаления. Наконец, мы рассмотрели, как при необходимости можно работать с объектами соединений ActiveRecord напрямую. В следующей главе мы продолжим изучение ActiveRecord
и поговорим о том, как объекты во взаимосвязанных моделях могут
взаимодействовать посредством ассоциаций.

7
Ассоциации в ActiveRecord
Если вы можете что-то материализовать, создать нечто,
воплощающее концепцию, то затем это позволит вам
работать с абстрактной идеей более эффективно.
Именно так обстоит дело с конструкцией has_many :through
Джош Сассер

Ассоциации в ActiveRecord позволяют декларативно выражать отношения между классами моделей. Мощь и понятный стиль Associations
API немало способствует тому, что с Rails так приятно работать.
В этой главе мы рассмотрим различные виды ассоциаций ActiveRecord,
сделав особый упор на то, когда какие ассоциации применять и как
можно их настраивать. Мы также поговорим о классах, дающих доступ к самим отношениям.

Иерархия ассоциаций
Как правило, ассоциации выглядят как методы объектов моделей
ActiveRecord. Например, метод timesheets может представлять табели,
ассоциированные с данным объектом user.
>> user.timesheets

Однако может быть непонятно, объекты какого типа возвращают такие методы. Связано это с тем, что такие объекты маскируются под
обычные объекты или массивы Ruby (в зависимости от типа конкрет-

212

Глава 7. Ассоциации в ActiveRecord

ной ассоциации). В показанном выше фрагменте метод timesheet может
возвращать нечто, похожее на массив объектов, представляющих проекты.
Консоль даже подтвердит эту мысль. Спросите, какой тип имеет любой
ассоциированный набор, и консоль ответит, что это Array:
>> obie.timesheets.class
=> Array

Но она лжет, пусть даже неосознанно. Методы для ассоциации has_many –
на самом деле экземпляры класса HasManyAssociation, иерархия которого показана на рис. 7.1.
AssociationProxy

AssociationCollection

BelongsToAssociation

HasAndBelongsToManyAssociation

HasOneAssociation

HasManyAssociation

HasManyThroughAssociation

BelongsToPolymorphicAssociation

Рис. 7.1. Иерархия прокси-классов Association

Предком всех ассоциаций является класс AssociationProxy. Он определяет базовую структуру и функциональность прокси-классов любой ассоциации. Если посмотреть в начало его исходного текста (листинг 7.1),
то обнаружится, что он уничтожает определения целого ряда методов.
Листинг 7.1. Фрагмент кода из файла lib/active_record/associations/
association_proxy.rb
instance_methods.each { |m|
undef_method m unless m =~ /(^__|^nil\?$|^send$|proxy_)/ }

В результате значительная часть обычных методов экземпляроа в проксиобъекте отсутствует, а их функциональность делегируется объекту,
который прокси замещает, с помощью механизма method_missing. Это
означает, что обращение к методу timesheets.class возвращает класс
скрытого массива, а не класс самого прокси-объекта. Убедиться в том,
что timesheet действительно является прокси-объектом, можно, спросив, отвечает ли он на какой-нибудь из открытых методов класса AssociationProxy, например proxy_owner:

Отношения один-ко-многим

213

>> obie.timesheets.respond_to? :proxy_owner
=> true

К счастью, в Ruby не принято интересоваться фактическим классом
объекта. Гораздо важнее, на какие сообщения объект отвечает. Поэтому я думаю, что было бы ошибкой писать код, который зависит от того,
с чем работает: с массивом или с прокси-объектом ассоциации. Если
без этого никак не обойтись, вы всегда можете вызвать метод to_a и получить фактический объект Array:
>> obie.timesheets.to_a # make absolutely sure we're working with an
Array
=> []

Предком всех ассоциаций has_many является класс AssociationCollection,
и большая часть определенных в нем методов работает независимо от
объявления параметров, определяющих отношение. Прежде чем переходить к деталям прокси-классов ассоциаций, познакомимся с самым
фундаментальным типом ассоциации, который встречается в приложениях Rails чаще всего: парой has_many / belongs_to.

Отношения один-ко-многим
В нашем демонстрационном приложении примером отношения одинко-многим служит ассоциация между классами User, Timesheet и ExpenseReport:
class User < ActiveRecord::Base
has_many :timesheets
has_many :expense_reports
end

Табели и отчеты о расходах необходимо связывать и в обратном направлении, чтобы можно было получить пользователя user, которому
принадлежит отчет или табель:
class Timesheet < ActiveRecord::Base
belongs_to :user
end
class ExpenseReport < ActiveRecord::Base
belongs_to :user
end

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

214

Глава 7. Ассоциации в ActiveRecord
>> obie = User.create :login => 'obie', :password => '1234',
:password_confirmation => '1234', :email => 'obiefernandez@gmail.com'
=> #

Теперь проверю, появились ли наборы для табелей и отчетов о рас­
ходах:
>> obie.timesheets
ActiveRecord::StatementInvalid:
SQLite3::SQLException: no such column: timesheets.user_id:
SELECT * FROM timesheets WHERE (timesheets.user_id = 1)
from /.../connection_adapters/abstract_adapter.rb:128:in `log'

Тут Дэвид мог бы воскликнуть: «Фу-у-у!» Я забыл добавить внешние
ключи в таблицы timesheets и expense_reports, поэтому, прежде чем идти дальше, сгенерирую миграцию для внесения изменений:
$ script/generate migration add_user_foreign_keys
exists db/migrate
create db/migrate/004_add_user_foreign_keys.rb

Теперь открываю файл db/migrate/004_add_user_foreign_keys.rb и добавляю недостающие колонки:
class AddUserForeignKeys < ActiveRecord::Migration
def self.up
add_column :timesheets, :user_id, :integer
add_column :expense_reports, :user_id, :integer
end
def self.down
remove_column :timesheets, :user_id
remove_column :expense_reports, :user_id
end
end

Запускаю rake db:migrate для внесения изменений:
$ rake db:migrate
(in /Users/obie/prorails/time_and_expenses)
== AddUserForeignKeys: migrating
==============================================
-- add_column(:timesheets, :user_id, :integer)
-> 0.0253s
-- add_column(:expense_reports, :user_id, :integer)
-> 0.0101s
== AddUserForeignKeys: migrated (0.0357s)
==============================================

Теперь можно добавить новый пустой табель для только что созданного
пользователя и убедиться, что он попал в набор timesheets:
>> obie = User.find(1)
=> #
>> obie.timesheets [#]
>> obie.timesheets
=> [#]

Добавление ассоциированных объектов в набор
Согласно документации по Rails, добавление объекта в набор has_many
приводит к автоматическому его сохранению при условии, что родительский объект (владелец набора) уже сохранен в базе. Проверим, что
это действительно так, для чего вызовем метод reload, который повторно считывает значения атрибутов объекта из базы данных:
>> obie.timesheets.reload
=> [#"1", "user_id"=>"1"}>]

Все на месте. Метод obie.timesheets.create
=> #

Однако будьте осторожны, выбирая между timesheet.user = obie
=> #
>> timesheet.user.login
=> "obie"
>> timesheet.reload
=> #

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

Перезагрузка ассоциации
Простое обращение к атрибуту влечет за собой выполнение запроса
к базе данных (если необходимо), в результате чего возвращается эк-

Ассоциация belongs_to

219

земпляр ассоциированного объекта. Акцессор принимает параметр
force_reload, который указывает ActiveRecord, надо ли перезагружать
объект, если в настоящий момент он кэширован в результате предыдущей операции доступа.
В следующей взятой с консоли распечатке показано, как я получаю object_id объекта user, ассоциированного с табелем. Обратите внимание,
что при втором вызове ассоциации через user значение object_id не изменилось. Ассоциированный объект кэширован. Однако, если передать акцессору параметр true, будет выполнена повторная загрузка,
и я получу другой экземпляр.
>> ts = Timesheet.find :first
=> #"2006-11-21
05:44:09", "id"=>"3", "user_id"=>"1", "submitted"=>nil,
"created_at"=>"2006-11-21 05:44:09"}>
>> ts.user.object_id
=> 27421330
>> ts.user.object_id
=> 27421330
>> ts.user(true).object_id
=> 27396270

Построение и создание связанных объектов
через ассоциацию
Метод belongs_to с помощью техники метапрограммирования добавляет фабричные методы для автоматического создания новых экземпляров связанного класса и присоединения их к родительскому объекту
с помощью внешнего ключа.
Метод build_association не сохраняет новый объект, тогда как create_
association сохраняет. Обоим методам можно передать необязательный
хеш, содержащий значения атрибутов, которыми инициализируется
вновь созданный объект. Оба метода – не более чем однострочные утилиты, которые, на мой взгляд, не очень полезны, так как создавать экземпляры в этом направлении обычно не имеет смысла!
Для иллюстрации я просто приведу код построения объекта User по
объекту Timesheet или объекта Client по BillingCode. В реальной программе они, скорее всего, никогда не встретятся в силу бессмысленно­
сти подобной операции:
>> ts = Timesheet.find :first
=> #"2006-11-21
05:44:09", "id"=>"3", "user_id"=>"1", "submitted"=>nil, "created_at"
=>"2006-11-21 05:44:09"}>
>> ts.build_user
=> #nil, "updated_at"=>nil,
"crypted_password"=>nil, "remember_token_expires_at"=>nil,

220

Глава 7. Ассоциации в ActiveRecord
"remember_token"=>nil, "login"=>nil, "created_at"=>nil, "email"=>nil},
@new_record=true>
>> bc = BillingCode.find :first
=> #"TRAVEL", "client_id"
=>nil, "id"=>"1", "description"=>"Travel expenses of all sorts"}>
>> bc.create_client
=> #nil, "code"=>nil,
"id"=>1}, @new_record=false>

Гораздо чаще вам придется создавать экземпляры объектов, принадлежащих владельцу, со стороны отношения has_many.

Параметры метода belongs_to
Методу belongs_to можно передать следующие параметры в хеше.

:class_name
Представим на секунду, что нам нужно установить еще одно отношение belongs_to между классами Timesheet и User, чтобы промоделировать
лицо, утверждающее табель. Для начала вы, наверное, добавили бы
колонку approver_id в таблицу timesheets и колонку authorized_approver
в таблицу users:
class AddApproverInfo < ActiveRecord::Migration
def self.up
add_column :timesheets, :approver_id, :integer
add_column :users, :authorized_approver, :boolean
end
def self.down
remove_column :timesheets, :approver_id
remove_column :users, :authorized_approver
end
end

А затем – такую ассоциацию belongs_to:
class Timesheet < ActiveRecord::Base
belongs_to :approver
...

Проблема в том, что Rails не может определить, с каким классом вы
пытаетесь установить связь, на основе только этой информации, потому что вы (вполне законно) нарушили соглашение об именовании отношения по связываемому классу. Тут-то и приходит на помощь параметр :class_name.

Ассоциация belongs_to

221

class Timesheet < ActiveRecord::Base
belongs_to :approver, :class_name => 'User'
...

:conditions
А как насчет добавления условий к ассоциации belongs_to? Rails позволяет добавлять к отношению условия, которые должны выполняться,
чтобы отношение считалось допустимым. Для этого предназначен параметр :conditions с тем же синтаксисом, что для добавления условий
при обращении к методу find.
В последней миграции я добавил в таблицу users колонку authorized_
approver, и сейчас мы ею воспользуемся:
class Timesheet < ActiveRecord::Base
belongs_to :approver,
:class_name => 'User',
:conditions => ['authorized_approver = ?', true]
...
end

Теперь присваивание объекта user полю approver допустимо, только если пользователь имеет право утверждать табели. Я добавлю тест, который одновременно документирует мои намерения и демонстрирует их
в действии.
Сначала нужно позаботиться о том, чтобы в фикстуре, описывающей
пользователей (users.yml) был представлен пользователь, имеющий
право утверждения. Для полноты картины я добавлю еще и пользователя, не имеющего такого права. В конец файла test/fixtures/users.yml
я вставил такую разметку:
approver:
id: 4
login: "manager"
authorized_approver: true
joe:
id: 5
login: "joe"
authorized_approver: false

Теперь обратимся к файлу test/unit/timesheet_test.rb, в который я добавил тест, проверяющий работу приложения:
require File.dirname(__FILE__) + '/../test_helper'
class TimesheetTest < Test::Unit::TestCase
fixtures :users
def test_only_authorized_user_may_be_associated_as_approver
sheet = Timesheet.create

222

Глава 7. Ассоциации в ActiveRecord
sheet.approver = users(:approver)
assert_not_nil sheet.approver, "approver assignment failed"
end
end

Для начала неплохо, но я хочу быть уверен, что система не даст записать в поле approver объект, представляющий неуполномоченного пользователя. Поэтому добавлю еще один тест:
def test_non_authorized_user_cannot_be_associated_as_approver
sheet = Timesheet.create
sheet.approver = users(:joe)
assert sheet.approver.nil?, "approver assignment should have failed"
end

Однако у меня есть некоторые подозрения по поводу корректности этого теста, и, как я и опасался, он не работает должным образом:
1) Failure:
test_non_authorized_user_cannot_be_associated_as_approver(TimesheetTest)
[./test/unit/timesheet_test.rb:16]:
approver assignment should have failed.
is not true.

Проблема в том, что ActiveRecord (хорошо это или плохо, но, скорее,
все-таки плохо) позволяет мне выполнить недопустимое присваивание. Параметр :conditions применяется только во время запроса, когда
ассоциация считывается из базы данных. Мне еще предстоит потрудиться, чтобы получить желаемое поведение, но пока я просто смирюсь
с тем, что делает Rails, и исправлю свои тесты:
def test_only_authorized_user_may_be_associated_as_approver
sheet = Timesheet.create
sheet.approver = users(:approver)
assert sheet.save
assert_not_nil sheet.approver(true), "approver assignment failed"
end
def test_non_authorized_user_cannot_be_associated_as_approver
sheet = Timesheet.create
sheet.approver = users(:joe)
assert sheet.save
assert sheet.approver(true).nil?, "approver assignment should fail"
end

Эти тесты проходят. Я сохранил объект sheet, поскольку одного лишь
присваивания атрибуту недостаточно для сохранения записи. Затем
я воспользовался параметром force_reload, чтобы заставить Rails перечитать approver из базы данных, а не просто вернуть мне тот же экземпляр, который я только что сам присвоил в качестве значения атрибута.
Запомните, что параметр :conditions для отношений не влияет на присваивание ассоциированных объектов. Он влияет лишь на то, как эти

Ассоциация belongs_to

223

объекты считываются из базы данных. Для гарантии того, что пользователь, утверждающий табель, имеет на это право, придется добавить
обратный вызов before_save в сам класс Timesheet. Подробно об обратных вызовах мы будем говорить в начале главы 9 «Дополнительные
возможности ActiveRecord», а пока вернемся к параметрам ассоциации belongs_to.

:foreign_key
Задает имя внешнего ключа, с помощью которого следует искать ассоциированный объект. Обычно Rails самостоятельно выводит эту информацию из имени ассоциации, добавляя суффикс _id. Но этот параметр
позволяет при необходимости переопределить данное соглашение.
# без явного указания Rails предполагает, в какой колонке находится
# идентификатор администратора
belongs_to :administrator, :foreign_key => 'admin_user_id'

:counter_cache
Этот параметр заставляет Rails автоматически обновлять счетчик ассоциированных объектов, принадлежащих данному объекту. Если параметр равен true, предполагается, что имя колонки составлено из множественного числа имени подчиненного класса и суффикса _count, но
можно задать имя колонки и самостоятельно:
:counter_cache => true
:counter_cache => 'number_of_children'

Если в любой момент времени значительная доля наборов ассоциаций
пуста, можно оптимизировать производительность ценой небольшого
увеличения базы данных за счет кэширования счетчиков. Причина состоит в том, что, когда кэшированный счетчик равен 0, Rails даже не
пытается запрашивать ассоциированные записи у базы данных!

Примечание
В колонку, где хранится кэшированный счетчик, СУБД должна записывать по умолчанию
значение 0! В противном случае кэширование счетчиков вообще не будет работать. Связано это с тем, что Rails реализует данный механизм, добавляя простой обратный вызов,
который выполняет предложение UPDATE, увеличивающее счетчик на единицу.

Если вы проявите беспечность и не зададите для колонки счетчика
значение 0 по умолчанию или неправильно укажете имя колонки, то
все равно будет казаться, что механизм кэширования счетчика работает! Во всех классах с ассоциацией has_many имеется магический метод collection_count. Он возвращает правильное значение, если параметр :counter_cache не задан или значение в колонке для кэширования
счетчика равно null!

224

Глава 7. Ассоциации в ActiveRecord

:include
Принимает список имен ассоциаций второго порядка, которые должны быть загружены одновременно с загрузкой объекта. На лету конструируется предложение SELECT с необходимыми частями LEFT OUTER
JOIN, так что одним запросом к базе мы получаем весь граф объектов.
При условии здравого использования :include и тщательного замера временных характеристик иногда удается весьма значительно повысить
производительность приложения, главным образом за счет устранения
запросов N+1. С другой стороны, сложные запросы со многими соединениями и создание больших деревьев объектов обходятся очень дорого,
поэтому иногда использование :include может весьма существенно замедлить работу приложения. Как говорится, раз на раз не приходится.

Говорит Уилсон…
Если :include ускоряет приложение, значит оно слишком сложное и нуждается в перепроектировании.

:polymorphic => true
Параметр :polymorphic говорит, что объект связан с ассоциацией полиморфно. Так в Rails говорят о ситуации, когда в базе данных вместе
с внешним ключом хранится также тип связанного объекта. Сделав отношение belongs_to полиморфным, вы абстрагируете ассоциацию таким образом, что ее может заполнить любая другая модель в системе.
Полиморфные ассоциации позволяют слегка поступиться ссылочной
целостностью ради удобства реализации отношений родитель-потомок, допускающих повторное использование. Типичные примеры дают такие модели, как прикрепленные фотографии, комментарии, примечания и т. д.
Проиллюстрируем эту идею, написав класс Comment, который прикрепляется к аннотируемым объектам полиморфно. Мы ассоциируем его
с отчетами о расходах и табелями. В листинге 7.2 приведен код миграции, содержащий информацию об измененной схеме БД и соответствующие классы. Обратите внимание на колонку :subject_type, в которой
хранится имя ассоциированного класса.
Листинг 7.2. Класс Comment, в котором используется полиморфное
отношение belongs_to
create_table :comments do |t|
t.column :subject, :string
t.column :body, :text

Ассоциация has_many

225

t.column :subject_id, :integer
t.column :subject_type, :string
t.column :created_at, :datetime
end
class Comment < ActiveRecord::Base
belongs_to :subject, :polymorphic => true
end
class ExpenseReport < ActiveRecord::Base
belongs_to :user
has_many :comments, :as => :subject
end
class Timesheet < ActiveRecord::Base
belongs_to :user
has_many :comments, :as => :subject
end

Как видно из кода классов ExpenseReport и Timesheet в листинге 7.2, имеется парный синтаксис, с помощью которого вы сообщаете ActiveRecord,
что отношение полиморфно – :as => :subject. Мы пока даже не обсуждали отношения has_many, а полиморфным отношениям посвящен отдельный раздел в главе 9. Поэтому не будем забегать вперед, а обратимся
к отношениям has_many.

Ассоциация has_many
Ассоциация has_many позволяет определить отношение, при котором
для одной модели существуют много других принадлежащих ей моделей. Непревзойденная понятность таких программных конструкций,
как has_many, – одна из причин, по которым люди влюбляются в Rails.
Метод класса has_many часто применяется без дополнительных параметров. Если Rails может вывести тип класса, участвующего в отношении, из имени ассоциации, то никакого добавочного конфигурирования не требуется. Следующий фрагмент кода должен быть вам уже
привычен:
class User
has_many :timesheets
has_many :expense_reports

Имена ассоциаций можно привести к единственному числу и сопоставить с именами моделей в приложении, поэтому все работает, как
и ожидается.

Параметры метода has_many
Несмотря на простоту использования метода has_many, для знакомых
с его параметрами открывается широкое поле для настройки.

226

Глава 7. Ассоциации в ActiveRecord

:after_add
Этот обратный вызов выполняется после добавления записи в набор
методом lamda {raise "You can't add a post"}

:before_remove
Вызывается перед удалением записи из набора методом delete. Дополнительную информацию см. в описании параметра :before_add.

:class_name
Параметр :class_name – общий для всех ассоциаций. Он позволяет задать строку, представляющую имя класса ассоциации, и необходим,
когда это имя невозможно вывести из имени самой ассоциации.

:conditions
Параметр :conditions – общий для всех ассоциаций. Он позволяет добавить дополнительные условия в генерируемый ActiveRecord SQL-запрос, который загружает в ассоциацию объекты из базы данных.
Для указания дополнительных условий в параметре :conditions могут
быть различные причины. Вот пример, отбирающий только утвержденные комментарии:
has_many :comments, :conditions => ['approved = ?', true]

Кроме того, никто не запрещает иметь несколько ассоциаций has_many
для представления одной и той же пары таблиц разными способами.
Не забывайте только задавать еще и имя класса:
has_many :pending_comments, :conditions => ['approved = ?', true],
:class_name => 'Comment'

:counter_sql
Переопределяет генерируемый ActiveRecord SQL-запрос для подсчета
числа записей, принадлежащих данной ассоциации. Использовать этот
параметр совместно с :finder_sql необязательно, так как ActiveRecord
автоматически генерирует SQL-запрос для получения количества записей по переданному SQL-предложению выборки.
Как и для любых других SQL-предложений в ActiveRecord, необходимо заключать всю строку в одиночные кавычки во избежание преждевременной интерполяции (вы же не хотите, чтобы эта строка была ин-

228

Глава 7. Ассоциации в ActiveRecord

терполирована в контексте данного класса в момент объявления ассоциации, – необходимо, чтобы интерполяция произошла во время выполнения).
has_many :things, :finder_sql => 'select * from t where id = #{id}'

:delete_sql
Переопределяет генерируемый ActiveRecord SQL-запрос для разрыва
ассоциации. Доступ к ассоциированной модели предоставляет метод
record.

:dependent => :delete_all
Все ассоциированные объекты удаляются одним махом с помощью
единственной SQL-команды. Примечание: хотя этот режим гораздо
быстрее, чем задаваемый параметром :destroy_all, в нем не активируются обратные вызовы при удалении ассоциированных объектов. По­
этому пользоваться им необходимо с осторожностью и только для ассоциаций, зависящих исключительно от родительского объекта.

:dependent => :destroy_all
Все ассоциированные объекты уничтожаются вместе с уничтожением
родительского объекта путем вызова метода destroy каждого объекта.

:dependent => :nullify
По умолчанию при удалении ассоциированных записей внешние ключи, соединяющие их с родительской записью, просто очищаются (в них
записывается null). Поэтому явно задавать этот параметр совершенно
необязательно, он приведен только для справки.

:exclusively_dependent
Объявлен устаревшим; эквивалентен :dependent => :delete_all.

:extend => ExtensionModule
Задает модуль, методы которого расширяют класс прокси-набора ассоциации. Применяется в качестве альтернативы определению дополнительных методов в блоке, передаваемом самому методу has_many. Обсуждается в разделе «Расширение ассоциаций».

:finder_sql
Задает полное SQL-предложение выборки данных для ассоциаций. Это
удобный способ загрузки сложных ассоциаций, зависящих от нескольких таблиц. Но применять его приходится сравнительно редко.
Операции подсчета количества записей выполняются с помощью SQLпредложения, конструируемого на основе запроса, который передает-

Ассоциация has_many

229

ся в параметре :finder_sql. Если ActiveRecord не справляется с преобразованием, необходимо также явно передать запрос в параметре
:counter_sql.

:foreign_key
Переопределяет принимаемое по соглашению имя внешнего ключа,
который используется в предложении SQL для загрузки ассоциации.

:group
Имя атрибута, по которому следует группировать результаты. Используется в части GROUP BY SQL-запроса.

:include
Принимает массив имен ассоциаций второго порядка, которые следует загрузить одновременно с загрузкой данного набора. Как и в случае
параметра :include для ассоциаций типа belongs_to, иногда это позволяет заметно повысить производительность приложения, но нуждается в тщательном тестировании.
Для иллюстрации проанализируем, как параметр :include отражается
на SQL-запросе, генерируемом для навигации по отношениям. Воспользуемся следующими упрощенными версиями классов Timesheet,
BillableWeek и BillingCode:
class Timesheet < ActiveRecord::Base
has_many :billable_weeks
end
class BillableWeek < ActiveRecord::Base
belongs_to :timesheet
belongs_to :billing_code
end
class BillingCode < ActiveRecord::Base
belongs_to :client
has_many :billable_weeks
end

Сначала необходимо подготовить тестовые данные, поэтому я создаю
экземпляр timesheet и добавляю в него две оплачиваемые недели. Затем каждой оплачиваемой неделе назначаю код оплаты, что приводит
к появлению графа объектов (включающего четыре объекта, связанных между собой ассоциациями).
Далее выполняю метод, записанный в одну строчку collect, который
возвращает массив кодов оплаты, ассоциированный с табелем:
>> Timesheet.find(3).billable_weeks.collect{ |w| w.billing_code.code }
=> ["TRAVEL", "DEVELOPMENT"]

230

Глава 7. Ассоциации в ActiveRecord

Если не задавать для ассоциации billable_weeks параметр :include, потребуется четыре обращения к базе данных (скопированы из протокола
log/development.log и немного «причесаны»):
Timesheet Load (0.000656)

SELECT * FROM timesheets
WHERE (timesheets.id = 3)
BillableWeek Load (0.001156) SELECT * FROM billable_weeks
WHERE (billable_weeks.timesheet_id = 3)
BillingCode Load (0.000485) SELECT * FROM billing_codes
WHERE (billing_codes.id = 1)
BillingCode Load (0.000439) SELECT * FROM billing_codes
WHERE (billing_codes.id = 2)

Это пример так называемой проблемы N+1 select, от которой страдают
многие системы. Для загрузки одной оплачиваемой недели требуется
N предложений SELECT, отбирающих ассоциированные с ней записи.
А теперь добавим в ассоциацию billable_weeks параметр :include, после
чего класс Timesheet будет выглядеть следующим образом:
class Timesheet < ActiveRecord::Base
has_many :billable_weeks, :include => [:billing_code]
end

Как просто! Запустив тот же тестовый запрос в консоли, мы получим те
же самые результаты:
>> Timesheet.find(3).billable_weeks.collect{ |w| w.billing_code.code }
=> ["TRAVEL", "DEVELOPMENT"]

Но посмотрите, как изменилось сгенерированное SQL-предложение:
Timesheet Load (0.002926) SELECT * FROM timesheets LIMIT 1
BillableWeek Load Including Associations (0.001168) SELECT
billable_weeks."id" AS t0_r0, billable_weeks."timesheet_id" AS t0_r1,
billable_weeks."client_id" AS t0_r2, billable_weeks."start_date" AS
t0_r3, billable_weeks."billing_code_id" AS t0_r4,
billable_weeks."monday_hours" AS t0_r5, billable_weeks."tuesday_hours"
AS t0_r6, billable_weeks."wednesday_hours" AS t0_r7,
billable_weeks."thursday_hours" AS t0_r8,
billable_weeks."friday_hours"
AS t0_r9, billable_weeks."saturday_hours" AS t0_r10,
billable_weeks."sunday_hours" AS t0_r11, billing_codes."id" AS t1_r0,
billing_codes."client_id" AS t1_r1, billing_codes."code" AS t1_r2,
billing_codes."description" AS t1_r3 FROM billable_weeks LEFT OUTER
JOIN
billing_codes ON billing_codes.id = billable_weeks.billing_code_id
WHERE
(billable_weeks.timesheet_id = 3)

Rails добавил часть LEFT OUTER JOIN, так что данные о кодах оплаты загружаются вместе с оплачиваемыми неделями. Для больших наборов
данных производительность может возрасти очень заметно!

231

Ассоциация has_many

Выявить проблему N+1 select проще всего, наблюдая, что записывается в протокол при выполнении различных операций приложения (конечно, работать надо с реальными данными, иначе это упражнение будет пустой тратой времени). Операции, для которых попутная загрузка
может обернуться выгодой, характеризуются наличием многочисленных однострочных предложений SELECT – по одному для каждой ассоциированной записи.
Если у вас «зудит» (быть может, «склонны к мазохизму» – более правильное выражение в данном случае), можете попробовать активировать глубокую иерархию ассоциаций, включив хеши в массив :include:
Post.find(:all, :include=>[:author, {:comments=>{:author=>:gravatar }}])

Здесь мы отбираем не только все комментарии для сообщения Post, но
и их авторов и аватары. Для описания загружаемых ассоциаций символы, массивы и хеши можно употреблять в любых сочетаниях.
Честно говоря, глубокая вложенность параметров :include плохо документирована, и, пожалуй, сопряженные с этим проблемы перевешивают потенциальные достоинства. Самая серьезная неприятность заключается в том, что выборка чрезмерно большого количества данных в одном запросе может «посадить» производительность. Начинать всегда
надо с простейшего работающего решения, затем выполнять замеры
и анализировать, способна ли оптимизация с применением попутной
загрузки улучшить быстродействие.

Говорит Уилсон…
Учиться применению попутной загрузки надо, ходя по тонкому
льду, как мы. Это закаляет характер!

:insert_sql
Переопределяет генерируемое ActiveRecord предложение SQL для создания ассоциаций. Для доступа к ассоциированной модели применяется метод record.

:limit
Добавляет часть LIMIT к сгенерированному SQL-предложению для загрузки данной ассоциации.

:offset
Целое число, задающее номер первой из отобранных строк, которые
следует вернуть.

232

Глава 7. Ассоциации в ActiveRecord

:order
Задает порядок сортировки ассоциированных объектов с помощью части ORDER BY предложения SELECT, например: "last_name, first_name DESC".

:select
По умолчанию *, как в предложении SELECT * FROM. Но это можно изменить, если, например, вы хотите включить дополнительные вычисляемые колонки или добавить в ассоциированный объект колонки из соединяемых таблиц.

:source и :source_type
Применяются исключительно как дополнительные параметры при использовании ассоциации has_many :through с полиморфной belongs_to;
более подробно рассматриваются ниже.

:table_name
Параметр :table_name позволяет переопределять имена таблиц (в части
FROM), используемых в SQL-предложениях, которые генерируются для
загрузки ассоциаций.

:through
Создает набор ассоциации посредством другой ассоциации. См. ниже
раздел «Конструкция has_many :through».

:uniq => true
Убирает объекты-дубликаты из набора. Полезно в сочетании с has_many
:through.

Методы проксиклассов
Метод класса has_many создает прокси-набор ассоциации, обладающий
всеми методами класса AssociationCollection, а также несколькими методами, определенными в классе HasManyAssociation.

build(attributes = {})
Создает новый объект в ассоциированном наборе и связывает его с владельцем, задавая внешний ключ. Не сохраняет объект в базе данных
и не добавляет его в набор ассоциации. Из следующего примера ясно,
что если не сохранить значение, возвращенное методом build, новый
объект будет потерян:
>> obie.timesheets
=>

Отношения многие-ко-многим

233

>> obie.timesheets.build
=> #1,
"submitted"=>nil}>
>> obie.timesheets
=>

В онлайновой документации по API подчеркивается, что метод build
делает в точности то же самое, что конструирование нового объекта с
передачей значения внешнего ключа в виде атрибута:
>> Timesheet.new(:user_id => 1)
=> #1,
"submitted"=>nil}>

count(*args)
Вычисляет количество ассоциированных записей в базе данных с использованием SQL.

find(*args)
Отличается от обычного метода find тем, что область видимости ограничена ассоциированными записями и дополнительными условиями,
заданными в объявлении отношения.
Припомните пример ассоциации has_one в начале этой главы. Он был
несколько искусственным, поскольку гораздо проще было бы найти
последний модифицированный табель с помощью find:.

Отношения многие-ко-многим
Ассоциирование хранимых объектов с помощью связующей таблицы –
один из самых сложных аспектов объектно-реляционного отображения, правильно реализовать его в среде отнюдь не тривиально. В Rails
есть два способа представить в модели отношения многие-ко-многим. Мы
начнем со старого и простого метода has_and_belongs_to_many, а затем рассмотрим более современную конструкцию has_many :through.

Метод has_and_belongs_to_many
Метод has_and_belongs_to_many устанавливает связь между двумя ассоциированными моделями ActiveRecord с помощью промежуточной связующей таблицы. Если связующая таблица явно не указана в параметрах, то Rails строит ее имя, конкатенируя имя таблиц в соединяемых
классах в алфавитном порядке с разделяющим подчерком.
Например, если бы метод has_and_belongs_to_many (для краткости habtm)
применялся для организации отношения между классами Timesheet
и BillingCode, то связующая таблица называлась бы billing_codes_

234

Глава 7. Ассоциации в ActiveRecord

timesheets, и это отношение было бы определено в моделях. Ниже приведены классы миграции и моделей:
class CreateBillingCodesTimesheets < ActiveRecord::Migration
def self.up
create_table :billing_codes_timesheets, :id => false do |t|
t.column :billing_code_id, :integer, :null => false
t.column :timesheet_id, :integer, :null => false
end
end
def self.down
drop_table :billing_codes_timesheets
end
end
class Timesheet < ActiveRecord::Base
has_and_belongs_to_many :billing_codes
end
class BillingCode < ActiveRecord::Base
has_and_belongs_to_many :timesheets
end

Отметим, что первичный ключ id здесь не нужен, поэтому методу create_table был передан параметр :id => false. Кроме того, поскольку
необходимы обе колонки с внешними ключами, мы задали параметр
:null => false (в реальной программе нужно было бы позаботиться
о построении индексов по обоим внешним ключам).

Отношение, ссылающееся само на себя
Что можно сказать об отношениях, ссылающихся на себя (self-refe­
ren­tial)? Связывание модели с самой собой с помощью отношения
habtm реализуется без труда – достаточно явно задать некоторые параметры.
В листинге 7.4 я создал связующую таблицу и установил связи между
ассоциированными объектами BillingCode. Как и раньше, показаны
классы миграции и моделей.
Листинг 7.4. Связанные коды оплаты
class CreateRelatedBillingCodes < ActiveRecord::Migration
def self.up
create_table :related_billing_codes, :id => false do |t|
t.column :first_billing_code_id, :integer, :null => false
t.column :second_billing_code_id, :integer, :null => false
end
end
def self.down

Отношения многие-ко-многим

235

drop_table :related_billing_codes
end
end
class BillingCode < ActiveRecord::Base
has_and_belongs_to_many :related,
:join_table => 'related_billing_codes',
:foreign_key => 'first_billing_code_id',
:association_foreign_key => 'second_billing_code_id',
:class_name => 'BillingCode'
end

Двусторонние отношения
Стоит отметить, что отношение related в классе BillingCode из листинга 7.4 не является двусторонним. Тот факт, что между двумя объектами существует ассоциация в одном направлении, еще не означает, что
и в другом направлении тоже есть ассоциация. Но как быть, если требуется автоматически установить двустороннее отношение?
Для начала напишем тест для класса BillingCode, который подтвердит работоспособность нашего решения. Начнем с добавления в файл test/fixtures/billing_codes.yml двух записей, с которыми будем работать далее:
travel:
code: TRAVEL
client_id:
id: 1
description: Разнообразные транспортные расходы
development:
code: DEVELOPMENT
client_id:
id: 2
description: Кодирование и т. д.

Мы не хотим, чтобы после добавления двустороннего отношения нормальная работа программы была нарушена, поэтому сначала мой тестовый метод проверяет, работает ли обычное отношение habtm:
require File.dirname(__FILE__) + '/../test_helper'
class BillingCodeTest < Test::Unit::TestCase
fixtures :billing_codes
def test_self_referential_habtm_association
billing_codes(:travel).related 'second_billing_code_id',
:class_name => 'BillingCode',
:insert_sql => 'INSERT INTO related_billing_codes
(`first_billing_code_id`,
`second_billing_code_id`)
VALUES (#{id}, #{record.id}), (#{record.id},
#{id})'
end

Эффективное связывание двух существующих объектов
До выхода версии Rails 2.0 метод :billable_weeks
end

Класс BillableWeek уже встречался в нашем приложении и пригоден
для использования в качестве модели соединения:
1

http://blog.hasmanythrough.com/articles/2006/02/28/association-goodness.

241

Отношения многие-ко-многим
class BillableWeek < ActiveRecord::Base
belongs_to :client
belongs_to :timesheet
end

Мы можем также подготовить обратное отношение – от табелей к клиентам:
class Timesheet < ActiveRecord::Base
has_many :billable_weeks
has_many :clients, :through => :billable_weeks
end

Обратите внимание, что ассоциация has_many :through всегда используется в сочетании с обычной ассоциацией has_many. Еще отметим, что
обычная ассоциация has_many часто называется одинаково в обоих соединяемых классах, следовательно, параметр :through будет выглядеть
идентично на обеих сторонах:
:through => :billable_weeks

А как насчет модели соединения: должна ли она всегда иметь две ассоциации belongs_to? Нет.
Вы можете использовать has_many :through для агрегирования ассоциаций has_many или has_one в модели соединения. Простите, что в качестве
примера я возьму далекую от практики предметную область, я просто
хочу как можно понятнее изложить то, что пытаюсь описать:
class Grandparent < ActiveRecord::Base
has_many :parents
has_many :grand_children, :through => :parents, :source => :childs
end
class Parent < ActiveRecord::Base
belongs_to :grandparent
has_many :childs
end

Чтобы избежать многословия в следующих главах, я буду называть такое использование ассоциации has_many :through агрегированием.

Говорит Кортенэ…
Мы постоянно применяем ассоциацию has_many :through! Она практически полностью вытеснила старый механизм has_and_belongs_
to_many, поскольку позволяет преобразовать модели соединения
в полноценные объекты.
Представьте, что вы пригласили девушку на свидание, а она заводит разговор об Отношениях (в конечном итоге, о Нашей Свадьбе). Это пример ассоциации, которая означает нечто более важное, чем индивидуальные объекты по обе стороны.

242

Глава 7. Ассоциации в ActiveRecord

Замечания о применении и примеры
Использовать неагрегирующие ассоциации has_many :through можно
почти так же, как любые другие ассоциации has_many. Ограничения касаются работы с несохраненными записями.
>> c = Client.create(:name => "Trotter's Tomahawks", :code => "ttom")
=> #
>> c.timesheets > c.save
=> true
>> c.timesheets.create
=> [#Sun Mar 18 15:37:18 UTC 2007,
"id"=>2,
"user_id"=>nil, "submitted"=>nil, "created_at"=>Sun Mar 18 15:37:18
UTC
2007}, @errors=#> ]

Основное достоинство ассоциации has_many :through заключается в том,
что ActiveRecord снимает с вас заботу об управлении экземплярами
модели соединения. Вызвав метод reload для ассоциации billable_weeks,
мы увидим, что объект, представляющий оплачиваемую неделю, создан автоматически:
>> c.billable_weeks.reload
=> [#nil,
"start_date"=>nil, "timesheet_id"=>"2", "billing_code_id"=>nil,
"sunday_hours"=>nil, "friday_hours"=>nil, "monday_hours"=>nil,
"client_id"=>"2", "id"=>"2", "wednesday_hours"=>nil,
"saturday_hours"=>nil, "thursday_hours"=>nil}> ]

Созданный объект BillableWeek правильно ассоциирован как с Client,
так и с Timesheet. К сожалению, ряд прочих атрибутов (например, колонки start_date и hours) не заполнен.

Отношения многие-ко-многим

243

Одно из возможных решений – вызвать вместо этого метод create ассоциации billable_weeks и включить новый объект Timesheet как одно из
предоставляемых свойств.
>> bw = c.billable_weeks.create(:start_date => Time.now,
:timesheet => Timesheet.new)
=> # :timesheets
...

Ассоциация billable_weeks агрегирует все объекты, представляющие
оплачиваемые недели, которые принадлежат всем табелям данного
пользователя:
class Timesheet < ActiveRecord::Base
belongs_to :user
has_many :billable_weeks, :include => [:billing_code]
...

А теперь зайдем в консоль Rails и подготовим данные, чтобы можно
было воспользоваться новым набором billable_weeks (для User):
>> quentin = User.find :first
#
>> quentin.timesheets
=> []
>> ts1 = quentin.timesheets.create
=> #
>> ts2 = quentin.timesheets.create
=> #
>> ts1.billable_weeks.create(:start_date => 1.week.ago)
=> #
>> ts2.billable_weeks.create :start_date => 2.week.ago
=> #

244

Глава 7. Ассоциации в ActiveRecord
>> quentin.billable_weeks
=> [#, #]

Просто ради смеха посмотрим, что получится, если мы попытаемся создать объект BillableWeek от имени экземпляра User:
>> quentin.billable_weeks.create(:start_date => 3.weeks.ago)
NoMethodError: undefined method `user_id=' for
#

Вот так-то… BillableWeek принадлежит не пользователю, а табелю, по­
этому в нем нет поля user_id.

Модели соединения и валидаторы
При добавлении в конец не-агрегирующей ассоциации has_many :through
с помощью метода :timesheet_id

Здесь мы говорим: «Для каждого табеля может существовать только
один экземпляр конкретного клиента».
Если в модели соединения имеются дополнительные атрибуты с собственной логикой контроля, то следует помнить еще об одной детали.
При добавлении записей непосредственно в ассоциацию has_many :through
автоматически создается новая модель соединения с пустым набором
атрибутов. Контроль дополнительных колонок, скорее всего, завершится неудачно. Если такое имеет место, то для добавления новой записи придется создать объект модели соединения и ассоциировать его
с помощью его же собственного прокси-объекта ассоциации:
timesheet.billable_weeks.create(:start_date => 1.week.ago)

Параметры ассоциации has_many :through
У ассоциации has_many :through такие же параметры, что и у has_many.
Напомним, что :through – сам по себе не более чем параметр has_many!
Однако при наличии :through некоторые параметры has_many изменяют
семантику или становятся более существенными. Прежде всего параметры :class_name и :foreign_key теперь недопустимы, так как они выводятся из целевой ассоциации модели соединения.
Ниже приведен перечень других параметров, которые в случае has_many
:through интерпретируются иначе.

Отношения многие-ко-многим

245

:source
Параметр :source определяет, какую ассоциацию использовать. Обычно задавать его необязательно, поскольку по умолчанию ActiveRecord
предполагает, что имеется в виду единственное (или множественное)
число имени ассоциации has_many. Если имена ассоциаций не соответ­
ствуют друг другу, требуется указать параметр:source явно.
Например, в следующей строке для заполнения timesheets используется ассоциация sheet в классе BillableWeek.
has_many :timesheets, :through => :billable_weeks, :source => :sheet

:source_type
Параметр :source_type необходим, когда вы устанавливаете ассоциацию has_many :through с полиморфной ассоциацией belongs_to в модели
соединения.
Рассмотрим следующий пример с клиентами и контактами:
class Client < ActiveRecord::Base
has_many :contact_cards
has_many :contacts, :through => :contact_cards
end
class ContactCard < ActiveRecord::Base
belongs_to :client
belongs_to :contacts, :polymorphic => true
end

Самое важное здесь то, что у клиента Client есть много контактов contacts, которые могут описываться любой моделью, а в модели соединения ContactCard объявлены как полиморфные. Для примера ассоциируем физических и юридических лиц с контактными карточками:
class Person < ActiveRecord::Base
has_many :contact_cards, :as => :contact
end
class Business < ActiveRecord::Base
has_many :contact_cards, :as => :contact
end

Теперь подумайте, какое сальто предстоит сделать ActiveRecord, чтобы понять, к каким таблицам обращаться в поисках контактов клиента. Теоретически необходимо знать обо всех классах моделей, связанных с другой стороной полиморфной ассоциации contacts.
На самом деле, на такие сальто ActiveRecord не способна, что и хорошо
с точки зрения производительности:
>> Client.find(:first).contacts
ArgumentError: /.../active_support/core_ext/hash/keys.rb:48:
in `assert_valid_keys': Unknown key(s): polymorphic

246

Глава 7. Ассоциации в ActiveRecord

Единственный способ добиться в этом сценарии хоть какого-то результата – немного помочь ActiveRecord, подсказав, в какой таблице нужно искать, когда запрашивается набор contacts, и сделать это позволяет
параметр source_type. Его значением является символ, представляющий имя конечного класса:
class Client < ActiveRecord::Base
has_many :people_contacts, :through => :contact_cards,
:source => :contacts, :source_type => :person
has_many :business_contacts, :through => :contact_cards,
:source => :contacts, :source_type => :business
end

После указания :source_type ассоциация работает должным образом.
>> Client.find(:first).people_contacts.create!
[#1}, @errors=
#, @new_record_before_save=true, @new_record=false>]

Код получился несколько длиннее, и магии в нем нет, но он работает.
Если вас расстроил тот факт, что нельзя ассоциировать people_contacts
и business_contacts в одной ассоциации contacts, можете попробовать
написать собственный метод-акцессор для контактов клиента:
class Client < ActiveRecord::Base
def contacts
people_contacts + business_contacts
end
end

Разумеется, вы должны понимать, что вызов метода contacts означает
по меньшей мере два запроса к базе данных, а вернет он объект Array
без методов прокси-класса ассоциации, на которые вы, возможно, рассчитывали.

:uniq
Параметр :uniq говорит, что ассоциация должна включать только уникальные объекты. Это особенно полезно при работе с has_many :through,
так как два разных объекта BillableWeek могут ссылаться на один и тот
же объект Timesheet.
>> client.find(:first).timesheets.reload
[#"1", ...}>,
#"1", ...}>]

Ничего экстраординарного в одновременном присутствии в памяти
двух разных экземпляров модели для одной и той же записи нет, просто обычно это нежелательно.
class Client < ActiveRecord::Base
has_many :timesheets, :through => :billable_weeks, :uniq => true
end

Отношения один-к-одному

247

Если задан параметр :uniq, возвращается только один экземпляр для
одной записи.
>> client.find(:first).timesheets.reload
[#]

Реализация метода uniq в классе AssociationCollection – поучительный
пример того, как в Ruby создать набор, содержащий уникальные значения, пользуясь классом Set и методом inject. Она доказывает также,
что уникальность определяется только первичным ключом записи
и ничем больше.
def uniq(collection = self)
seen = Set.new
collection.inject([]) do |kept, record|
unless seen.include?(record.id)
kept u = User.find(:first)
>> u.avatar
=> nil
>> u.build_avatar(:url => '/avatars/smiling')
#
"/avatars/smiling", "user_id"=>1}>
>> u.avatar.save
=> true

Как видите, для создания нового аватара и ассоциирования его с пользователем можно воспользоваться методом build_avatar. Хотя тот факт,
что has_one ассоциирует аватар с пользователем, радует, в нем ничего,
что не умеет делать has_many. Поэтому посмотрим, что происходит, ко­
гда мы присваиваем пользователю новый аватар:
>> u = User.find(:first)
>> u.avatar
=> #"/avatars/smiling",
"user_id"=>1}>
>> u.create_avatar(:url => '/avatars/frowning')
=> #
"/avatars/4567", "id"=>2, "user_id"=>1}, @errors=
#>
>> Avatar.find(:all)
=> [#"/avatars/smiling",
"id"=>"1", "user_id"=>nil}>, #"/avatars/frowning", "id"=>"2", "user_id"=>"1"}>]

Последняя строка – самая интересная, из нее следует, что первоначальный аватар больше не ассоциирован с пользователем. Правда, старый аватар не удален из базы данных, а нам бы этого хотелось. Поэтому зададим параметр :dependent => :destroy, чтобы аватары, более не
ассоциированные ни с каким пользователем, уничтожались:
class User
has_one :avatar, :dependent => :destroy
end

Немного поиграв с консолью, мы можем убедиться, что все работает,
как задумано:

Отношения один-к-одному

249

>> u = User.find(:first)
>> u.avatar
=> #"/avatars/frowning",
"id"=>"2", "user_id"=>"1"}>
>> u.avatar = Avatar.create(:url => "/avatars/jumping")
=> #"avatars/jumping", "id"=>3, "user_id"=>1},
@errors=#>
>> Avatar.find(:all)
=> [#"/avatars/smiling", "id"
=>"1", "user_id"=>nil}>, #
"avatars/jumping","id"=>"3", "user_id"=>"1"}>]

Как видите, параметр :dependent => :destroy помог нам избавиться от
сердитого аватара (frowning), но оставил улыбчивого (smiling). Rails
уничтожает лишь аватар, связь которого с пользователем была разорвана только что, а недействительные данные, которые находились
в базе до этого, там и остаются. Имейте это в виду, когда решите добавить параметр :dependent => :destroy, и не забудьте предварительно удалить плохие данные вручную.
Как я уже упоминал выше, ассоциация has_one часто используется,
чтобы выделить одну интересную запись из уже имеющегося отношения has_many. Предположим, например, что необходимо получить доступ к последнему табелю пользователя:
class User < ActiveRecord::Base
has_many :timesheets
has_one :latest_timesheet, :class_name => 'Timesheet'
end
Мне пришлось задать параметр :class_name, чтобы ActiveRecord знала,
какой объект ассоциировать (она не может вывести имя класса из имени ассоциации :latest_timesheet).
При добавлении отношения has_one в модель, где уже имеется отношение has_many с той же самой моделью, необязательно добавлять еще
один вызов метода belongs_to только ради нового отношения has_one.
На первый взгляд, это противоречит интуиции, но ведь для чтения
данных из базы используется тот же самый внешний ключ, не так ли?
Что произойдет при замене существующего конечного объекта has_one
другим? Это зависит от того, был ли новый связанный объект создан до
или после заменяемого, поскольку ActiveRecord не добавляет никакой
сортировки в запрос, генерируемый для отношения has_one.

Параметры ассоциации has_one
У ассоциации has_one практически те же параметры, что и у has_many.

250

Глава 7. Ассоциации в ActiveRecord

:as
Позволяет организовать полиморфную ассоциацию (см. главу 9).

:class_name
Позволяет задать имя класса, используемого в этой ассоциации. Написав
has_one :latest_timesheet :class_name => 'Timesheet', :class_name => 'Timesheet',
вы говорите, что latest_timesheet – последний объект Timesheet из всех ассоциированных с данным пользователем. Обычно этот параметр Rails
выводит из имени ассоциации.

:conditions
Позволяет задать условия, которым должен отвечать объект, чтобы
быть включенным в ассоциацию. Условия задаются так же, как при
вызове метода ActiveRecord#find:
class User
has_one :manager,
:class_name => 'Person',
:conditions => ["type = ?", "manager"]
end

Здесь manager определен как объект класса Person, для которого поле type =
"manager". Я почти всегда применяю параметр :conditions в сочетаниис отношением has_one. Когда ActiveRecord загружает ассоциацию,
она потенциально может найти много строк с подходящим внешним
ключом. В отсутствие условий (или, быть может, задания сортировки) вы оставляете выбор конкретной записи на усмотрение базы
данных.

:dependent
Параметр :dependent определяет, как ActiveRecord должна поступать
с ассоциированными объектами, когда удаляется их родитель. Этот параметр может принимать несколько значений, которые работают точно так же, как в ассоциации has_many.
Если задать значение :destroy, Rails уничтожит ассоциированный объект, который не связан ни с одним родителем. Значение :delete указывает, что ассоциированный объект следует уничтожить, не активируя
обычных обратных вызовов. Наконец, подразумеваемое по умолчанию
значение :nullify записывает во внешний ключ null, разрывая тем самым связь между объектами.

:foreign_key
Задает имя внешнего ключа в таблице ассоциации.

Несохраненные объекты и ассоциации

251

:include
Разрешает «попутную загрузку» дополнительных объектов вместе с загрузкой ассоциированного объекта. Дополнительную информацию см.
в описании параметра :include для ассоциаций has_many и belongs_to.

:order
Позволяет задать фрагмент предложения SQL для сортировки результатов. В отношениях has_one это особенно полезно, если нужно ассоциировать последнюю запись или что-то в этом роде:
class User
has_one :latest_timesheet,
:class_name => 'Timesheet',
:order => 'created_at desc'
end

Несохраненные объекты и ассоциации
Разрешается манипулировать объектами и ассоциациями до сохранения их в базе данных, но имеется специальное поведение, о котором вы
должны помнить, и связано оно главным образом с сохранением ассоциированных объектов. Считается ли объект несохраненным, зависит
от того, что возвращает метод new_record?.

Ассоциации одинкодному
Присваивание объекта ассоциации типа has_one автоматически сохраняет как сам этот объект, так и объект, который он заместил (если таковой был), чтобы обновить значения в поле внешнего ключа. Исключением из этого правила является случай, когда родительский объект
еще не сохранен, поскольку в данной ситуации значение внешнего
ключа еще неизвестно.
Если сохранение невозможно хотя бы для одного из обновленных объектов (поскольку его состояние недопустимо), операция присваивания
возвращает falsе, и присваивание не выполняется. Это поведение разумно, но может служить источником недоразумений, если вы про него не знаете. Если кажется, что ассоциация не работает должным образом, проверьте правила контроля связанных объектов.
Если по какой-то причине необходимо выполнить присваивание объекта ассоциации has_one без сохранения, можно воспользоваться методом build ассоциации:
user.profile_photo.build(params[:photo])

252

Глава 7. Ассоциации в ActiveRecord

Присваивание объекта ассоциации belongs_to не приводит к сохранению родительского или ассоциированного объекта.

Наборы
Добавление объекта в наборы has_many и has_and_belongs_to_many приводит к автоматическому сохранению при условии, что родительский
объект (владелец набора) уже сохранен в базе данных.
Если объект, добавленный в набор (методом "David"
person.last_name # => "Heinemeier Hansson"

Если один и тот же комплект расширений желательно применить к нескольким ассоциациям, то можно написать модуль расширения вместо блока с определениями методов.

Класс AssociationProxy

253

Ниже реализована та же функциональность, что в листинге 7.5, только помещена она в отдельный модуль Ruby:
module ByNameExtension
def named(name)
first_name, last_name = name.split(" ", 2)
find_or_create_by_first_name_and_last_name(first_name, last_name)
end
end

Теперь этот модуль можно использовать для расширения различных
ассоциаций, лишь бы они были совместимы (для этого примера контракт состоит в том, что должен существовать метод с именем find_or_
create_by_first_name_and_last_name).
class Account < ActiveRecord::Base
has_many :people, :extend => ByNameExtension
end
class Company < ActiveRecord::Base
has_many :people, :extend => ByNameExtension
end

Если вы хотите использовать несколько модулей расширения, то можете передать в параметре :extend не один модуль, а целый массив:
has_many :people, :extend => [ByNameExtension, ByRecentExtension]

В случае конфликта имена методы, определенные в модулях, которые
добавляются позднее, замещают методы, определенные в предшествующих модулях.

Класс AssociationProxy
Класс AssociationProxy, предок прокси-классов всех ассоциаций (если
забыли, обратитесь к рис. 7.1), предоставляет ряд полезных методов,
которые применимы к большинству ассоциаций и бывают необходимы
при написании расширений.

Методы reload и reset
Метод reset восстанавливает начальное состояние прокси-объекта выгруженной ассоциации (кэшированные объекты ассоциаций очищаются). Метод reload сначала вызывает reset, а потом загружает ассоциированные объекты из базы данных.

Методы proxy_owner, proxy_reflection и proxy_target
Возвращают ссылки на внутренние атрибуты owner, reflection и target
прокси-объект ассоциации.

254

Глава 7. Ассоциации в ActiveRecord

Метод proxy_owner возвращает ссылку на объект, владеющий ассоциацией.
Объект proxy_reflection является экземпляром класса ActiveRecord::
Reflection::AssociationReflection и содержит все конфигурационные
параметры ассоциации. В их число входят как параметры, имеющие
значения по умолчанию, так и параметры, которые были явно переданы в момент объявления ассоциации1.
proxy_target – это ассоциированный массив (или сам ассоциированный
объект в случае ассоциаций типа belongs_to и has_one).
На первый взгляд кажется неразумным предоставлять открытый доступ к этим атрибутам и разрешать манипулирование ими. Однако без
доступа к ним писать нетривиальные расширения ассоциаций было бы
гораздо труднее. Методы loaded?, loaded, target и target= объявлены открытыми по той же причине.
В следующем примере демонстрируется использование proxy_owner
в методе расширения published_prior_to, который предложил Уилсон
Билкович:
class ArticleCategory < ActiveRecord::Base
acts_as_tree
has_many :articles do
def published_prior_to(date, options = {})
if proxy_owner.top_level?
Article.find_all_published_prior_to(date, :category =>
proxy_owner)
else
# здесь self – это ассоциация 'articles', поэтому унаследуем
# ее контекст
self.find(:all, options)
end
end
end # расширение has_many :articles
def top_level?
# есть ли у нас родитель и является ли он корневым узлом дерева?
self.parent && self.parent.parent.nil?
end
end
1

Дополнительную информацию о том, когда может быть полезен отражающий объект, а также рассказ об установке ассоциации типа has_many :through
посредством других ассоциаций того же типа см. в статье по адресу http://
www.pivotalblabs.com/articles/2007/08/26/ten-things-ihate-about-proxyobjects-part-i, которую обязательно должен прочесть каждый разработчик
на платформе Rails.

Заключение

255

Подключаемый к ActiveRecord модуль расширения acts_as_tree создает ассоциацию, ссылающуюся на себя по колонке parent_id. Ссылка
proxy_owner используется, чтобы проверить, является ли родитель этой
ассоциации узлом дерева «верхнего уровня».

Заключение
Способность моделировать ассоциации – это то, что превращает Active­
Re­cord в нечто большее, чем просто уровень доступа к базе данных.
Простота и элегантность объявления ассоциаций – причина того, что
ActiveRecord больше, чем обычный механизм объектно-реляционного
отображения.
В этой главе были изложены основные принципы работы ассоциаций
в ActiveRecord. Мы начали с рассмотрения иерархии классов ассоциаций с корнем в AssociationProxy. Хочется надеяться, что знакомство
с внутренними механизмами работы помогло вам проникнуться их
мощью и гибкостью. Ну а руководство по параметрам и методам всех
типов ассоциаций должно стать хорошим подспорьем в повседневной
работе.

8
Валидаторы в ActiveRecord
Компьютеры подобны ветхозаветным богам –
множество правил и никакой пощады.
Джозеф Кэмпбелл

Validations API в ActiveRecord позволяет декларативно объявлять допустимые состояния объектов модели. Методы контроля включены
в разные точки жизненного цикла объекта модели и могут инспектировать объект на предмет того, установлены ли определенные атрибуты,
находятся ли их значения в заданном диапазоне и удовлетворяют ли
другим заданным логическим условиям.
В этой главе мы опишем имеющиеся методы контроля (валидаторы)
и способы их эффективного применения, Мы изучим, как валидаторы
взаимодействуют с атрибутами модели и как можно применить встроенный механизм выдачи сообщений об ошибках в пользовательском
интерфейсе.
Наконец, мы обсудим важный RubyGem-пакет Validatable, который позволяет выйти за пределы встроенных в Rails возможностей и определить
собственные критерии контроля для данного объекта модели в зависимости от того, какую роль он играет в системе в данный момент.

Нахождение ошибок
Проблемы, обнаруживаемые в ходе контроля данных, еще называются
(маэстро, туш…) ошибками! Любой объект модели ActiveRecord содержит

Простые декларативные валидаторы

257

набор ошибок, к которому можно получить доступ с помощью атрибута
с именем (каким бы вы думали?) errors. Это экземпляр класса ActiveRecord:
:Errors, определенного в файле lib/active_record/validations.rb наряду
с прочим кодом, относящимся к контролю.
Если объект модели не содержит ошибок, набор errors пуст. Когда вы
вызываете для объекта модели метод valid?, выполняется целая последовательность шагов для нахождения ошибок. В слегка упрощенном
виде она выглядит так:
1. Очистить набор error
2. Выполнить валидаторы
3. Вернуть признак, показывающий, пуст набор errors или нет
Если набор errors пуст, объект считается корректным. Вот так все просто. Если вы сами пишете валидатор, реализующий логику контроля
(такие примеры имеются в этой главе), то помечаете объект как некорректный, добавляя элементы в набор errors с помощью метода add.
Позже мы рассмотрим класс Errors подробнее. Но сначала имеет смысл
познакомиться собственно с методами контроля.

Простые декларативные валидаторы
Всюду, где возможно, рекомендуется задавать валидаторы модели декларативно с помощью одного или сразу нескольких методов класса,
доступных всем экземплярам ActiveRecord. Если явно не оговорено противное, все методы validates принимают переменное число атрибутов
плюс необязательные параметры. Существуют параметры, общие для
всех валидаторов, их мы рассмотрим в конце раздела.

validates_acceptance_of
Во многих веб-приложениях есть окно, в котором пользователю предлагают согласиться с условиями обслуживания или сделать другой выбор.
Обычно в окне присутствует флажок. Атрибуту, объявленному в этом
валидаторе, не соответствует никакая колонка в базе данных; при вызове данного метода он автоматически создает виртуальные атрибуты
для каждого заданного вами именованного атрибута. Я считаю такой
тип контроля синтаксической глазурью, поскольку он специфичен
именно для веб-приложений.
class Account < ActiveRecord::Base
validates_acceptance_of :privacy_policy, :terms_of_service
end

Сообщение об ошибке
Если валидатор validates_acceptance_of обнаруживает ошибку, то в объекте модели сохраняется сообщение attribute must be accepted (атрибут необходимо принять).

258

Глава 8. Валидаторы в ActiveRecord

Параметр accept
Необязательный параметр :accept позволяет изменить значение, иллюстрирующее согласие. По умолчанию оно равно "1", что соответствует
значению, генерируемому для флажков методами-помощниками Rails.
class Cancellation < ActiveRecord::Base
validates_acceptance_of :account_cancellation, :accept => 'YES'
end

Если в предыдущем примере вы воспользуетесь текстовым полем, связав его с атрибутом account_cancellation, пользователь должен будет
ввести YES, иначе валидатор сообщит об ошибке.

validates_associated
Если с данной моделью ассоциированы другие объекты модели, корректность которых должна проверяться при сохранении, можно применить метод validates_associated, работающий для всех типов ассоциаций. При вызове этого валидатора (по умолчанию в момент сохранения) будет вызван метод valid? каждого ассоциированного объекта.
class Invoice < ActiveRecord::Base
has_many :line_items
validates_associated :line_items
end

Стоит отметить, что безответственное использование метода validates_
associated может привести к циклическим зависимостям и бесконечной рекурсии. Не бесконечной, конечно, просто программа рухнет.
Если взять предыдущий пример, то не следует делать нечто подобное
в классе LineItem:
class LineItem < ActiveRecord::Base
belongs_to :invoice
validates_associated :invoice
end

Этот валидатор не сообщит об ошибке, если ассоциация равна nil, так
как в этот момент ее еще просто не существует. Если вы хотите убедиться, что ассоциация заполнена и корректна, то должны будете использовать validates_associated в сочетании с validates_presence_of
(рассматривается ниже).

validates_confirmation_of
Метод validates_confirmation_of дает еще один пример синтаксической
глазури для веб-приложений, где часто встречаются пары дублирующих текстовых полей, в которые пользователь должен ввести одно и то
же значение, например пароль или адрес электронной почты. Этот валидатор создает виртуальный атрибут для значения в поле подтверж-

Простые декларативные валидаторы

259

дения и сравнивает оба атрибута. Чтобы объект считался корректным,
значения атрибутов должны совпадать.
Вот пример все для той же фиктивной модели Account:
class Account < ActiveRecord::Base
validates_confirmation_of :email, :password
end

В пользовательский интерфейс для модели Account необходимо включить дополнительные текстовые поля, имена которых заканчиваются
суффиксом _confirmation. В отправленной форме значения в этих полях должны совпадать со значениями в парных им полях без суффикса
_confirmation.

validates_each
Метод validates_each отличается от своих собратьев тем, что для него не
определена конкретная функция контроля. Вы просто передаете ему
массив имен атрибутов и блок, в котором проверяется допустимость
значения каждого из них.
Блок может пометить объект модели как некорректный, поместив информацию об ошибках в набор errors. Значение, возвращаемое блоком,
игнорируется.
Ситуаций, в которых этот метод может пригодиться, не так уж много;
примером может служить контроль с помощью внешних служб. Вы можете представить такой внешний валидатор в виде фасада, специфичного для своего приложения, и вызвать его с помощью блока validates_
each:
class Invoice < ActiveRecord::Base
validates_each :supplier_id, :purchase_order do |record, attr, value|
record.errors.add(attr) unless PurchasingSystem.validate(attr, value)
end
end

Отметим, что параметры для экземпляра модели (record), имя атрибута и проверяемое значение передаются в виде параметров блока:

validates_inclusion_of и validates_exclusion_of
Метод validates_inclusion_of и парный ему validates_exclusion_of очень
полезны, но, если вы не благоговейно относитесь к требованиям, предъявляемым к приложению, то готов поспорить на небольшую сумму,
что не понимаете, зачем они нужны.
Эти методы принимают переменное число имен атрибутов и необязательный параметр :in. Затем они проверяют, что значение атрибута
входит (или соответственно не входит) в перечисляемый объект, переданный в :in.

260

Глава 8. Валидаторы в ActiveRecord

Примеры, приведенные в документации по Rails, наверное, лучше всего иллюстрируют применение этих методов, поэтому я возьму их за основу:
class Person < ActiveRecord::Base
validates_inclusion_of :gender, :in => ['m','f'],
:message => 'Среднего рода?'
class Account
validates_exclusion_of :login,
:in => ['admin', 'root', 'superuser'],
:message => 'Борат говорит: "Уйди, противный!"'
end

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

validates_existence_of
Этот валидатор реализован в подключаемом модуле, но я считаю его настолько полезным в повседневной работе, что решил включить в свой
перечень. Он проверяет, что внешний ключ в ассоциации belongs_to ссылается на существующую запись в базе данных. Можете считать это огра­
ни­чением внешнего ключа, реализованным на уровне Rails. Валидатор
также хорошо работает с полиморфными ассоциациями belongs_to.
class Person < ActiveRecord::Base
belongs_to :address
validates_existence_of :address
end

Саму идею и реализующий ее подключаемый модуль предложил Джош
Сассер, который написал в своем блоге следующее:
Меня всегда раздражало отсутствие валидатора, проверяющего,
ссылается ли внешний ключ на существующую запись. Есть валидатор validates_presence_of, который проверяет, что внешний ключ не
равен nil. А validates_associated сообщит, если для записи, на которую ссылается этот ключ, не проходят ее собственные проверки. Но
это либо слишком мало, либо слишком много, а мне нужно нечто среднее. Поэтому я решил, что пора написать собственный валидатор.
http://blog.hasmanythrough.com/2007/7/14/validate-your-existence

Чтобы установить этот дополнительный модуль, зайдите в каталог
своего проекта и выполните следующую команду:
$ script/plugin install
http://svn.hasmanythrough.com/public/plugins/validates_existence/

Простые декларативные валидаторы

261

Если параметр :allow_nil => true, то сам ключ может быть равен nil,
и никакой контроль тогда не выполняется. Если же ключ отличен от
nil, посылается запрос с целью удостовериться, что запись с таким внешним ключом существует в базе данных. По умолчанию в случае ошибки выдается сообщение does not exist (не существует), но, как и в других валидаторах, его можно переопределить с помощью параметра :
message.

validates_format_of
Чтобы применять валидатор validates_format_of, вы должны уметь
пользоваться регулярными выражениями в Ruby. Передайте этому методу один или несколько подлежащих проверке атрибутов и регулярное выражение в параметре :with (обязательном). В документации по
Rails приведен хороший пример – проверка формата адреса электронной почты:
class Person < ActiveRecord::Base
validates_format_of :email,
:with => /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\Z/i
end

Кстати, этот валидатор не имеет ничего общего со спецификацией электронных почтовых адресов в RFC1.

Говорит Кортенэ…
Регулярные выражения – замечательный инструмент, но иногда они бывают очень сложными, особенно если нужно проверять
доменные имена или адреса электронной почты.
Чтобы разбить длинное регулярное выражение на обозримые
куски, можно воспользоваться механизмом интерполяции #{ }:
validates_format_of :name, :with =>
/^((localhost)|#{DOMAIN}|#{NUMERIC_IP})#{PORT}$/

Это выражение понять довольно легко.
Сами же подставляемые константы сложнее, но разобраться в них
проще, чем если бы все было свалено в кучу:
PORT = /(([:]\d+)?)/
DOMAIN = /([a-z0-9\-]+\.?)*([a-z0-9]{2,})\.[a-z]{2,}/
NUMERIC_IP = /(?>(?:1?\d?\d|2[0-4]\d|25[05])\.){3}(?:1?\d?\d|2[0-4]\d|25[05])(?:\/(?:[12]?\d|3[012])|-(?> (?:1?\d?\d|2[04]\d|25[0-5])\.){3}(?:1?\d?\d|2[0-4]\d|25[0-5]))?/

1

Если вам нужно контролировать электронные адреса, попробуйте подключаемый модуль по адресу http://code.dunae.ca/validates_email_format_of.

262

Глава 8. Валидаторы в ActiveRecord

validates_length_of
Метод validates_length_of принимает различные параметры, позволяющие точно задать ограничения на длину одного из атрибутов модели.
class Account < ActiveRecord::Base
validates_length_of :login, :minimum => 5
end

Параметры, задающие ограничения
В параметрах :minimum и :maximum нет никаких неожиданностей, только
не надо использовать их вместе. Чтобы задать диапазон, воспользуйтесь параметром :within и передайте в нем диапазон Ruby, как показано в следующем примере:
class Account < ActiveRecord::Base
validates_length_of :login, :within => 5..20
end

Чтобы задать точную длину атрибута, воспользуйтесь параметром :is:
class Account < ActiveRecord::Base
validates_length_of :account_number, :is => 16
end

Параметры, управляющие сообщениями об ошибках
Rails позволяет задать детальное сообщение об ошибке, обнаруженной
валидатором validates_length_of с помощью параметров :too_long, :too_
short и :wrong_length. Включите в текст сообщения спецификатор %d,
который будет замещен числом, соответствующим ограничению:
class Account < ActiveRecord::Base
validates_length_of :account_number, :is => 16,
:wrong_length => "длина должна составлять %d знаков"
end

validates_numericality_of
Несколько коряво названный метод validates_numericality_of служит
для проверки того, что атрибут содержит числовое значение. Параметр
:integer_only позволяет дополнительно указать, что значение должно
быть целым, и по умолчанию равен false.
class Account < ActiveRecord::Base
validates_numericality_of :account_number, :integer_only => true
end

validates_presence_of
Один из наиболее употребительных валидаторов :validates_presence_of
гарантирует, что обязательный атрибут задан. Для проверки значения

Простые декларативные валидаторы

263

используется метод blank?, определенный в классе Object, который возвращает true, если значение равно nil или пустой строке "".
class Account < ActiveRecord::Base
validates_presence_of :login, :email, :account_number
end

Проверка наличия ассоциированных объектов
Желая проверить наличие ассоциации, применяйте валидатор к атрибуту, содержащему внешний ключ, а не к самой ассоциации. Отметим,
что контроль завершится неудачей, если оба объекта – родитель и потомок – еще не сохранены (поскольку в этом случае внешний ключ будет пуст).

validates_uniqueness_of
Метод validates_uniqueness_of проверяет, что значение атрибута уникально среди всех моделей одного и того же типа. При этом не добавляется ограничение уникальности на уровне базы данных. Вместо этого
строится и выполняется запрос на поиск подходящей записи в базе. Если запись будет найдена, валидатор сообщит об ошибке.
class Account < ActiveRecord::Base
validates_uniqueness_of :login
end

Параметр :scope позволяет использовать дополнительные атрибуты
для проверки уникальности. С его помощью можно передать одно или
несколько имен атрибутов в виде символов (несколько символов помещаются в массив).
class Address < ActiveRecord::Base
validates_uniqueness_of :line_two, :scope => [:line_one, :city, :zip]
end

Можно также указать, должны ли строки сравниваться с учетом регистра; для этого служит параметр :case_sensitive (для не-текстовых атрибутов он игнорируется).

Гарантии уникальности модели соединения
При использовании моделей соединения (посредством has_many :through)
довольно часто возникает необходимость сделать отношение уникальным. Рассмотрим пример, в котором моделируется запись студентов на
посещение курсов:
class Student < ActiveRecord::Base
has_many :registrations
has_many :courses, :through => :registrations
end

264

Глава 8. Валидаторы в ActiveRecord
class Registration < ActiveRecord::Base
belongs_to :student
belongs_to :course
end
class Course < ActiveRecord::Base
has_many :registrations
has_many :students, :through => :registrations
end

Как гарантировать, что студент не запишется на один и тот же курс более одного раза? Самый короткий способ – воспользовать валидатором
validates_uniqueness_of с ограничением :scope. Но не забывайте, что указывать необходимо внешние ключи, а не имена самих ассоциаций:
class Registration < ActiveRecord::Base
belongs_to :student
belongs_to :course
validates_uniqueness_of :student_id, :scope => :course_id,
:message => "может записаться на каждый курс только один раз"
end

Поскольку в случае неудачи этой проверки сообщение об ошибке, принимаемое по умолчанию, бессмысленно, я задал собственное сообщение, которое после подстановки будет выглядеть так: «Student может
записаться на каждый курс только один раз».

Исключение RecordInvalid
Если вы выполняете операции с восклицательным знаком (например,
save!) или Rails самостоятельно пытается выполнить сохранение,
и при этом валидатор обнаруживает ошибку, будьте готовы к обработке исключения ActiveRecord::RecordInvalid. Оно возникает в случае
ошибки при контроле, а в сопроводительном сообщении описывается
причина ошибки.
Вот простой пример, взятый из одного моего приложения, в котором
модель User подвергается довольно строгим проверкам:
>> u = User.new
=> #
>> u.save!
ActiveRecord::RecordInvalid: Validation failed: Name can't be blank,
Password confirmation can't be blank, Password is too short (minimum
is 5 characters), Email can't be blank, Email address format is bad

Общие параметры валидаторов
Перечисленные ниже параметры применимы ко всем методам контроля.

Общие параметры валидаторов

265

:allow_nil
Во многих случаях контроль необходим, только если значение задано,
поскольку пустое значение допустимо. Параметр :allow_nil позволяет
пропустить проверку, если значение атрибута равно nil. Помните, что
значение сравнивается только с nil, на пустые строки этот параметр не
распространяется.

:if
Параметр :if рассматривается в следующем разделе «Условная проверка».

:message
Выше мы уже говорили, что ошибки, обнаруженные в процессе контроля, запоминаются в наборе Errors проверяемого объекта модели.
Частью любого элемента этого набора является сообщение, описывающее причину ошибки. Все валидаторы принимают параметр :message,
позволяющий переопределить сообщение, подразумеваемое по умолчанию.
class Account < ActiveRecord::Base
validates_uniqueness_of :login, :message => "is already taken"
end

:on
По умолчанию валидаторы запускаются при любом сохранении (в операциях создания и обновления). При необходимости можно ограничиться только одной из этих операций, передав в параметре :on значение :create или :update.
Например, режим :on => :create удобно использовать в сочетании
с валидатором validates_uniqueness_of, поскольку проверка уникальности при больших наборах данных может занимать много времени.
class Account < ActiveRecord::Base
validates_uniqueness_of :login, :on => :create
end

Минуточку – а не приведет ли это к проблемам, если при обновлении
модели в будущем будет задано неуникальное значение? Вот тут-то на
сцену выходит метод attr_protected. С его помощью важные атрибуты
модели можно защитить от массового присваивания. В действии контроллера, которое создает новые учетные записи, вы должны будете
уста­новить значение параметра login вручную.

266

Глава 8. Валидаторы в ActiveRecord

Условная проверка
Все методы контроля принимают параметр :if, который позволяет во
время выполнения (а не на стадии определения класса) решить, нужна
ли проверка.
При вызове показанного ниже метода evaluate_condition из класса ActiveRecord::Validations ему передается значение параметра :if в качестве
параметра condition и проверяемый объект модели в качестве параметра record:
# Определить по заданному условию condition, нужно ли проверять запись
# record (условие может быть задано в виде блока, метода или строки)
def evaluate_condition(condition, record)
case condition
when Symbol: record.send(condition)
when String: eval(condition, binding)
else
if condition_block?(condition)
condition.call(record)
else
raise ActiveRecordError,
"Должен быть символ, строка (передаваемая eval) или Proc-объект"
end
end
end

Анализ предложения case в реализации этого метода показывает, что
параметр :if можно задать тремя способами:
• Symbol – имя вызываемого метода передается в виде символа. Пожалуй, это самый распространенный вариант, обеспечивающий наивысшую производительность;
• String – задавать кусок кода на Ruby, интерпретируемый с помощью eval, может быть удобно, если условие совсем короткое. Но
помните, что динамическая интерпретация кода работает довольно
медленно;
• блок – Proc-объект, передаваемый методу call. Наверное, самый элегантный выбор для однострочных условий.

Замечания по поводу применения
Когда имеет смысл применять условные проверки? Ответ такой: всякий раз, когда сохраняемый объект может находиться в одном из нескольких допустимых состояний.
В качестве типичного примера (используется в подключаемом модуле
acts_as_authenticated) приведем модель User (или Person), применяемую
при регистрации и аутентификации:
validates_presence_of :password, :if => :password_required?

Работа с объектом Errors

267

validates_presence_of :password_confirmation, :if =>
:password_required?
validates_length_of :password, :within => 4..40,
:if=>:password_required?
validates_confirmation_of :password, :if => :password_required?

Этот код не отвечает принципу DRY (то есть в нем встречаются повторения). О его переработке с помощью метода with_options см. в главе 14
«Регистрация и аутентификация». Там же подробно рассматривается
применение и реализация подключаемого модуля acts_as_authenticated.
Существует лишь два случая, когда для корректности модели необходимо наличие поля пароля (в открытом виде):
protected
def password_required?
crypted_password.blank? || !password.blank?
end

Первый случай – когда атрибут crypted_password пуст, поскольку это означает, что мы имеем дело с новым экземпляром класса User, еще не
имеющим пароля. Второй случай – когда сам атрибут password не пуст;
быть может, это свидетельствует об операции обновления и о том, что
пользователь пытается переустановить свой пароль.

Работа с объектом Errors
Ниже приведен перечень стандартных текстов сообщений об ошибках,
взятый непосредственно из кода Rails:
@@default_error_messages = {
:inclusion => "is not included in the list",
:exclusion => "is reserved",
:invalid => "is invalid",
:confirmation => "doesn't match confirmation",
:accepted => "must be accepted",
:empty => "can't be empty",
:blank => "can't be blank",
:too_long => "is too long (maximum is %d characters)",
:too_short => "is too short (minimum is %d characters)",
:wrong_length => "is the wrong length (should be %d characters)",
:taken => "has already been taken",
:not_a_number => "is not a number"
}

Как мы уже отмечали, для формирования окончательного сообщения
об ошибке контроля в начало любой из этих строк дописывается имя
атрибута с заглавной буквы. Не забывайте, что стандартное сообщение
можно переопределить с помощью параметра :message.

268

Глава 8. Валидаторы в ActiveRecord

Манипулирование набором Errors
Есть ряд методов, позволяющих вручную добавлять информацию об
ошибках в набор Errors и изменять его состояние.

add_to_base(msg)
Добавляет сообщение об ошибке, относящееся к состоянию объекта
в целом, а не к значению конкретного атрибута. Сообщение должно
быть законченным предложением, поскольку Rails не применяет к нему никакой дополнительной обработки.

add(attribute, msg)
Добавляет сообщение об ошибке, относящееся к конкретному атрибуту.
Сообщение должно быть фрагментом предложения, которое приобретает
смысл после дописывания в начало имени атрибута с заглавной буквы.

clear
Как и следовало ожидать, метод clear очищает набор Errors.

Проверка наличия ошибок
Есть также два метода, позволяющих узнать, есть ли в объекте Errors
ошибки, относящиеся к конкретным атрибутам.

invalid?(attribute)
Возвращает true или false в зависимости от наличия ошибок, относящихся к атрибуту attribute.

on(attribute)
Возвращает значения разных типов в зависимости от состояния набора
ошибок, относящихся к атрибуту attribute. Возвращает nil, если для
данного атрибута нет ни одной ошибки. Возвращает строку с сообщением об ошибке, если с данным атрибутом ассоциирована ровно одна
ошибка. Возвращает массив строк с сообщениями об ошибках, если
с данным атрибутом ассоциировано несколько ошибок.

Нестандартный контроль
Вот мы и подошли к вопросу о нестандартных методах контроля, к которым вы можете прибегнуть, если нормальная декларативная форма
вас не устраивает.

Нестандартный контроль

269

Выше в этой главе я описал процедуру нахождения ошибок во время
контроля, оговорившись, что объяснение было несколько упрощенным. Ниже приведена фактическая реализация, поскольку, на мой
взгляд, она чрезвычайно элегантна, легко читается и помогает понять,
куда именно можно поместить нестандартную логику контроля.
def valid?
errors.clear
run_validations(:validate)
validate
if new_record?
run_validations(:validate_on_create)
validate_on_create
else
run_validations(:validate_on_update)
validate_on_update
end
errors.empty?
end

Здесь есть три обращения к методу run_validations, который и запускает декларативные валидаторы, если таковые были определены.
Кроме того, имеется три метода обратного вызова (абстрактных?),
которые специально оставлены в модуле Validations без реализации.
При необходимости вы можете переопределить их в своей модели
ActiveRecord.
Нестандартные методы контроля полезны для проверки состояния
объекта в целом, а не его отдельных атрибутов. За неимением лучшего
примера предположим, что вы работаете с объектом модели с тремя целочисленными атрибутами (:attr1, :attr2 и :attr3), а также заранее вычисленной суммой (:total). Атрибут :total всегда должен быть равен
сумме трех других атрибутов:
class CompletelyLameTotalExample < ActiveRecord::Base
def validate
if total != (attr1 + attr2 + attr3)
errors.add_to_base("Сумма не сходится!")
end
end
end

Помните: чтобы пометить объект как некорректный, необходимо добавить информацию об ошибках в набор Errors. Значение, возвращаемое
валидатором, не используется.

270

Глава 8. Валидаторы в ActiveRecord

Отказ от контроля
Модуль Validations, примешанный к классу ActiveRecord::Base, влияет
на три метода экземпляра, как видно из следующего фрагмента (взят из
файла activerecord/lib/active_record/ validations.rb, входящего в дистрибутив Rails):
def self.included(base) # :nodoc:
base.extend ClassMethods
base.class_eval do
alias_method_chain :save, :validation
alias_method_chain :save!, :validation
alias_method_chain :update_attribute, :validation_skipping
end
end

Затрагиваются методы save, save! и update_attribute. Процедуру контроля для методов save и save! можно опустить, передав методу параметр false.
Впервые наткнувшись на вызов save(false) в коде Rails, я был слегка
ошарашен. Я подумал: «Не припомню, чтобы у метода save был параметр», потом заглянул в документацию по API, и оказалось, что память меня не подводит! Заподозрив, что документация врет, я полез
смотреть реализацию этого метода в классе ActiveRecord::Base. Нет никакого параметра. «Что за черт! Добро пожаловать в чудесный мир
Ruby, – сказал я себе. – Как же выходит, что я не получаю ошибку
о лишнем аргументе?»
В конечном итоге то ли я сам догадался, то ли кто-то подсказал: нормальный метод Base#save подменяется, когда примешивается модуль
Validations, а по умолчанию так оно и есть. Из-за наличия alias_method_
chain вы получаете открытый, хотя и недокументированный метод
save_without_validation, и, как мне кажется, с точки зрения сопровождения, это куда понятнее, чем save(false).
А что насчет метода update_attribute? Модуль Validations переопределяет принимаемую по умолчанию реализацию, заставляя ее вызвать
save(false). Это короткий фрагмент, поэтому я приведу его целиком:
def update_attribute_with_validation_skipping(name, value)
send(name.to_s + '=', value)
save(false)
end

Вот почему update_attribute не вызывает валидаторов, хотя родственный ему метод update_attributes вызывает; этот вопрос очень часто задают в списках рассылки. Тот, кто писал документацию по API, полагает, что это поведение «особенно полезно для булевых флагов в суще­
ствующих записях».

Заключение

271

Не знаю, правда это или нет, зато точно знаю, что это источник постоянных споров в сообществе. К сожалению, я мало что могу добавить,
кроме простого совета, продиктованного здравым смыслом: «Будьте
очень осторожны, применяя метод update_attribute. Он легко может сохранить объекты ваших моделей в противоречивом состоянии».

Заключение
В этой относительно короткой главе мы подробно рассмотрели API валидаторов в ActiveRecord. Один из самых притягательных аспектов
Rails – возможность декларативно задавать критерии корректности
объектов моделей.

9
Дополнительные возможности
ActiveRecord
ActiveRecord – это простая структура объектно-реляционного отображения (ORM), если сравнивать ее с другими подобными модулями, например Hibernate в Java. Но пусть это не вводит вас в заблуждение –
несмотря на скромный экстерьер, в ActiveRecord немало весьма продвинутых функций. Для максимально эффективной работы с Rails вы
должны освоить не только основы ActiveRecord, но и понимать, например, когда имеет смысл выйти за пределы паттерна «одна таблица –
один класс» или как пользоваться модулями Ruby, чтобы избавиться
от дублирования и сделать свой код чище.
В этой главе мы завершим рассмотрение ActiveRecord, уделив внимание обратным вызовам, наблюдателям, наследованию с одной таблицей (single-table inheritance – STI) и полиморфным моделям. Мы также
немного поговорим о метапрограммировании и основанных на Ruby
предметно-ориентированных языках (domain-specific language – DSL)
применительно к ActiveRecord.

Обратные вызовы
Эта функция ActiveRecord позволяет умелому разработчику подключить то или иное поведение в различные точки жизненного цикла модели, например после инициализации, перед вставкой, обновлением
или удалением записи из базы данных и т. д.

Обратные вызовы

273

С помощью обратных вызовов можно решать самые разные задачи – от
простого протоколирования и модификации атрибутов перед выполнением контроля до сложных вычислений. Обратный вызов может прервать жизненный цикл. Некоторые обратные вызовы даже позволяют
на лету изменять поведение класса модели. В этом разделе мы изучим
все упомянутые сценарии, но сначала посмотрим, как выглядит обратный вызов. Взгляните на следующий незамысловатый пример:
class Beethoven < ActiveRecord::Base
before_destroy :last_words
...
protected
def last_words
logger.info "Рукоплещите, друзья, комедия окончена"
end
end

Итак, перед смертью (пардон, уничтожением методом destroy) класс
Beethoven произносит прощальные слова, которые будут запротоколированы навечно. Как мы скоро увидим, существует 14 разных способов
добавить подобное поведение в модель. Но прежде, чем огласить весь
список, поговорим о механизме регистрации обратного вызова.

Регистрация обратного вызова
Вообще-то, самый распространенный способ зарегистрировать обратный вызов – поместить его в начало класса, воспользовавшись типичным для Rails методом класса в стиле макроса. Но есть и более многословный путь к той же цели. Просто реализуйте обратный вызов как
метод в своем классе. Иными словами, предыдущий пример можно было бы записать и так:
class Beethoven < ActiveRecord::Base
...
protected
def before_destroy
logger.info "Рукоплещите, друзья, комедия окончена"
end
end

Это тот редкий случай, когда более лаконичное решение оказывается
хуже. На самом деле, почти всегда предпочтительнее – осмелюсь даже
сказать, что в этом состоит путь Rails, – использовать обратные вызовы
в виде макросов, а не реализовывать их в виде методов, и вот почему:

274

Глава 9. Дополнительные возможности ActiveRecord

• объявления обратных вызовов в виде макросов размещаются в начале определения класса, то есть само наличие обратного вызова
становится более очевидным по сравнению с размещением тела метода где-то в середине файла;
• обратные вызовы в виде макросов ставятся в очередь. Это означает,
что к одной и той же точке в жизненном цикле можно подключить
более одного метода. Обратные вызовы выполняются в том порядке,
в котором были помещены в очередь;
• обратные вызовы для одной и той же точки расширения можно поставить в очередь на разных уровнях иерархии наследования, и они все
равно будут работать, не замещая друг друга, как в случае методов;
• обратные вызовы, реализованные как методы модели, всегда вызываются в последнюю очередь.

Однострочные обратные вызовы
Если (и только если) процедура обратного вызова совсем коротенькая1,
вы можете добавить ее, передав блок макросу обратного вызова. Коротенькая – значит состоящая из одной строки!
class Napoleon < ActiveRecord::Base
before_destroy {|r| logger.info "Josephine..." }
...
end

Защищенный или закрытый
За исключением случаев, в которых используется блок, методы обратных вызовов должны быть защищенными или закрытыми. Только не
открытыми, поскольку метод обратного вызова никогда не должен вызываться из кода вне модели.
Хотите верьте, хотите нет, но есть и другие способы реализации обратных вызовов, однако их мы рассмотрим ниже. А пока приведем перечень точек расширения, к которым можно подключить обратные вызовы.

Парные обратные вызовы before/after
Всего существует 14 типов обратных вызовов, которые можно зарегистрировать в моделях! Двенадцать из них – это пары before/after, напри1

Если вы заглядывали в исходный код старых версий Rails, то, возможно,
встречали обратные вызовы в виде макросов, получающие короткую строку кода на Ruby, которую предстояло интерпретировать (eval) в контексте
объекта модели. Начиная с Rails 1.2 такой способ добавления обратных вызовов объявлен устаревшим, потому что в подобных ситуациях всегда лучше использовать блоки.

Обратные вызовы

275

мер before_validation и after_validation (оставшиеся два, after_initialize
и after_find – особые случаи, которые мы обсудим позже).

Перечень обратных вызовов
Ниже приведен перечень точек расширения, вызываемых в ходе операции save (этот перечень немного различен для сохранения новой и существующей записи):
• before_validation;
• before_validation_on_create;
• after_validation;
• after_validation_on_create;
• before_save;
• before_create (для новых записей) и before_update (для существующих записей);
• ActiveRecord обращается к базе данных и выполняет INSERT или UPDATE;
• after_create (для новых записей) и before_update (для существующих записей);
• after_save.
Для операций удаления определены еще два обратных вызова:
• before_destroy;
• ActiveRecord обращается к базе данных и выполняет DELETE;
• after_destroy вызывается после замораживания всех атрибутов (они
делаются доступными только для чтения).

Прерывание выполнения
Если вы вернете из метода обратного вызова булево значение false (не
nil), то ActiveRecord прервет цепочку выполнения. Больше никакие обратные вызовы не активируются. Метод save возвращает false, а save!
возбуждает исключение RecordNotSaved.
Имейте в виду, что в Ruby метод неявно возвращает значение последнего вычисленного выражения, поэтому при написании обратных вызовов часто допускают ошибку, которая приводит к непреднамеренному прерыванию выполнения. Если объект, содержащий обратные вызовы, по какой-то таинственной причине не хочет сохраняться, проверьте, не возвращает ли обратный вызов false.

Примеры применения обратных вызовов
Разумеется, решение о том, какой обратный вызов использовать в данной ситуации, зависит от целей, которых вы хотите добиться. Я не мо-

276

Глава 9. Дополнительные возможности ActiveRecord

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

Сброс форматирования атрибутов
с помощью before_validate_on_create
Типичный пример использования обратных вызовов before_validate
касается очистки введенных пользователем атрибутов. Например,
в следующем классе CreditCard (цитирую документацию по Rails API)
атрибут number предварительно нормализуется, чтобы предотвратить
ложные срабатывания валидатора:
class CreditCard < ActiveRecord::Base
...
private
def before_validation_on_create
# Убрать из номера кредитной карты все, кроме цифр
self.number = number.gsub(/[^0-9]/, "")
end
end

Геокодирование с помощью before_save
Предположим, что ваше приложение хранит адреса и умеет наносить
их на карту. Перед сохранением для адреса необходимо выполнить геокодирование (нахождение координат), чтобы потом можно было легко
поместить точку на карту1.
Как часто бывает, сама формулировка требования наводит на мысль об
использовании обратного вызова before_save:
class Address < ActiveRecord::Base
include GeoKit::Geocoders
before_save :geolocate
validates_presence_of :line_one, :state, :zip
...
private
def geolocate
res = GoogleGeocoder.geocode(to_s)
self.latitude = res.lat
self.longitude = res.lng
end
end
1

Рекомендую отличный подключаемый модуль GeoKit for Rails, который
находится по адресу http://geokit.rubyforge.org/.

Обратные вызовы

277

Прежде чем двигаться дальше, сделаем парочку замечаний. Предыдущий код прекрасно работает, если геокодирование завершилось успешно. А если нет? Надо ли в этом случае сохранять запись? Если не
надо, цепочку выполнения следует прервать:
def geolocate
res = GoogleGeocoder.geocode(to_s)
return false if not res.success # прервать выполнение
self.latitude = res.lat
self.longitude = res.lng
end

Но остается еще одна проблема – вызывающая программа (а, стало
быть, и конечный пользователь) ничего не знает о том, что цепочка была прервана. Хотя мы сейчас не находимся в валидаторе, я думаю, что
было бы уместно поместить информацию об ошибке в набор errors:
def geolocate
res = GoogleGeocoder.geocode(to_s)
if res.success
self.latitude = res.lat
self.longitude = res.lng
else
errors.add_to_base("Ошибка геокодирования. Проверьте адрес.")
return false
end
end

Если выполнить геокодирование не удалось, мы добавляем сообщение
об ошибке для объекта в целом, прерываем выполнение и не сохраняем
запись.

Перестраховка с помощью before_destroy
Что если приложение обрабатывает очень важные данные, которые,
будучи раз введены, уже не должны удаляться? Может быть, имеет
смысл вклиниться в механизм удаления ActiveRecord и вместо того,
чтобы реально удалять запись, просто пометить ее как удаленную?
В следующем примере предполагается, что в таблице accounts имеется
колонка deleted_at типа datetime.
class Account < ActiveRecord::Base
...
def before_destroy
update_attribute(:deleted_at, Time.now) and return false
end
end

Я решил реализовать этот обратный вызов в виде метода, гарантируя
тем самым, что он будет выполнен последним в очереди, связанной

278

Глава 9. Дополнительные возможности ActiveRecord

с точкой расширения before_destroy. Он возвращает false, поэтому выполнение прерывается и запись не удаляется из базы данных1.
Пожалуй, стоит отметить, что при определенных обстоятельствах Rails
позволяет случайно обойти обратные вызовы before_destroy:
• методы delete и delete_all класса ActiveRecord::Base почти идентичны. Они напрямую удаляют строки из базы данных, не создавая экземпляров моделей, а это означает, что никаких обратных вызовов
не будет;
• объекты моделей в ассоциациях, заданных с параметром :dependent =>
:delete_all, удаляются напрямую из базы данных одновременно с удалением из набора с помощью методов ассоциации clear или delete.

Стирание ассоциированных файлов с помощью after_destroy
Если с объектом модели ассоциированы какие-то файлы, например
вложения или загруженные картинки, то при удалении объекта можно стереть и эти файлы с помощью обратного вызова after_destroy. Хорошим примером может служить следующий метод из великолепного
подключаемого модуля AttachmentFu2 Рика Олсона:
# Стирает файл. Вызывается из обратного вызова after_destroy
def destroy_file
FileUtils.rm(full_filename)
...
rescue
logger.info "Исключение при стирании #{full_filename ... }"
logger.warn $!.backtrace.collect { |b| " > #{b}" }.join("\n")
end

Особые обратные вызовы: after_initialize и after_find
Обратный вызов after_initialize активируется при создании новой модели ActiveRecord (с нуля или из базы данных). Его наличие позволяет
обойтись без переопределения самого метода initialize.
Обратный вызов after_find активируется, когда ActiveRecord загружает
объект модели из базы данных и предшествует after_initialize, если реализованы оба. Поскольку методы поиска вызывают after_find и after_
1

В реальной программе надо было бы модифицировать еще все методы поиска, так чтобы в часть WHERE добавлялось условие deleted_at is NULL; в противном случае записи, помеченные как удаленные, будут по-прежнему видны.
Это нетривиальная задача, но, к счастью, вам не придется решать ее самостоятельно. Рик Олсон написал подключаемый модуль ActsAsParanoid, который именно это и делает; вы можете найти его по адресу http://svn.technoweenie.net/projects/plugins/acts_as_paranoid.

2

Скачать AttachmentFu можно по адресу http://svn.techno-weenie.net/
projects/plugins/attachment_fu.

Обратные вызовы

279

initialize для каждого найденного объекта, то из соображений производительности реализовывать их следует как методы, а не как макросы.
Что если нужно выполнить некоторый код только при первом создании экземпляра модели, а не после каждой его загрузки из базы? Такой обратный вызов не предсумотрен, но это можно сделать с помощью
after_initialize. Просто добавьте проверку на новую запись:
def after_initialize
if new_record?
...
end
end

Написав много приложений Rails, я обнаружил, что удобно хранить
предпочтения пользователя в сериализованном хеше, ассоциированном с объектом User. Реализовать эту идею позволяет метод serialize
моделей ActiveRecord, который прозрачно сохраняет граф объектов
Ruby в текстовой колонке таблицы в базе данных. К сожалению, ему
нельзя передать значение по умолчанию, поэтому я вынужден задавать
его самостоятельно:
class User < ActiveRecord::Base
serialize :preferences # по умолчанию nil
...
private
def after_initialize
self.preferences ||= Hash.new
end
end

В обратном вызове after_initialize я могу автоматически заполнить
атрибут preferences модели пользователя, записав в него пустой хеш,
поэтому мне не придется проверять его на nil при таком способе доступа: user.preferences [:show_help_text] = false. Конечно, хранить в сериализованном виде имеет смысл только данные, которые не будут фигурировать в SQL-запросах.
Средства метапрограммирования Ruby в сочетании с возможностью
выполнять код в момент загрузки модели с помощью обратного вызова
after_find – это поистине «гремучая смесь». Поскольку мы еще не закончили изучение обратных вызовов, я вернусь к вопросу об использовании after_find ниже в разделе «Модификация классов ActiveRecord
во время выполнения».

Классы обратных вызовов
Достаточно часто возникает желание повторно использовать код обратного вызова для нескольких объектов. Поэтому Rails позволяет писать так называемые классы обратных вызовов. Вам нужно лишь пере-

280

Глава 9. Дополнительные возможности ActiveRecord

дать в очередь данного обратного вызова объект, который отвечает на
имя этого обратного вызова и принимает объект модели в качестве параметра.
Вот пример из раздела о перестраховке, записанный в виде класса обратного вызова:
class MarkDeleted
def self.before_destroy(model)
model.update_attribute(:deleted_at, Time.now) and return false
end
end

Поскольку класс MarkDeleted не обладает состоянием, я реализовал обратный вызов в виде метода класса. Поэтому не придется создавать
объекты MarkDeleted только ради вызова этого метода. Достаточно просто передать класс в очередь обратного вызова моделей, которые должны обладать поведением «пометить вместо удаления»:
class Account < ActiveRecord::Base
before_destroy MarkDeleted
...
end
class Invoice < ActiveRecord::Base
before_destroy MarkDeleted
...
end

Несколько методов обратных вызовов в одном классе
Нет такого закона, который запрещал бы иметь более одного метода
обратного вызова в классе обратного вызова. Например, можно реализовать специальные требования к контрольному журналу:
class Auditor
def initialize(audit_log)
@audit_log = audit_log
end
def after_create(model)
@audit_log.created(model.inspect)
end
def after_update(model)
@audit_log.updated(model.inspect)
end
def after_destroy(model)
@audit_log.destroyed(model.inspect)
end
end

Обратные вызовы

281

Чтобы добавить к классу ActiveRecord механизм записи в контрольный
журнал, нужно сделать вот что:
class Account < ActiveRecord::Base
after_create Auditor.new(DEFAULT_AUDIT_LOG)
after_update Auditor.new(DEFAULT_AUDIT_LOG)
after_destroy Auditor.new(DEFAULT_AUDIT_LOG)
...
end

Но это же коряво – добавлять три объекта Auditor в трех строчках.
Можно было бы завести локальную переменную auditor, но так повторения все равно не избежать. Вот удобный случай воспользоваться механизмом открытости классов в Ruby, который позволяет модифицировать классы, не являющиеся частью вашего приложения.
Не лучше было бы просто поместить предложение acts_as_audited в начало модели, нуждающейся в контрольном журнале? Можно добавить
его даже в класс ActiveRecord::Base, и тогда средства ведения журнала
будут доступны всем моделям.
В своих проектах я помещаю сляпанный «на скорую руку» код, подобный приведенному в листинге 9.1, в файл lib/core_ext/active_record_
base.rb, но вы можете решить по-другому. Можно даже оформить его
в виде подключаемого модуля (детали см. в главе 19 «Расширение Rails
с помощью подключаемых модулей»). Не забудьте только затребовать
его в файле config/environment.rb, а то он никогда не загрузится.
Листинг 9.1. Сляпанный на скорую руку метод acts as audited
class ActiveRecord::Base
def self.acts_as_audited(audit_log=DEFAULT_AUDIT_LOG)
auditor = Auditor.new(audit_log)
after_create auditor
after_update auditor
after_destroy auditor
end
end

Теперь код класса Account уже не выглядит таким загроможденным:
class Account < ActiveRecord::Base
acts_as_audited
...
end

Тестопригодность
После добавления методов обратного вызова в класс модели необходимо проверить, что они корректно работают в сочетании с моделью,
в которую добавлены. Иногда это проблематично, иногда нет. Классы
обратных вызовов, напротив, очень легко тестировать автономно.

282

Глава 9. Дополнительные возможности ActiveRecord

Следующий тестовый метод проверяет правильность функционирования класса обратного вызова Auditor (с помощью библиотеки Mocha,
которую можно загрузить с сайта http://mocha.rubyforge.org/):
def test_auditor_logs_created
(model = mock).expects(:inspect).returns('foo')
(log = mock).expects(:created).with('foo')
Auditor.new(log).after_create(model)
end

В главе 17 «Тестирование» и в главе 18 «RSpec on Rails» рассматриваются методики тестирования с помощью библиотек Test::Unit и RSpec
соответственно.

Наблюдатели
Принцип одной функции (single responsibility principle) – один из столпов объектно-ориентированного программирования. Его смысл в том,
что у каждого класса должна быть единственная функция. В предыдущем разделе вы узнали об обратных вызовах – полезной возможности
моделей ActiveRecord, которая позволяет подключать новое поведение
к разным точкам жизненного цикла объекта модели. Даже если поместить дополнительное поведение в классы обратных вызовов, их наличие
все равно требует вносить изменения в определение класса самой модели. С другой стороны, Rails предоставляет механизм расширения, полностью прозрачный для класса модели, – это наблюдатели (Observer).
Вот как можно реализовать функциональность класса обратного вызова Auditor в виде наблюдателя за объектами Account:
class AccountObserver < ActiveRecord::Observer
def after_create(model)
DEFAULT_AUDIT_LOG.created(model.inspect)
end
def after_update(model)
DEFAULT_AUDIT_LOG.updated(model.inspect)
end
def after_destroy(model)
DEFAULT_AUDIT_LOG.destroyed(model.inspect)
end
end

Соглашения об именовании
При создании подкласса, наследующего классу ActiveRecord::Observer,
часть Observer в имени подкласса отщепляется. В случае класса AccountObserver из предыдущего примера ActiveRecord знает, что наблюдать нужно за классом Account. Однако не всегда такое поведение же-

Наследование с одной таблицей

283

лательно. На самом деле для такого универсального класса, как Auditor, это было бы даже шагом в неверном направлении, поэтому пре­
доставляется возможность переопределить указанное соглашение
с помощью метода-макроса observe. Мы по-прежнему расширяем класс
ActiveRecord::Observer, но свободны в выборе имени подкласса и можем явно сообщить ему, за чем наблюдать:
class Auditor < ActiveRecord::Observer
observe Account, Invoice, Payment
def after_create(model)
DEFAULT_AUDIT_LOG.created(model.inspect)
end
def after_update(model)
DEFAULT_AUDIT_LOG.updated(model.inspect)
end
def after_destroy(model)
DEFAULT_AUDIT_LOG.destroyed(model.inspect)
end
end

Регистрация наблюдателей
Если бы не существовало места, где Rails мог бы найти зарегистрированных наблюдателей, они вообще никогда не загрузились бы, потому
что никаких ссылок на них из кода приложения нет. В главе 1 «Среда
и конфигурирование Rails» мы упоминали, что в сгенерированном для
вашего приложения файле config/environment.rb есть закомментированная строка, в которой можно определить подлежащих загрузке наблюдателей:
# Активировать наблюдателей, которые должны работать постоянно
config.active_record.observers = [:auditor]

Момент оповещения
Структура извещает наблюдателей о событиях перед срабатыванием
добавленных в объект обратных вызовов. В противном случае было бы
невозможно воздействовать на объект в целом, например в наблюдателе before_destroy, до того как выполнились собственные обратные вызовы объекта.

Наследование с одной таблицей
Очень многие приложения начинаются с разработки вариации на тему
класса User. Со временем появляются различные виды пользователей,
и между ними надо как-то проводить различие. Так возникают классы

284

Глава 9. Дополнительные возможности ActiveRecord

Admin и Guest, являющиеся подклассами User. Теперь общее поведение
можно оставить в User, а поведение подтипа перенести в подкласс. При
этом все данные о пользователях можно по-прежнему хранить в таблице
users – нужно лишь завести колонку type, где будет находиться имя класса, объект которого нужно создать для представления данной строки.
Вернемся к упоминавшемуся выше классу Timesheet и продолжим рассмотрение наследования с одной таблицей на его примере. Нам нужно
знать, сколько оплачиваемых часов billable_hours еще не оплачено для
данного пользователя. Подойти к вычислению этой величины можно
разными способами, мы решили добавить метод экземпляра в класс
Timesheet:
class Timesheet < ActiveRecord::Base
...
def billable_hours_outstanding
if submitted?
billable_weeks.map(&:total_hours).sum
else
0
end
end
def self.billable_hours_outstanding_for(user)
user.timesheets.map(&:billable_hours_outstanding).sum
end
end

Я вовсе не хочу сказать, что это хороший код. Он работает, но неэффективен, а предложение if/else не вызывает восторга. Недостатки становятся очевидны, когда появляется требование помечать табель Timesheet
как оплаченный. Нам приходится снова модифицировать метод billable_hours_outstanding:
def billable_hours_outstanding
if submitted? and not paid?
billable_weeks.map(&:total_hours).sum
else
0
end
end

Это изменение – вопиющее нарушение приниципа открытости-закрытости1, который понуждает нас писать код так, чтобы он был открыт для расширения, но закрыт для модификации. Принцип нарушен, потому что нам пришлось изменить метод billable_hours_outstanding, чтобы учесть новое состояние объекта Timesheet. В таком простом
1

Хорошее краткое изложение имеется на странице http://en.wikipedia.org/
wiki/Open/closed_principle.

Наследование с одной таблицей

285

примере это, возможно, не кажется серьезной проблемой, но подумайте, сколько ветвей пришлось бы добавить в класс Timesheet, чтобы реализовать такую функциональность, как paid_hours (оплаченные часы)
и unsubmitted_hours (не представленные к оплате часы).
И каково же решение проблемы с постоянно изменяющимся условным
предложением? Поскольку вы читаете раздел о наследовании с одной
таблицей, то, надо думать, не удивитесь, узнав, что мы рекомендуем
объектно-ориентированное наследование. Для этого разобьем исходный класс Timesheet на четыре:
class Timesheet < ActiveRecord::Base
# код, не имеющий отношения к делу, опущен
def self.billable_hours_outstanding_for(user)
user.timesheets.map(&:billable_hours_outstanding).sum
end
end
class DraftTimesheet < Timesheet
def billable_hours_outstanding
0
end
end
class SubmittedTimesheet < Timesheet
def billable_hours_outstanding
billable_weeks.map(&:total_hours).sum
end
end

Если позже потребуется обсчитывать частично оплаченные табели, то
нужно будет просто добавить новое поведение в виде класса PaidTimesheet.
И никаких условных предложений!
class PaidTimesheet < Timesheet
def billable_hours_outstanding
billable_weeks.map(&:total_hours).sum - paid_hours
end
end

Отображение наследования на базу данных
Задача эффективного отображения наследования объектов на реляционную базу данных не имеет универсального решения. Мы затронем
лишь одну стратегию, которую Rails поддерживает изначально. Называется она наследование с одной таблицей (single-table inheritance),
или (для краткости) STI.
В случае STI вы заводите в базе данных одну таблицу, в которой хранятся все объекты, принадлежащие данной иерархии наследования.

286

Глава 9. Дополнительные возможности ActiveRecord

В ActiveRecord для этой таблицы выбирается имя, основанное на корневом классе иерархии. В нашем примере она будет называться timesheets.
Но ведь именно так она и была названа раньше, разве нет? Да, однако
для поддержки STI нам придется добавить в нее колонку type, где будет
находиться строковое представление типа хранимого объекта. Для модификации базы данных подойдет следующая миграция:
class AddTypeToTimesheet < ActiveRecord::Migration
def self.up
add_column :timesheets, :type, :string
end
def self.down
remove_column :timesheets, :type
end
end

Значение по умолчанию не нужно. Коль скоро в модель ActiveRecord добавлена колонка type, Rails автоматически будет помещать в нее правильное значение. Полюбоваться этим поведением мы можем в консоли:
>> d = DraftTimesheet.create
>> d.type
=> 'DraftTimesheet'

Когда вы пытаетесь найти объект с помощью любого из методов find,
определенных в базовом классе с поддержкой STI, Rails автоматически
создает объекты подходящего подкласса. Особенно это полезно в таких
случаях, как рассматриваемый пример с табелем, когда мы извлекаем
все записи, относящиеся к конкретному пользователю, а затем вызываем методы, которые по-разному ведут себя в зависимости от класса
объекта:
>> Timesheet.find(:first)
=> #

Говорит Себастьян…
Слово type употребляется для именования колонок очень часто,
в том числе для целей, не имеющих никакого отношения к STI.
Именно поэтому вам, скорее всего, доводилось сталкиваться
с ошибкой ActiveRecord::SubclassNotFound. Rails видит колонку
type в классе Car и пытается найти класс SUV, которого не сущест­
вует.
Решение простое: скажите Rails, что для STI нужно использовать другую колонку:
set_inheritance_column "not_sti"

Наследование с одной таблицей

287

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

Замечания об STI
Хотя Rails существенно упрощает наследование с одной таблицей, стоит помнить о нескольких подводных камнях.
Начнем с того, что запрещается заводить в двух подклассах атрибуты с одинаковым именем, но разного типа. Поскольку все подклассы
хранятся в одной таблице, такие атрибуты должны храниться в одной
колонке таблицы. Честно говоря, проблемой это может стать лишь тогда, когда вы неправильно подошли к моделированию данных.
Гораздо важнее другое: в любом подклассе на каждый атрибут должна
отводиться только одна колонка, и любой атрибут, не являющийcя общим для всех подклассов, должен допускать значение nil. В подклассе
PaidTimesheet есть колонка paid_hours, не встречающаяся больше ни в каких подклассах. Подклассы DraftTimesheet и SubmittedTimesheet не используют эту колонку и оставляют ее равной null в базе данных. Для
контроля данных в колонках, не являющихся общими для всех подклассов, необходимо пользоваться валидаторами ActiveRecord, а не сред­
ствами СУБД.
Кроме того, не стоит заводить подклассы со слишком большим количеством уникальных атрибутов. Иначе в таблице базы данных будет
много колонок, содержащих null. Обычно, появление в дереве наследования подклассов с большим числом уникальных атрибутов свидетельствует о том, что вы допустили ошибку при проектировании и должны
переработать проект. Если STI-таблица выходит из-под контроля, то,
быть может, для решения вашей задачи наследование непригодно.
А, может быть, базовый класс слишком абстрактный?
Наконец, при работе с унаследованными базами данных может случиться так, что вместо type для колонки придется выбрать другое имя.
В таком случае задайте имя колонки в своем базовом классе с помощью
метода класса set_inheritance_column. Для класса Timesheet можно по­
ступить следующим образом:
class Timesheet < ActiveRecord::Base
set_inheritance_column 'object_type'
end

288

Глава 9. Дополнительные возможности ActiveRecord

Теперь Rails будет автоматически помещать тип объекта в колонку
object_type.

STI и ассоциации
Во многих приложениях, особенно для управления данными, можно
встретить модели, очень похожие с точки зрения данных, но различающиеся поведением и ассоциациями. Если до перехода на Rails вы работали с другими объектно-ориентированными языками, то, наверное,
привыкли разбивать задачу на иерархические структуры.
Взять, например, приложение Rails, занимающиеся учетом населения
в штатах, графствах, городах и пригородах. Все это – местности, поэтому возникает желание определить STI-класс Place, как показано в листинге 9.2. Для ясности я включил также схему базы данных1:
Листинг 9.2. Схема базы данных Places и класс Place
#
#
#
#
#
#
#
#
#
#
#
#
#
#

== Schema Information
Table name: places
id
region_id
type
name
description
latitude
longitude
population
created_at
updated_at

:integer(11) not null, primary key
:integer(11)
:string(255)
:string(255)
:string(255)
:decimal(20, 1)
:decimal(20, 1)
:integer(11)
:datetime
:datetime

class Place < ActiveRecord::Base
end

Place – это квинтэссенция абстрактного базового класса. Его не следует
инстанцировать, но в Ruby нет механизма, позволяющего гарантированно предотвратить это (ну и не страшно, это же не Java!). А теперь
определим конкретные подклассы Place:
class State < Place
has_many :counties, :foreign_key => 'region_id'
has_many :cities, :through => :counties
end
1

Если вы хотите включать автоматически сгенерированную информацию
о схеме в начало классов моделей, познакомьтесь с написанным Дэйвом Томасом подключаемым модулем annotate_models, который можно скачать
со страницы http://svn.pragprog.com/Public/plugins/annotate_models.

Наследование с одной таблицей

289

class County < Place
belongs_to :state, :foreign_key => 'region _id'
has_many :cities, :foreign_key => 'region _id'
end
class City < Place
belongs_to :county, :foreign_key => 'region _id'
end

У вас может возникнуть искушение добавить ассоциацию cities в класс
State, поскольку известно, что конструкция has_many :through работает
как с belongs_to, так и с has_many. Тогда класс State принял бы такой вид:
class State < Place
has_many :counties, :foreign_key => 'region_id'
has_many :cities, :through => :counties
end

Оно бы и замечательно, если бы только это работало. К сожалению,
в данном случае, поскольку мы опрашиваем только одну таблицу, невозможно различить разные типы объектов в таком запросе:
Mysql::Error: Not unique table/alias: 'places': SELECT places.* FROM
places INNER JOIN places ON places.region_id = places.id WHERE
((places.region_id = 187912) AND ((places.type = 'County'))) AND
((places.`type` = 'City' ))

Как заставить это работать? Лучше всего было бы использовать специфические внешние ключи, а не пытаться перегрузить семантику
region_id во всех подклассах. Для начала изменим определение таблицы places, как показано в листинге 9.3.
Листинг 9.3. Пересмотренная схема базы данных Places

#
#
#
#
#
#
#
#
#
#
#
#
#
#
#

== Schema Information
Table name: places
id
state_id
county_id
type
name
description
latitude
longitude
population
created_at
updated_at

:integer(11) not null, primary key
:integer(11)
:integer(11)
:string(255)
:string(255)
:string(255)
:decimal(20, 1)
:decimal(20, 1)
:integer(11)
:datetime
:datetime

Без параметра :foreign_key в ассоциациях подклассы упростились.
Плюс, можно воспользоваться обычным отношением has_many от State
к City, а не более сложной конструкцией has_many :through.

290

Глава 9. Дополнительные возможности ActiveRecord
class State < Place
has_many :counties
has_many :cities
end
class County < Place
belongs_to :state
has_many :cities
end
class City < Place
belongs_to :county
end

Разумеется, многочисленные колонки, допускащие null, не принесут
вам признания в среде сторонников чистоты реляционных баз данных.
Но это еще цветочки. Чуть ниже в этой главе мы будем более подробно
заниматься полиморфными отношениями has_many, и уж тогда-то вы
точно заслужите ненависть всех пуристов.

Абстрактные базовые классы моделей
В моделях ActiveRecord допустимо не только наследование с одной
таблицей. Можно организовать обобществление кода с помощью наследования, но сохранять объекты в разных таблицах базы данных.
Для этого требуется создать абстрактный базовый класс модели, который будут расширять подклассы, представляющие сохраняемые объекты. По существу, из всех рассматриваемых в настоящей главе приемов это один из самых простых.
Возьмем класс Place из предыдущего раздела (см. листинг 9.3) и превратим его в абстрактный базовый класс, показанный в листинге 9.4.
Это совсем просто – достаточно добавить всего одну строчку:
Листинг 9.4. Абстрактный класс Place
class Place < ActiveRecord::Base
self.abstract = true
end

Я же говорил – просто. Помечая модель ActiveRecord как абстрактную,
вы делаете нечто противоположное созданию STI-класса с колонкой
type. Вы говорите Rails: «Я не хочу предполагать, что существует таблица с именем places».
В нашем примере это означает, что надо будет создать отдельные таблицы для штатов, графств и городов. Возможно, это именно то, что надо. Но помните, что теперь мы уже не сможем запрашивать все подтипы с помощью такого кода:
Place.find(:all)

Полиморфные отношения has_many

291

Абстрактные классы – это область Rails, где почти не существует авторитетных правил – помочь может только опыт и интуиция.
Если вы еще не обратили внимания, отмечу, что в иерархии моделей
ActiveRecord наследуются как методы класса, так и методы экземпляра. А равно константы и другие члены классов, появляющиеся в результате включения модулей. Это означает, что в класс Place можно
поместить код, полезный всем его подклассам.

Полиморфные отношения has_many
Rails позволяет определить класс, связанный отношением belong_to
с классами разных типов. Об этом красноречиво рассказал в своем блоге Майк Байер (Mike Bayer):
«Полиморфная ассоциация», хотя и имеет некоторое сходство с обычным полиморфным объединением в иерархии классов, в действительности таковым не является, поскольку вы имеете дело с конкретной ассоциацией между одним конечным классом и произвольным
числом исходных классов, а исходные классы не имеют между собой
ничего общего. Иными словами, они не связаны отношением наследования и, возможно, хранятся в совершенно разных таблицах. Таким
образом, полиморфная ассоциация относится, скорее, не к объектному
наследованию, а к аспектно-ориентированному программированию
(AOP); некая концепция применяется к набору различных сущностей,
которые больше никак не связаны между собой. Такая концепция называется сквозной задачей (cross-cutting concern); например, все сущно­
сти предметной области должны поддерживать протокол изменений
в одной таблице базы данных. А в нашем примере для ActiveRecord объекты Order и User должны иметь ссылки на объект Address1.

Другими словами, это не полиморфизм в объектно-ориентированном
смысле слова, а некая своеобразная особенность Rails.

Случай модели с комментариями
Возвращаясь к нашему примеру о временных затратах и расходах,
предположим, что с каждым из объектов классов BillableWeek и Timesheet
может быть связано много комментариев (общий класс Comment). Наивный подход к решению этой задачи – сделать класс Comment принадлежащим одновременно BillableWeek и Timesheet и завести в таблице базы
данных колонки billable_week_id и timesheet_id:
class Comment < ActiveRecord::Base
belongs_to :timesheet
belongs_to :expense_report
end
1

http://techspot.zzzeek.org/?p=13.

292

Глава 9. Дополнительные возможности ActiveRecord

Этот подход наивен, потому что с ним было бы трудно работать и нелегко обобщить. Помимо всего прочего, необходимо было бы включить
в приложение код, гарантирующий, что никакой объект Comment не
принадлежит одновременно объектам BillableWeek и Timesheet. Написать код для определения того, к чему присоединен данный комментарий, было бы затруднительно. Хуже того – если понадобится ассоциировать комментарии еще с каким-то классом, придется добавлять
в таблицу comments еще один внешний ключ, допускающий null.
В Rails эта проблема имеет элегантное решение с помощью так называемых полиморфных ассоциаций, которые мы уже затрагивали, когда обсуждали параметр :polymorphic => true ассоциации belongs_to в главе 7
«Ассоциации в ActiveRecord».

Интерфейс
Для использования полиморфной ассоциации нужно определить лишь
одну ассоциацию belongs_to и добавить в таблицу базы данных две взаимосвязанных колонки. И после этого к любому классу в системе можно будет присоединять комментарии (что наделяет его свойством commentable),
не изменяя ни схему базы данных, ни саму модель Comment.
class Comment < ActiveRecord::Base
belongs_to :commentable, :polymorphic => true
end

В нашем приложении нет класса (или модуля) Commentable. Мы назвали
ассоциацию :commentable, потому что это слово точно описывает интерфейс объектов, ассоциируемых подобным способом. Имя :commentable
фигурирует и на другом конце ассоциации:
class Timesheet < ActiveRecord::Base
has_many :comments, :as => :commentable
end
class BillableWeek < ActiveRecord::Base
has_many :comments, :as => :commentable
end

Здесь мы видим ассоциацию has_many с параметром :as. Этот параметр
помечает ассоциацию как полиморфную и указывает, какой интерфейс
используется на другом конце. Раз уж мы заговорили об этом, отметим,
что на другом конце полиморфной ассоциации belongs_to может быть ассоциация has_many или has_one, порядок работы при этом не изменяется.

Колонки базы данных
Ниже приведена миграция, создающая таблицу comments:
class CreateComments < ActiveRecord::Migration
def self.up
create_table :comments do |t|

Полиморфные отношения has_many

293

t.column :text, :text
t.column :commentable_id, :integer
t.column :commentable_type, :string
end
end
end

Как видите, имеется колонка commentable_type, в которой хранится имя
класса ассоциированного объекта. Принцип работы можно наблюдать
в консоли Rails:
>>
>>
>>
>>
=>
>>
=>
>>
=>
>>
=>

c = Comment.create(:text => "I could be commenting anything.")
t = TimeSheet.create
b = BillableWeek.create
c.update_attribute(:commentable, t)
true
"#{c.commentable_type}: #{c.commentable_id}"
"Timesheet: 1"
c.update_attribute(:commentable, b)
true
"#{c.commentable_type}: #{c.commentable_id}"
"BillableWeek: 1"

Видно, что оба объекта Timesheet и BillableWeek имеют один и тот же
идентификатор id (1). Но благодаря атрибуту commentable_type, хранящемуся в виде строки, Rails может понять, с каким объектом связан
комментарий.

Конструкция has_many :through и полиморфизм
При работе с полиморфными ассоциациями имеются некоторые логические ограничения. Например, поскольку Rails не может понять, какие таблицы необходимо соединять при наличии полиморфной ассоциации, следующий гипотетический код работать не будет:
class Comment < ActiveRecord::Base
belongs_to :user
belongs_to :commentable, :polymorphic => true
end
class User < ActiveRecord::Base
has_many :comments
has_many :commentables, :through => :comments
end
>> User.find(:first).comments
ActiveRecord::HasManyThroughAssociationPolymorphicError: Cannot have
a has_many :through association 'User#commentables' on the polymorphic
object 'Comment#commentable'.

Если вам действительно необходимо нечто подобное, то использование
has_many :through с полиморфными ассоциациями все же возможно, толь-

294

Глава 9. Дополнительные возможности ActiveRecord

ко надо точно указать, какой из возможных типов вам нужен. Для этого
служит параметр :source_type. В большинстве случаев придется еще указать параметр :source, так как имя ассоциации не будет соответ­ствовать
имени интерфейса, заданного для полиморфной ассоциации:
class User < ActiveRecord::Base
has_many :comments
has_many :commented_timesheets, :through => :comments,
:source => :commentable, :source_type => 'Timesheet'
has_many :commented_billable_weeks, :through => :comments,
:source => :commentable, :source_type => 'BillableWeek'
end

Это многословно, и вообще при таком подходе элегантность решения
начинает теряться, но все же он работает:
>> User.find(:first).commented_timesheets
=> [# ]

Замечание об ассоциации has_many
Мы уже близимся к завершению рассмотрения ActiveRecord, а, как
вы, возможно, заметили, еще не был затронут вопрос, представляющийся очень важным многим программистам: ограничения внешнего
ключа в базе данных. Объясняется это тем, что путь Rails не подразумевает использования таких ограничений для обеспечения ссылочной
целостности. Данный аспект вызывает, мягко говоря, противоречивые
мнения, и некоторые разработчики вообще сбросили со счетов Rails
(и его авторов) именно по этой причине.
Никто не мешает вам включить в схему ограничения внешнего ключа,
хотя вы поступите мудро, подождав с этим до написания большей части приложения. Разумеется, полиморфные ассоциации представляют
собой исключение. Это, наверное, самое яркое проявление неприятия
ограничений внешних ключей со стороны Rails. Если вы не готовы идти на бой, то, наверное, не стоит привлекать внимание администратора
базы данных к этой теме.

Модули как средство
повторного использования общего поведения
В этом разделе мы обсудим одну из стратегий выделения функциональности, общей для не связанных между собой классов моделей. Вместо
наследования мы поместим общий код в модули.
В разделе «Полиморфные отношения has_many» мы описали, как добавить комментарии, в примере, касающемся временных затрат и расходов. Продолжим работу с этим примером, так как он отлично подходит для разложения на модули.

Модули как средство повторного использования общего поведения

295

Мы реализуем следующее требование: как пользователи, так и утверждающие должны иметь возможность добавлять комментарии к объектам Timesheet и ExpenseReport. Кроме того, поскольку наличие комментариев служит индикатором того, что табель или отчет о расходах потребовал дополнительного рассмотрения и времени на обработку, администратор приложения должен иметь возможность быстро просмотреть
список недавних комментариев. Но такова уж человеческая природа,
что администратор время от времени просто проглядывает комментарии, не читая их, поэтому в требованиях оговорено, что должен быть
предоставлен механизм пометки комментария как прочитанного сначала утверждающим, а потом администратором.
Снова воспользуемся полиморфной ассоциацией has_many :as, которая
положена в основу этой функциональности:
class Timesheet < ActiveRecord::Base
has_many :comments, :as => :commentable
end
class ExpenseReport < ActiveRecord::Base
has_many :comments, :as => :commentable
end
class Comment < ActiveRecord::Base
belongs_to :commentable, :polymorphic => true
end

Затем создадим для администратора контроллер и действие, которое
будет выводить список из 10 последних комментариев, причем каждый элемент будет ссылкой на комментируемый объект.
class RecentCommentsController < ApplicationController
def show
@recent_comments = Comment.find( :all, :limit => 10,
:order => 'created_at DESC' )
end
end

Вот простой шаблон представления для вывода недавних комментариев:

Комментарий о:

296

Глава 9. Дополнительные возможности ActiveRecord

Пока все хорошо. Полиморфная ассоциация позволяет легко свести
в один список комментарии всех типов. Но напомним, что каждый комментарий должен быть помечен «OK» утверждающим и/или администратором. Помеченный комментарий не должен появляться в списке.
Не станем здесь описывать интерфейс для одобрения комментариев.
Достаточно сказать, что в классе Comment есть атрибут reviewed, который
возвращает true, если комментарий помечен «OK».
Чтобы найти все непрочитанные комментарии к некоторому объекту,
мы можем воспользоваться расширением ассоциации, изменив определения классов моделей следующим образом:
class Timesheet < ActiveRecord::Base
has_many :comments, :as => :commentable do
def approved
find(:all, :conditions => {:reviewed => false })
end
end
end
class ExpenseReport < ActiveRecord::Base
has_many :comments, :as => :commentable do
def approved
find(:all, :conditions => {:reviewed => false })
end
end
end

Мне этот код не нравится, и я надеюсь, что теперь вы уже понимаете
почему. Он нарушает принцип DRY! В классах Timesheet и ExpenseReport
имеются идентичные методы поиска непрочитанных комментариев.
По сути дела, они обладают общим интерфейсом – commentable!
В Ruby имеется механизм определения общих интерфейсов – необходимо включить в каждый класс модуль, который содержит код, разделяемый всеми реализациями общего интерфейса.
Определим модуль Commentable и включим его в наши классы моделей:
module Commentable
has_many :comments, :as => :commentable do
def approved
find( :all,
:conditions => ['approved = ?', true ] )
end
end
end
class Timesheet < ActiveRecord::Base
include Commentable
end

Модули как средство повторного использования общего поведения

297

class ExpenseReport < ActiveRecord::Base
include Commentable
end

Не работает! Чтобы исправить ошибку, необходимо понять, как Ruby
интерпретирует код, в котором используются открытые классы.

Несколько слов об области видимости класса
и контекстах
Во многих других интерпретируемых объектно-ориентированных языках программирования есть две фазы выполнения – сначала интерпретатор загружает определения классов и говорит «вот определение,
с которым я должен работать», а потом исполняет загруженный код. Но
при таком подходе очень трудно (хотя и возможно) добавлять в класс
новые методы на этапе выполнения.
Напротив, Ruby позволяет добавлять методы в класс в любой момент.
В Ruby, написав class MyClass, вы не просто сообщаете интерпретатору,
что нужно определить класс, но еще и говорите: «выполни следующий
код в области видимости этого класса».
Пусть имеется такой сценарий на Ruby:
1 class Foo < ActiveRecord::Base
2 has_many :bars
3 end
4 class Foo
5 belongs_to :spam
6 end

Когда интерпретатор видит строку 1, он понимает, что нужно выполнить следующий далее код (до завершающего end) в контексте объекта
класса Foo. Поскольку объекта класса Foo еще не существует, интерпретатор создает этот класс. В строке 2 предложение has_many :bars выполняется в контексте объекта класса Foo. Что бы ни делало сообщение
has_many, это делается сейчас.
Когда в строке 2 еще раз встретится объявление class Foo, мы снова
попросим интерпретатор выполнить следующий далее код в контексте
объекта класса Foo, но на этот раз интерпретатор уже будет знать
о классе Foo и больше не создаст его. Поэтому в строке 5 мы просто говорим интерпретатору, что нужно выполнить предложение belongs_to
:spam в контексте того же самого объекта класса Foo.
Чтобы можно было выполнить предложения has_many и belongs_to, эти
методы должны существовать в том контексте, в котором вызваны.
Поскольку они определены как методы класса ActiveRecord::Base, а выше мы объявили, что класс Foo расширяет ActiveRecord::Base, то этот
код выполняется без ошибок.

298

Глава 9. Дополнительные возможности ActiveRecord

Однако, определив модуль Commentable следующим образом:
module Commentable
has_many :comments, :as => :commentable do
def approved
find( :all,
:conditions => ['approved = ?', true ] )
end
end
end

мы получим ошибку при попытке выполнить в нем предложение has_
many. Дело в том, что метод has_many не определен в контексте объекта
модуля Commentable.
Теперь, разобравшись с тем, как Ruby интерпретирует код, мы понимаем, что предложение has_many на самом деле должно быть выполнено
в контексте включающего класса.

Обратный вызов included
К счастью, в классе Module определен удобный обратный вызов, который позволит нам достичь желаемой цели. Если в объекте Module определен метод included, то он будет вызываться при каждом включении
этого модуля в другой модуль или класс. В качестве аргумента ему передается объект модуля/класса, в который включен данный модуль.
Мы можем определить метод included в объекте модуля Commentable, так
чтобы он выполнял предложение has_many в контексте включающего
класса (Timesheet, ExpenseReport и т. д.):
module Commentable
def self.included(base)
base.class_eval do

Говорит Кортенэ…
Тут надо соблюсти тонкий баланс. Такие магические трюки, как
include Commentable, конечно, позволяют вводить меньше текста,
и модель выглядит проще, но это может также означать, что код
вашей ассоциации делает такие вещи, о которых вы не подозреваете. Можно легко запутаться и потратить долгие часы, пока
трассировка программы не заведет совсем в другой модуль.
Лично я предпочитаю оставлять все ассоциации в модели и расширять их с помощью модуля. Тогда весь перечень ассоциаций
виден в коде модели как на ладони:
has_many :comments, :as => :commentable, :extend =>
Commentable

Модификация классов ActiveRecord во время выполнения

299

has_many :comments, :as => :commentable do
def approved
find(:all, :conditions => ['approved = ?', true ])
end
end
end
end
end

Теперь при включении модуля Commentable в классы наших моделей
предложение has_many будет выполняться так, будто мы написали его
в теле соответствующего класса.

Модификация классов ActiveRecord
во время выполнения
Средства метапрограммирования Ruby в сочетании с обратным вызовом after_find открывают двери для ряда интересных возможностей,
особенно если вы готовы к размыванию границ между кодом и данными. Я говорю о модификации поведения классов модели на лету, в момент, когда они загружаются в приложение.
В листинге 9.5 приведен сильно упрощенный пример такой техники
в предположении, что в модели существует колонка config. При выполнении обратного вызова after_find мы получаем описатель уникального синглетного (singleton) класса1 экземпляра загруженной модели.
Затем с помощью метода class_eval выполняется содержимое атрибута
config, принадлежащего данному экземпляру класса Account. Так как
мы делаем это с помощью синглетного класса, относящегося только
к данному экземпляру, а не с помощью глобального класса Account, то
на остальных экземплярах учетных записей в системе это ровным счетом никак не сказывается.
Листинг 9.5. Метапрограммирование во время выполнения
в обратном вызове after_find
class Account < ActiveRecord::Base
...
private
def after_find
singleton = class true
validates_length_of
:product_code, :is => 10

Замечания
Сколько-нибудь полное описание всего того, что можно достичь за счет
метапрограммирования в Ruby, и способов правильно это делать составило бы целую книгу. Например, вы бы поняли, что выполнение произвольного Ruby-кода, хранящегося в базе, – штука опасная. Поэтому
я еще раз подчеркиваю, что все примеры сильно упрощены. Я лишь
хочу познакомить вас с имеющимися возможностями.
Если вы решите применять такие приемы в реальных приложениях,
нужно принять во внимание безопасность, порядок утверждения
и многие другие аспекты. Быть может, вы захотите выполнять не произвольный Ruby-код, а ограничиться небольшим подмножеством языка, достаточным для решаемой задачи. Вы можете спроектировать
компактный API или даже разработать предметно-ориентированный
язык (DSL), предназначенный специально для выражения бизнесправил и поведений, которые должны загружаться динамически. Все
глубже проваливаясь в кроличью нору, вы, возможно, загоритесь идеей написать специализированные анализаторы своего языка, которые
могли бы исполнять его в различных контекстах: для обнаружения
ошибок, формирования отчетов и т. п. В этой области возможности
безграничны.

Модификация классов ActiveRecord во время выполнения

301

Ruby и предметноориентированные языки
Мой бывший коллега Джей Филдс и я были первыми, кто применил
сочетание метапрограммирования на Ruby и внутренних1 предметноориентированных языках в ходе разработки приложений Rails для
клиентов компании ThoughtWorks. Я все еще иногда выступаю на конференциях с докладами о создании DSL на Ruby и пишу об этом в своем
блоге.
Джей тоже продолжал писать и рассказывать об эволюции техники
разработки Ruby DSL, которую он называет естественными языками
бизнеса (Business Natural Languages, сокращенно BNL2). При разработке BNL вы проектируете предметно-ориентированный язык, который синтаксически может отличаться от Ruby, но достаточно близок
к нему, чтобы программу можно было легко преобразовать в корректный Ruby-код и выполнить на этапе выполнения, как показано в листинге 9.6.
Листинг 9.6. Пример естественного языка бизнеса
employee John Doe
compensate 500 dollars for each deal closed in the past 30 days
compensate 100 dollars for each active deal that closed more than
365 days ago
compensate 5 percent of gross profits if gross profits are greater
than
1,000,000 dollars
compensate 3 percent of gross profits if gross profits are greater
than
2,000,000 dollars
compensate 1 percent of gross profits if gross profits are greater
than
3,000,000 dollars

Возможность использовать такие продвинутые приемы, как DSL, –
еще один могучий инструмент в руках опытного разработчика для
Rails.

1

Уточнение «внутренний» служит, чтобы отличить предметно-ориентированный язык, полностью погруженный в некий язык общего назначения,
например Ruby, от языка, для которого требуется специально написанный
анализатор.

2

Если поискать слово BNL в Google, то вы получите кучу ссылок на сайт
Barenaked Ladies, находящийся в Торонто, поэтому лучше уж сразу отправляйтесь к первоисточнику по адресу http://bnl.jayfields.com.

302

Глава 9. Дополнительные возможности ActiveRecord

Говорит Кортенэ…
Все DSL – отстой! За исключением, конечно, написанных Оби.
Читать и писать на DSL-языке может только его автор. Когда
проект переходит к другому разработчику, часто проще сделать все заново, чем разбираться в разных хитростях и заучивать слова, которые допустимо употреблять в имеющемся DSLязыке.
На самом деле, метапрограммирование в Ruby – тоже отстой.
Люди, которым дали в руки этот новенький инструмент, часто
не знают меры. Я считаю, что метапрограммирование – self.
included, class_eval и им подобные – лишь портят код в большинстве проектов.
Если вы пишете веб-приложение, то программисты, которые
присоединятся к разработке и сопровождению проекта в будущем, скажут спасибо, если вы будете использовать простые, ясные, четко очерченные и хорошо протестированные методы, а не
залезать в существующие классы или прятать ассоциации в модулях.
Ну а если, прочитав все это, вы все-таки решите попробовать
и справитесь с задачей… что ж, ваш код может оказаться мощнее и выразительнее, чем вы можете себе представить.

Заключение
Этой главой мы завершаем рассмотрение ActiveRecord – одного из самых важных и мощных структур, встроенных в Rails. Мы видели, как
обратные вызовы и наблюдатели помогают элегантно структурировать
код в объектно-ориентированном духе. Мы также пополнили свой арсенал моделирования техникой наследования с одной таблицей и уникальными для ActiveRecord полиморфными отношениями.
К этому моменту мы рассмотрели две составных части паттерна MVC:
модель и контроллер. Настало время приступить к третьей и последней
части: видам, или представлениям.

10
ActionView
У самого великого и самого тупого есть две общие черты.
Вместо того чтобы изменять свои представления
в соответствии с фактами, они пытаются подогнать
факты под свои представления… и это может оказаться
очень неудобно, если одним из фактов, нуждающихся
в изменении, являетесь вы сами.
Doctor Who1

Контроллеры – это скелет и мускулатура приложения Rails. Продолжая аналогию, можно сказать, что модели – это ум и сердце приложения, а шаблоны представлений (основанные на библиотеке ActionView,
третьем из основных компонентов Rails) – его кожа, то есть то, что видно внешнему миру.
ActionView – это часть Rails API, предназначенная для сборки визуального компонента приложения, то есть HTML-разметки и связанного
с ней контента, который отображается в броузере, когда кто-нибудь обращается к приложению. На самом деле, в прекрасном новом мире
REST-ресурсов ActionView отвечает за генерацию любой информации,
исходящей из приложения.
ActionView включает полноценную систему шаблонов, основанную на
библиотеке ERb для Ruby. Она получает от контроллера данные и объединяет их с кодом представления, образуя презентационный уровень
для конечного пользователя.
1

Главный герой научно-фантастического сериала компании BBC. –
Примеч. перев.

304

Глава 10. ActionView

В этой главе мы рассмотрим фундаментальные принципы структуры
ActionView, начиная с основ шаблонов и заканчивая эффективным использованием подшаблонов (partial), а также значительным повышением быстродействия за счет кэширования.

Основы ERb
Стандартные файлы шаблонов в Rails пишутся на диалекте Ruby, который называется Embedded Ruby, или ERb. Библиотека ERb входит
в дистрибутив Ruby и не является уникальной особенностью Rails.
ERb-документ обычно содержит статическую HTML-разметку, перемежащуюся кодом на Ruby, который динамически исполняется во время рендеринга шаблона. Коль скоро вы вообще занимаетесь программированием для Rails, то, конечно, знаете, как Ruby-код, вставляемый
в ERb-документ, обрамляется парой ограничителей.
Существует два вида ограничителей, которые служат разным целям
и работают точно так же, как их аналоги в технологиях JSP и ASP, с которыми, вы, возможно, знакомы:
и

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

Практикум по ERb
Поэкспериментировать с ERb можно и вне Rails, так как интерпретатор ERb – стандартная часть Ruby. Поэтому можно попрактиковаться
в написании и обработке ERb-шаблонов с помощью этого интерпретатора. Вызывается он командной утилитой erb.
Например, введите в файл (скажем, demo.erb) такой код:
Перечислим все методы класса string в Ruby.
Для начала нам понадобится строка.

Итак, строка у нас есть: вот она:

А теперь посмотрим на ее методы:

Основы ERb

305

.

Теперь подайте этот файл на вход erb:
$ erb demo.erb

Вот что вы увидите:
Перечислим все методы класса string в Ruby.
Для начала нам понадобится строка.
Итак, строка у нас есть: вот она:
Я – строка!
А теперь посмотрим на ее методы: -- может быть, не все, чтобы не уходить за
пределы экрана
1. %
2. *
3. +
4. <
5.

Методы stylesheet_link_tag и javascript_include_tag – это помощники,
которые автоматически вставляют в документ стандартные теги LINK
и SCRIPT, необходимые для включения CSS и JavaScript-файлов. Кроме
них, интерес в этом шаблоне представляет только обращение к методу
yield :layout, которое мы сейчас и обсудим.

Подстановка содержимого
Встроенное в Ruby ключевое слово yield нашло элегантное применение в организации совместной работы шаблонов макета и действий.

Макеты и шаблоны

309

Обратите внимание, как это слово употребляется в середине шаблона
макета:

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

В центральный элемент DIV подставляется содержимое, порожденное
главным шаблоном. Но как передать Rails содержимое двух боковых
колонок? Легко – воспользуйтесь в коде шаблона методом content_for:

Навигация

...

Справка
Lorem ipsum dolor sit amet, consectetur adipisicing elit,
sed do eiusmod tempor incididunt ut labore et dolore magna
aliqua. Ut enim ad minim veniam, quis nostrud ...

Заголовок страницы
Обычное содержимое шаблона, подставляемое вместо символа
:layout
...

310

Глава 10. ActionView

Помимо боковых колонок и иных видимых блоков, я рекомендую использовать yield для вставки дополнительного содержимого в элемент
страницы HEAD, как показано в листинге 10.2. Это исключительно полезная техника, поскольку Internet Explorer иногда ведет себе непредсказуемо, если теги SCRIPT встречаются вне элемента HEAD.
Листинг 10.2. Подстановка дополнительного содержимого в заголовок

Мое приложение Rails
"all" %>

Переменные шаблона
Мы видели, как работает механизм подстановки блоков содержимого
в макет, но есть ли другие способы передачи данных от контроллера представлению? В ходе подготовки шаблона переменные экземпляра, которым были присвоены значения на этапе выполнения действия контроллера, копируются в одноименные переменные экземпляра в контексте
шаблона.

Переменные экземпляра
Копирование переменных экземпляра – основная форма взаимодействия между контроллером и представлением, и, честно говоря, это поведение – одна из фундаментальные особенностей Rails, поэтому вы
о нем, конечно, знаете:
class HelloWorldController < ActionController::Base
def index
@msg = "Здравствуй, мир!"
end
end
# template file /app/views/hello_world/index.html.erb

А вот что вы, возможно, не знаете, так это то, что из контроллера
в шаблон копируются не только переменные экземпляра. Однако вклю-

Макеты и шаблоны

311

чать в свой код явные зависимости от некоторых из перечисленных ниже объектов не стоит. Особенно остерегайтесь их использования в операциях с данными. Помните, что стандартное применение паттерна
MVC подразумевает, что на уровне контроллера готовятся данные для
рендеринга, а не само представление!
assigns
Хотите увидеть все, что пересекает границу между контроллером
и представлением? Включите в шаблон строку
и посмотрите, что она выведет. Атрибут assigns – часть внутреннего
устройства Rails, поэтому пользоваться им напрямую в промышленном коде не следует.
base_path
Путь в локальной файловой системе к каталогу приложения, начиная с которого хранятся шаблоны:
controller
С помощью этой переменной можно получить доступ к экземпляру
текущего контроллера до того, как он выйдет за пределы области
видимости в конце обработки запроса. Вы можете воспользоваться
тем, что контроллер знает свое собственное имя (атрибут controller_name) и имя только что выполненного действия (атрибут action_
name); это позволит более эффективно структурировать CSS-стили
(см. листинг 10.3).
Листинг 10.3. Классы для тега BODY образованы из имени
контроллера и действия

...

...

В результате тег BODY будет выглядеть примерно так (в зависимости
от выполненного действия):

Надеюсь, вы знаете, что буква C в аббревиатуре CSS расшифровывается, как cascading (каскадные), а означает это, что имена классов каскадно распространяются вниз по дереву элементов, встречающихся в разметке, и могут употребляться при создании правил. Трюк, примененный в листинге 10.3, заключается в том, что
мы автоматически включили имена контроллера и действия в качестве имен классов для элемента BODY, поэтому в дальнейшем их
можно очень гибко использовать для настройки внешнего вида
страницы.

312

Глава 10. ActionView

Вот как этот прием позволяет варьировать фоновую картинку для
элементов класса header в зависимости от пути к контроллеру:
body.timesheets .header {
background: url(../images/timesheet-bg.png) no-repeat left top
}
body.expense_reports .header {
background: url(../images/expense-reports-bg.png) no-repeat left top
}

flash
flash – это переменная представления, которой вы, несомненно, будете регулярно пользоваться. Она уже проскальзывала в примерах
и применяется, когда нужно отправить пользователю сообщение
с уровня контроллера, но только на время следующего запроса.
В Rails часто употребляется конструкция flash[:notice] для отправки информационных сообщений и flash[:error], если сообщение более серьезно. Лично я люблю заключать их в элементы DIV, располагаемые в начале макета и позиционируемые с помощью CSS-стилей,
как показано в листинге 10.4.
Листинг 10.4. Стандартизованное место для информационного
сообщения и сообщения об ошибке в файле application.html.erb

...

'notice', :id => 'notice' if flash[:notice] %>
'notice error', :id => 'error' if flash[:error] %>

Метод-помощник content_tag позволяет выводить все это содержимое условно. Без него мне пришлось бы заключать HTML-разметку
в блок if, что сделало бы описанную схему довольно громоздкой.
headers
В переменной headers хранятся значения HTTP-заголовков, сопровождающих обрабатываемый запрос. В представлении они могут понадобиться разве что для того, чтобы посмотреть на них в целях отладки. Поместите в любое место макета строку ,
и вы увидите в броузере (после обновления страницы, конечно) чтото вроде:
-Status: 200 OK
cookie:

Макеты и шаблоны

313

- - adf69ed8dd86204d1685b6635adae0d9ea8740a0
Cache-Control: no-cache

logger
Хотите записать что-то в протоколы во время рендеринга представления? Воспользуйтесь методом logger, чтобы получить экземпляр
класса Logger. Если вы ничего не меняли, то по умолчанию это будет
RAILS_DEFAULT_LOGGER.
params
Это тот же самый хеш params, который доступен контроллеру; он содержит пары имя/значение, указанные в запросе. Иногда я напрямую использую значения из хеша params в представлении, особенно
когда речь идет о страницах с фильтрацией и сортировкой строк:
Фильтр по месяцу:

С точки зрения безопасности крайне нежелательно помещать неочищенные данные из запроса в выходной поток, формируемый
шаблоном. В следующем разделе «Защита целостности представления от данных, введенных пользователем» мы рассмотрим эту тему
более подробно.
request и response
Представлению доступны объекты request и response, соответствующие запросу и ответу HTTP, но, помимо отладки, я не вижу для них
других применений в шаблоне.
session
В переменной session хранится хеш, представляющий сеанс пользователя. Возможно, бывают ситуации, когда значения из него можно
использовать для изменения рендеринга, но меня в дрожь бросает
от мысли, что вы захотите устанавливать параметры сеанса из уровня представления. Применяйте с осторожностью и, в основном, для
отладки, как request и response.

Защита целостности представления от данных,
введенных пользователем
Если данные, необходимые приложению, вводятся пользователем или
поступают из иного не заслуживающего доверия источника, то не забывайте о необходимости экранировать и обезопасить содержимое
шаблона. В противном случае вы распахнете двери для самых разнообразных хакерских атак.
Рассмотрим, например, следующий фрагмент шаблона, который всего
лишь копирует значение params[:page_number] в выходной поток:

314

Глава 10. ActionView
Результаты поиска
Страница

Простой способ включить номер страницы, не так ли? Но подумайте,
что произойдет, если кто-нибудь отправит этой странице запрос, содержащий вложенный тег SCRIPT и некоторое вредоносное значение в качестве параметра page_number. Бах! Злонамеренный код попал прямо
в ваш шаблон!
К счастью, существует совсем несложный способ предотвратить такие
атаки, и, поскольку разработчики Rails ожидают, что вы будете пользоваться им часто, они присвоили соответствующему методу однобуквенное имя h:
Результаты поиска
Страница

Метод h экранирует HTML-содержимое – вместо того, чтобы включать
его напрямую в разметку, знаки < и > заменяются соответствующими
компонентами, в результате чего попытки внедрения вредоносного кода терпят неудачу. Разумеется, на содержимое, в котором нет разметки, это не оказывает никакого влияния.
Но что, если необходимо отобразить введенную пользователем HTMLразметку, как часто бывает в блогах, где допустимы форматированные
комментарии? В таком случае попробуйте воспользоваться методом sani­
tize из класса ActionView::Helpers::TextHelper. Он уберет теги, которые
наиболее часто применяются для атак: FORM и SCRIPT, а все прочие оставит
без изменения. Метод sanitize подробно рассматривается в главе 11.

Подшаблоны
Подшаблоном (partial) называется фрагмент кода шаблона. В Rails подшаблоны применяются для разбиения кода представления на отдельные
блоки, из которых можно собирать макеты с минимумом дублирования.
Синтаксически подшаблон начинается со строки render :partial => "name".
Перед именем шаблона должен стоять подчерк, что позволяет визуально отличить его от других файлов в каталоге шаблонов. Однако при
ссылке на подшаблон знак подчерка опускается.

Простые примеры
В простейшем случае подшаблон используется для оформления части
кода шаблона. Некоторые разработчики таким образом разбивают
шаблоны на логически независимые части. Иногда понять структуру
страницы легче, если разложить ее на существенные фрагменты. Например, в листинге 10.5 приведена простая страница регистрации,
разбитая на ряд подшаблонов.

Подшаблоны

315

Листинг 10.5. Простая форма регистрации пользователя
с подшаблонами
Регистрация пользователя

users_path do -%>

'details' %>
'demographics' %>

'location' %>

'opt_in' %>

'terms' %>

И давайте сразу посмотрим на один из подшаблонов. Для экономии места возьмем самый маленький – содержащий флажки, которые описывают настройки данного приложения. Его код приведен в листинге 10.6;
обратите внимание, что имя файла начинается с подчерка.
Листинг 10.6. Подшаблон с настройками в файле app/views/users/
_opt_in.html.erb

Spam Opt In

Посылать извещения о новых событиях!

Уведомлять меня о новых службах

Лично я предпочитаю заключать подшаблоны в семантически значимые
контейнеры в разметке. В случае подшаблона из листинга 10.6 оба флажка помещены внутрь элемента , которому присвоен атрибут id.
Следование этому неформальному правилу помогает мне мысленно
представлять, как содержимое данного подшаблона соотносится с родительским шаблоном. Если бы речь шла о другой разметке, например вне
формы, возможно, вместо я выбрал бы контейнер .
Почему не оформить в виде подшаблонов содержимое тегов ? Это
вопрос стиля – я люблю, когда весь скелет разметки находится в одном

316

Глава 10. ActionView

месте. В данном случае скелетом является табличная структура, показанная в листинге 10.5. Если бы части данной таблицы находились
в подшаблонах, это лишь затемнило бы общую верстку страницу. Признаю, что это область личных предпочтений, и могу лишь поделиться,
как удобнее работать лично мне.

Повторное использование подшаблонов
Поскольку форма регистрации аккуратно разбита на компоненты, легко создать простую форму редактирования, в которой некоторые из
этих компонентов используются повторно (листинг 10.7).
Листинг 10.7. Простая форма редактирования данных о пользователе,
повторно использующая некоторые подшаблоны
Редактирование пользователя

user_path(@user),
:html => { :method => :put } do -%>

'details' %>

'demographics' %>

'opt_in' %>

Сравнив листинги 10.5 и 10.7, вы увидите, что в форме редактирования структура таблицы несколько изменилась, и содержимого в ней
меньше, чем в форме регистрации. Возможно, адресная информация
более подробно вводится на отдельной странице, и уж конечно вы не
хотите заставлять пользователя принимать соглашение при каждом
изменении настроек.

Разделяемые подшаблоны
До сих пор мы рассматривали работу с подшаблонами, находящимися
в том же каталоге, что и их родительский шаблон. Однако никто не
мешает ссылаться на подшаблоны, хранящиеся в других каталогах, –

Подшаблоны

317

достаточно просто написать в начале имя каталога. Но знак подчерка
все равно следует опускать, что может показаться немного странным.
Добавим в конец формы регистрации листинга 10.5 подшаблон captcha, мешающий спамерам автоматически регистрироваться:
...

'terms' %>

'shared/captcha' %>

Так как подшаблон captcha используется в разных частях приложения,
имеет смысл поместить его в общую папку, а не в папку конкретного
представления. Однако, преобразуя существующий код шаблона в разделяемый подшаблон, нужно проявлять осторожность. Очень легко
неосознанно создать подшаблон, который будет неявно зависеть от того, откуда выполняется его рендеринг.
Рассмотрим, например, участника списка рассылки Rails-talk с некорректным подшаблоном в файле login/_login.rhtml:

Имя:

Пароль:

Отправка формы работает, если подшаблон выводится как часть дейст­
вия контроллера login (на «странице входа»), но перестает работать,
когда этот же подшаблон включается в шаблон представления любой
другой части сайта. Проблема в том, что метод form_tag (рассматривается в следующей главе) обычно принимает необязательный параметр
action, который говорит, куда отправлять информацию. Если этот параметр опущен, форма отправляется странице с текущим URL, а он зависит от страницы, в которую был включен разделяемый подшаблон.

Передача переменных подшаблонам
Подшаблоны наследуют переменные экземпляра, доступные родительскому шаблону. Именно поэтому работают методы-помощники (text,
password и т. п.), встречающиеся в листингах 10.5 и 10.7, – они неявно

318

Глава 10. ActionView

полагаются на то, что в области видимости есть переменная @user. Мне
кажется, что в некоторых случаях таким неявным разделением переменных вполне можно пользоваться, особенно когда подшаблон тесно
связан со своим родителем. Сомнений вообще не возникало бы, если бы
единственной причиной разбиения на подшаблоны было стремление
уменьшить размер и сложность особенно больших шаблонов.
Однако, когда у вас появляется привычка выделять подшаблоны с целью повторного использования в разных местах приложения, вводить
зависимости от неявно передаваемых переменных становится рискованно. Поэтому Rails поддерживает передачу подшаблону переменных
с локальной областью видимости с помощью параметра-хеша :locals:
render :partial => 'shared/address', :locals => { :form => form }

Имена и значения, переданные в хеше :locals, преобразуются в локальные переменные (без префикса @) подшаблона. В листинге 10.8
приведена вариация на тему шаблона страницы регистрации. На этот
раз мы воспользовались вариантом метода form_for, который передает
блоку параметр form, представляющий саму форму. Этот параметр далее передается подшаблонам.
Листинг 10.8. Простой шаблон страницы регистрации пользователя,
в котором форма передается как локальная переменная
Регистрация пользователя

users_path do |form| -%>

'details',
:locals => {:form => form } %>
'shared/address',
:locals => {:form => form } %>

И наконец, в листинге 10.9 приведена разделяемая форма для ввода
адреса.
Листинг 10.9. Простой разделяемый подшаблон для ввода адреса
с использованием локальной переменной

Адрес
Улица
2, :cols => 40 %>
Город

Подшаблоны

319

Штат
2 %>
Почтовый индекс
15 %>

У методов-помощников для обработки форм, которые мы будем рассматривать в главе 11, есть варианты для вызова с переменной form,
поставляемой методом form_for. Именно ее мы и передали в подшаблоны с помощью хеша :locals.

Хеш local_assigns
При необходимости проверить наличие некоторой локальной переменной искать ее надо в хеше local_assigns, который является частью любого шаблона. Конструкция defined? variable работать не будет в силу
ограничений, присущих системе рендеринга.

Рендеринг наборов
Одно из самых удачных применений подшаблонов – рендеринг наборов. Привыкнув оформлять рендеринг с помощью подшаблонов, вы
уже не захотите вновь загромождать свои шаблоны уродливыми циклами for и each.
render :partial => 'entry', :collection => @entries

Коротко, ясно и опирается на соглашение об именовании. Самое существенное тут – способ, которым подшаблон узнает отображаемый
объект. Объект записывается в локальную переменную с тем же именем, что у самого подшаблона. Подшаблон, соответствующий предыдущему фрагменту, должен был бы сослаться на локальную переменную entry.

ago

Переменная partial_counter
Существует еще одна малоизвестная переменная, устанавливаемая
в подшаблонах для рендеринга наборов. Это счетчик с начальным значением 0, отслеживающий, сколько раз уже был выведен подшаблон.
Он полезен для вывода нумерованных списков. Имя переменной образуется из имени подшаблона и суффикса _counter.

320

Глава 10. ActionView

:

ago

Разделяемые подшаблоны для рендеринга наборов
Если бы вы захотели использовать подшаблон для наборов при выво­де единственного объекта, то этот объект следовало бы передать в хеше
:locals, описанном в предыдущем разделе:
render :partial => 'entry', :locals => {:entry => @entry }

Мне встречался следующий прием, позволяющий избежать передачи
параметра locals:

ago

Это работает, но код некрасивый, содержит повторения и неявно зависит от наличия необязательной переменной @entry. Не делайте так.
Пользуйтесь параметром :locals, который и предназначен для подобных задач.

Протоколирование
Заглянув в журнал разработки, вы увидите в нем записи о том, какие
подшаблоны выводились и сколько на это потребовалось времени:
Rendering template within layouts/application
Rendering listings/index
Rendered listings/_listing (0.00663)
Rendered listings/_listing (0.00296)
Rendered listings/_listing (0.00173)
Rendered listings/_listing (0.00159)
Rendered listings/_listing (0.00154)
Rendered layouts/_login (0.02415)
Rendered layouts/_header (0.03263)
Rendered layouts/_footer (0.00114)

Кэширование
Механизм кэширования в Rails позволяет создавать приложения, которые очень быстро отвечают при развертывании в режиме эксплуатации. Кэшировать можно все: от целых страниц до их фрагментов. Кэшированные части хранятся на диске в виде HTML-файлов и отправляются веб-сервером в ответ на последующие запросы при минимуме

Кэширование

321

участия со стороны Rails. В Rails поддерживается три способа кэширования:
• Кэширование страниц. Вся информация, сгенерированная действием контроллера, записывается на диск, и дальнейшая обработка
происходит без участия диспетчера Rails;
• Кэширование действий. Вся информация, сгенерированная дейст­
вием контроллера, записывается на диск, но диспетчер Rails все же
участвует в обработке последующих запросов, и выполняются фильтры контроллера;
• Кэширование фрагментов. Можно кэшировать на диске произвольные части сгенерированной страницы, чтобы не тратить время на их
рендеринг в будущем.

Кэширование в режиме разработки?
Я хотел с самого начала упомянуть, что в режиме разработки кэширование отключено. Если вы хотите поэкспериментировать, измените
следующую строку в файле config/environments/development.rb:
config.action_controller.perform_caching = false

Но, конечно, не забудьте восстановить ее в исходном виде, перед тем
как сохранять проект в репозитории, если не хотите столкнуться с некоторыми сбивающими с толку ошибками1.

Кэширование страниц
Кэширование страниц – это простейший вид кэширования. Для его
включения служит метод-макрос caches_page в контроллере. Он говорит Rails о том, что нужно записать на диск весь ответ на запрос, чтобы
в дальнейшем его мог отдавать сам веб-сервер без вмешательства со
стороны диспетчера. При этом не будет производиться запись в протокол Rails, не будут срабатывать фильтры контроллера – вообще Rails
никак себя не проявит, как будто речь идет о статическом HTML-файле
в каталоге public проекта.

Кэширование действий
По определению, если нечто может изменяться при каждом запросе
или в зависимости от того, кто просматривает страницу, то кэширование не допускается. С другой стороны, если нам нужно лишь выполнить фильтры, которые проверят некоторые условия перед отображе1

В замечательном ролике на эту тему Джеффри Грозенбэк предлагает ввести
в проект еще один режим с названием development_with_caching, где кэ­
ширование отключено в экспериментальных целях (http://peepcode.com/
products/page-action-and-fragment-caching).

322

Глава 10. ActionView

нием запрошенной страницы, подойдет метод caches_action. Он делает
почти то же самое, что и кэширование страниц, только перед возвратом кэшированного HTML-файла выполняются фильтры контроллера.
Это позволяет осуществить дополнительную обработку или даже переадресовать пользователя на другую страницу.
Кэширование действий реализовано на базе кэширования фрагментов
(рассматривается ниже) и around-фильтра (см. главу 2 «Работа с контроллерами»). Содержимое кэшированного действия индексируется текущим хостом и путем, то есть механизм работает, даже если приложение Rails обслуживает несколько субдоменов с помощью маскирования
DNS. Кроме того, различные представления одного и того же ресурса,
например HTML и XML, трактуются как разные запросы и кэшируются отдельно.
В этом разделе примеры кода будут относится к демонстрационному
приложению lil_journal1. В приложении есть как открытые, так и закрытые для публичного просмотра записи, поэтому в умалчиваемом режиме следует выполнить фильтр, который проверит, зарегистрирован
ли пользователь, и при необходимости переадресует его на действие
public. В листинге 10.10 приведен код контроллера EntriesController.
Листинг 10.10. Контроллер EntriesController в приложении lil_journal
class EntriesController < ApplicationController
before_filter :check_logged_in, :only => [:index]
caches_page :public
caches_action :index
def public
@entries = Entry.find(:all,
:limit => 10,
:conditions => {:private => false})
render :action => 'index'
end
def index
@entries = Entry.find(:all, :limit => 10)
end
private
def check_logged_in
redirect_to :action => 'public' unless logged_in?
end
end
1

Subversion URL: http://obiefernandez.com/svn/projects/awruby/prorails/
lil_journal.

Кэширование

323

Действие public отображает только открытые записи и доступно всем,
поэтому является кандидатом на кэширование страницы. Но, поскольку у него нет собственного шаблона, мы явно вызываем в конце дейст­
вия метод render :action => 'index'.

Замечания о проектировании
Априорная информация о том, что приложению потребуется кэширование, должна учитываться при проектировании. В проектах с необязательной аутентификацией часто имеются такие действия контроллеров, для которых кэширование страниц или действий невозможно,
поскольку они обрабатывают оба состояния «зарегистрированности»
внутри себя. Так произошло бы и в программе из листинга 10.10, если
бы действие index обрабатывало вывод открытых и закрытых записей:
def index
opts = {}
opts[:limit] = 10
opts[:conditions] = {:private => false } unless logged_in?
@posts = Entry.find(:all, opts)
end

Как правило, бывает не слишком много полностью статических страниц, которые можно кэшировать с помощью методов cache_page или
cache_action, поэтому наступает черед кэширования фрагментов.

Кэширование фрагментов
Пользователи уже привыкли к динамичным страницам, и в макете
вашего приложения наверняка будут области для приветствия или
счетчика уведомлений. Механизм кэширования фрагментов позволяет сохранить на диске части выводимой страницы и при последующих
запросах отправлять их пользователям, не выполняя рендеринг заново. Повышение производительности не так ощутимо, как при кэшировании страниц или действий, поскольку диспетчеру Rails все-таки
приходится вмешиваться. Тем не менее скорость работы приложения
можно существенно увеличить.

Метод cache
По своей природе кэширование фрагментов определяется в шаблоне
представления, а не на уровне контроллера. Для этого служит метод
cache класса ActionView. Он принимает блок, позволяющий обернуть
подлежащее кэшированию содержимое.
После того как пользователь регистрируется в приложении Lil’ Journal,
в заголовке должна отображаться информация о нем, поэтому вопрос
о кэшировании действий для индексной страницы даже не стоит. Мы
уберем директиву action_cache из EntriesController, но оставим директиву cache_page для действия public. Затем откроем шаблон entries/

324

Глава 10. ActionView

index.html.erb и добавим кэширование фрагментов, как показано в листинге 10.11.
Листинг 10.11. Шаблон entries/index.html.erb с кэшированием
фрагментов в приложении Lil’ Journal

'entry', :collection => @entries %>

Вот, собственно, и все. Теперь HTML-разметка, сгенерированная в результате рендеринга набора записей, сохранена в кэше фрагментов, ассоциированном со страницей entries/index. Этого достаточно, если вы
кэшируете только один фрагмент на странице, но обычно необходимо
назначить фрагменту дополнительный идентификатор.

Именованные фрагменты
Метод cache принимает необязательный параметр name:

Если не задавать его, как мы и поступили в листинге 10.11, то в качест­
ве ключа доступа к кэшу используется URL объемлющей страницы.
Такой способ подходит, когда на странице кэшируется только один
фрагмент.
Если же кэшируется несколько фрагментов страницы, необходим дополнительный идентификатор, чтобы не возникло конфликтов имен.
В листинге 10.12 приведен улучшенный вариант страницы со списком
записей, где мы добавили боковую колонку с перечнем недавних комментариев.
Листинг 10.12. Страница со списком записей, в которой присутствуют
две директивы кэширования фрагментов

'entries') do %>
'entry', :collection => @entries %>

'recent_comments') do %>
'comment', :collection => @recent_comments
%>

Кэширование

325

После рендеринга этого кода в кэше будут храниться два фрагмента с такими ключами:
• /entries/index?fragment=entries
• /entries/index?fragment=recent_comments
Индексация фрагментов по URL страницы представляет собой изящное решение довольно трудной задачи. Представьте, например, что
произошло бы, если бы мы добавили в приложение Lil’ Journal разбиение на страницы и захотели получить записи на второй странице. Без
каких бы то ни было усилий с нашей стороны дополнительные фрагменты попали бы в кэш с такими ключами:
• /entries/index?page=2&fragment=entries
• /entries/index?page=2&fragment=recent_comments

Примечание
В Rails для конструирования уникальных идентификаторов фрагментов применяется вспомогательный метод url_for. Не требуется, чтобы ключи фрагментов соответствовали
реальным URL, встречающимся в приложении.

Глобальные фрагменты
Иногда требуется кэшировать фрагменты, связанные не только с URL
единственной страницы приложения. Чтобы добавить в кэш фрагменты с глобальными ключами, мы снова воспользуемся параметром name
метода-помощника cache, но на этот раз передадим в качестве его значения строку, а не хеш.
Для демонстрации данной методики потребуем, чтобы приложение
Lil’ Journal отображало на каждой странице статистические сведения
о пользователе. В листинге 10.13 подшаблон stats кэшируется для
каждого пользователя, причем в качестве ключа выступает имя пользователя с суффиксом "_stats".
Листинг 10.13. Страница со списком записей, на которой отображается
глобальная статистика пользователя

'entries') do %>
'entry', :collection => @entries %>

'stats' %>

326

Глава 10. ActionView
'recent_comments') do %>
'comment', :collection => @recent_comments %>

При обсуждении кэширования мы упустили из виду один вопрос – истечение срока хранения устаревшего кэшированного содержимого,
которое уже не соответствует реальным данным.

Устранение ненужных обращений к базе данных
Поместив фрагменты представления в кэш, бессмысленно обращаться
к базе за получением данных для этих фрагментов. Ведь результаты
запросов все равно не будут использоваться, пока не истечет срока хранения фрагментов в кэше. Метод read_fragment позволяет проверить,
существует ли кэшированное содержимое; он принимает те же параметры, что и ассоциированный с ним метод cache.
Вот как следует модифицировать действие index:
def index
unless read_fragment(:fragment => 'entries')
@entries = Entry.find(:all, :limit => 10)
end
end

Теперь метод поиска будет выполняться, только если кэш необходимо
обновить.

Истечение срока хранения кэшированного содержимого
При применении кэширования необходимо рассмотреть все ситуации,
которые могут привести к устареванию кэша. А затем написать код,
который удалит старое содержимое, освободив, так сказать, место для
более свежего.

Очистка кэша страниц и действий
Методы expire_page и expire_action позволяют явно удалить содержимое из кэша так, чтобы при следующем запросе оно сгенерировалось
заново. Для идентификации удаляемого содержимого используется
тот же метод url_for, что и в других местах Rails. В листинге 10.14 показано, как включить очистку в метод create контроллера entries.
Листинг 10.14. Действие create контроллера entries
1 def create
2 @entry = @user.entries.build(params[:entry])
3 if @entry.save
4
expire_page :action => 'public'
5
redirect_to entries_path(@entry)

Кэширование

327

6 else
7
render :action => 'new'
8 end
9 end

Обратите внимание на строку 4, где из кэша явно удаляется страница,
ассоциированная с действием public. Но, если подумать, обнаружится,
что не только действие create делает кэш недействительным. То же самое относится к действиям update и destroy.
Разрабатывая свои приложения, особенно в стиле REST, помните, что
различные представления одного и того же ресурса считаются разными запросами и кэшируются отдельно. Если вы кэшировали XMLпредставление действия, то для стирания его из кэша необходимо добавить параметр :format => :xml в спецификацию действия.

Очистка кэша фрагментов
Ой! Я почти забыл (честное слово), что надо очищать еще и кэшированные фрагменты, для чего предназначен метод expire_fragment. С учетом
этого действие create будет выглядеть так:
def create
@entry = @user.entries.build(params[:entry])
if @entry.save
expire_page :action => 'public'
expire_fragment(:fragment => 'entries')
expire_fragment(:fragment => (@user.name + "_stats"))
redirect_to entries_path(@entry)
else
render :action => 'new'
end
end

Использование регулярных выражений
в методах очистки кэша
В процедуре очистки, которую мы добавили в действие create, все еще (!)
осталась серьезная проблема. Если помните, мы говорили, что механизм кэширования фрагментов будет работать и в случае разбиения
списка записей на страницы, причем ключи фрагментов при этом выглядят так:
'/entries/index?page=2&fragment=entries'

Но если ограничиться лишь вызовом expire_fragment(:fragment =>
'entries'), из кэша будут удалены только фрагменты для первой страницы. Поэтому метод expire_fragment понимает также регулярные выражения, и мы должны этим воспользоваться:
expire_fragment(r%{entries/.*})

328

Глава 10. ActionView

Однако должен же быть более удобный способ сделать кэш недействительным. Очень не хочется помнить о необходимости вставлять кучу
сложных предложений удаления из кэша во все действия. Кроме того,
кэширование – это отдельная задача, и, похоже, подходить к ней нужно с позиций аспектно-ориентированного программирования.

Автоматическая очистка кэша с помощью дворников
Класс Sweeper (дворник) во многом напоминает объект Observer из библиотеки ActiveRecord, но предназначен специально для очистки кэшированного содержимого. Вы говорите дворнику, за изменением каких
моделей он должен наблюдать, как делаете это для классов обратных
вызовов и наблюдателей.
В листинге 10.15 показан дворник, который следит за надлежащим кэшированием страниц со списком записей в приложении Lil’ Journal.
Листинг 10.15. Дворник для страниц со списком записей в приложении
Lil’ Journal
class EntrySweeper < ActionController::Caching::Sweeper
observe Entry
def expire_cached_content(entry)
expire_page :controller => 'entries', :action => 'public'
expire_fragment(r%{entries/.*})
expire_fragment(:fragment => (entry.user.name + "_stats"))
end
alias_method :after_save, :expire_cached_content
alias_method :after_destroy, :expire_cached_content
end

Написав класс-дворник (который должен находиться в каталоге app/
models), нужно приказать контроллеру использовать этот класс в сочетании с действиями. Вот как выглядит начало переработанного контроллера entries для приложения Lil’ Journal:
class EntriesController < ApplicationController
before_filter :check_logged_in, :only => [:index]
caches_page :public
cache_sweeper :entry_sweeper, :only => [:create, :update, :destroy]
...

Как и многие другие макросы контроллеров, метод cache_sweeper принимает необязательные параметры :only и :except. Не имеет смысла беспокоить дворника действиями, которые не могут изменить состояние
приложения, поэтому в нашем примере мы добавим параметр :only.
Как и наблюдатели, дворники могут наблюдать не только за одной моделью. Но, если вы соберетесь пойти по этому пути, помните, что методы

329

Кэширование

обратных вызовов должны знать, как работать с каждой моделью. В этом
случае может пригодиться предложение case, как показано в листин­ге 10.16 – полностью переработанной версии класса EntrySweeper, который
теперь способен наблюдать не только за объектами Entry, но и Comment.
Листинг 10.16. Класс EntrySweeper, переработанный для наблюдения за
объектами Entries и Comments
class EntrySweeper < ActionController::Caching::Sweeper
observe Entry, Comment
def expire_cached_content(record)
expire_page :controller => 'entries', :action => 'public'
expire_fragment(r%{entries/.*})
user = case entry
when Entry then record.user
when Comment then record.entry.user
end
expire_fragment(:fragment => (user.name + "_stats"))
end
alias_method :after_save, :expire_cached_content
alias_method :after_destroy, :expire_cached_content
end

Протоколирование работы кэша
Если вы включите кэширование в режиме разработки, то увидите
в протоколе записи о кэшировании и очистке кэша:
Processing Entries#index (for 127.0.0.1 at 2007-07-20 23:07:09) [GET]
...
Cached page: /entries.html (0.03949)
Processing Entries#create (for 127.0.0.1 at 2007-07-20 23:10:50)
[POST]
...
Expired page: /entries.html (0.00085)

Это неплохой способ убедиться, что кэширование работает так, как вы
хотели.

Подключаемый модуль Action Cache
Том Фейкс (Tom Fakes) и Скотт Лэрд (Scott Laird) написали подключаемый модуль Action Cache, который рекомендуется использовать вместо встроенных в Rails средств кэширования. Он не изменяет API кэширования, но подставляет другую реализацию.
script/plugin install http://craz8.com/svn/trunk/plugins/action_cache

330

Глава 10. ActionView

Этот модуль обладает следующими особенностями:
• записи в кэше хранятся в виде YAML-потоков (а не в виде HTMLразметки), поэтому в ответе вместе с кэшированным содержимым
можно также возвращать заголовки;
• добавляет в ответ заголовок last-modified, поэтому клиенты могут
посылать GET-запрос If-modified. Если у клиента уже есть копия кэшированного содержимого, он получит ответ 304 Not Modified;
• гарантирует, что кэшируются только ответы с кодом 200 OK. В противном случае в кэше могли бы застрять страницы с сообщениями
об ошибках без полезного содержимого (что приводит к труднодиагностируемым проблемам);
• позволяет разработчику подменить используемую в Rails реализацию механизма, генерирующего ключи кэша;
• разрешает задавать в действии необязательный срок хранения записи в кэше, по истечении которого она будет автоматически удалена;
• позволяет управлять кэшированием действия во время выполнения
в зависимости от параметров запроса (например, никогда не кэшировать содержимое для администраторов сайта).
• новый метод expire_all_actions очищает весь кэш действий;
• реализация метода expire_action изменена, так что из кэша удаляются все элементы, отвечающие регулярному выражению. Если вы
следуете стилю REST и для одного и того же действия можете возвращать представления в форматах HTML, JS и XML, то при очистке любого из них методом expire_action :controller => ‘foo’, :action =>
‘bar’ удаляются и все остальные.

Хранилища для кэша
В отличие от сеансовых данных, кэш фрагментов может занимать
очень много места. Rails предоставляет четыре варианта организации
хранилища для кэша:
• FileStore – фрагменты хранятся на диске в каталоге, определяемом
параметром cache_path. Этот способ хорошо работает в любом режиме, и фрагменты доступны всем процессам веб-сервера, запущенным из одного и того же каталога приложения;
• MemoryStore – фрагменты хранятся в памяти и могут занимать недопустимо большой объем в памяти процесса;
• DRbStore – фрагменты хранятся в отдельном разделяемом процессе
DRb. Только в этом случае имеется один кэш для всех процессов, но
в ходе развертывания приходится иметь дело с дополнительным
DRb-процессом;
• MemCacheStore – работает аналогично DRbStore, но использует проверенный сервер кэширования memcached. Я провел неформальный опрос

Кэширование

331

нескольких программистов, профессионально работающих с Rails,
и все они согласились, что memcache – наилучший вариант1.

Пример конфигурации
По умолчанию подразумевается режим :memory_store.
ActionController::Base.fragment_cache_store = :memory_store
ActionController::Base.fragment_cache_store = :file_store,
"/path/to/cache/directory"
ActionController::Base.fragment_cache_store = :drb_store,
"druby://localhost:9192"
ActionController::Base.fragment_cache_store = :mem_cache_store,
"localhost"

Ограничения на файловое хранилище
Если ваше приложение Rails размещено на одном сервере, настроить
кэширование довольно просто (разумеется, кодирование – совсем другое дело).
Но когда приложение работает на кластере физических серверов, очистка кэша может стать трудновыполнимой задачей. Если не разместить
файловое хранилище на разделяемой файловой системе, например
NFS или GFS, ничего работать не будет.

Ручная очистка с помощью rake-задания
Если вы остановитесь на файловом хранилище, то, вероятно, захотите
иметь способ ручной очистки всего кэша приложения. Это несложно
сделать с помощью системы Rake. Просто добавьте файл cache.rake
в папку lib/tasks. В нем должно содержаться примерно такое задание:
Листинг 10.17. Rake-задание cache_sweeper
desc "Ручная очистка кэша"
task :cache_sweeper do
FileUtils.rm_rf Dir[File.join(RAILS_ROOT, "public", "entries*")]
#pages
FileUtils.rm_rf Dir[File.join(RAILS_ROOT, "tmp", "cache*")]
#fragments
end

В этом примере я указал каталог entries, а вы не забудьте включить
одно или несколько предложений FileUtils.rm_rf, соответствующих
кэшируемым страницам вашего приложения.
1

Если вы остановитесь на режиме memcache, обязательно познакомьтесь
с подключаемым модулем CacheFu (автор Err the Blog), который можно скачать со страницы http://require.errtheblog.com/plugins/browser/cache_fu.

332

Глава 10. ActionView

И последнее – часто подобный грубый подход с применением FileUtils.
rm_rf предпочитают методам expire_*, потому что иногда проще стереть
весь кэш и заново заполнять его по мере необходимости.

Заключение
В этой главе мы рассмотрели структуру ActionView, подробно остановившись на системе ERb-шаблонов и механизме работы рендеринга
в Rails. Мы также уделили много внимания подшаблонам, поскольку
они принципиально важны для эффективного программирования
в Rails. От сравнительно простых принципов работы шаблонов мы перешли к более сложной теме – кэшированию. Зная, как реализовать
кэширование, вы сможете сэкономить день работы над приложением,
от которого требуется высокая производительность. Разработчики нагруженных сайтов склонны считать Rails весьма своеобразным генератором HTML, который помогает создавать кэшированное содержимое.
А теперь пришло время поговорить о механизме, с помощью которого
можно наделить уровень представления различными хитроумными возможностями, не загромождая шаблоны. Я имею в виду помощников.

11
Все о помощниках
Благодарю за помощь помощнику
в оказании помощи беспомощному.
Ваша помощь очень… помогла!
Миссис Дуонг в фильме Weekenders

Мы уже встречались с несколькими методами-помощниками, которые
Rails предоставляет для организации пользовательского интерфейса
веб-приложения. В этой главе описаны все модули-помощники и содержащиеся в них методы, а также даны инструкции по созданию
собственных помощников.
Библиотеки PrototypeHelper и ScriptaculousHelper вынесены из ядра
Rails 2.0 и теперь распространяются как подключаемые модули. Они
позволяют легко добавить в приложение Rails функциональность Ajax
и подробно рассматриваются в главе 12 «Ajax on Rails».

Примечание
Эта глава представляет собой справочник. Хотя я приложил максимум усилий к тому,
чтобы ее можно было читать подряд, обратите внимание, что модули-помощники ActionView расположены в алфавитном порядке, начиная с ActiveRecordHelper и заканчивая
UrlHelper. В разделе о каждом модуле методы собраны в логические группы там, где это
оправдано.

Модуль ActiveRecordHelper
Модуль ActiveRecordHelper содержит методы-помощники для быстрого
создания форм из моделей ActiveRecord. Метод form может создать всю
форму, сгенерировав поля для простых типов данных, содержащихся

334

Глава 11. Все о помощниках

в записи. Однако он не знает, как собирать компоненты пользовательского интерфейса для манипулирования ассоциациями. Как правило,
разработчики составляют формы с нуля, пользуясь методами из модуля FormHelper, а не из этого модуля.

Отчет об ошибках контроля
Методы error_message_on и error_messages_for помогают добавить в шаблон единообразно отформатированную информацию об ошибках, обнаруженных валидаторами.

error_message_on(object, method, prepend_text = “”,
append_text = “”, css_class = “formError”)
Возвращает тег DIV, внутри которого находится сообщение об ошибке,
присоединенное к указанному методу method объекта object, если таковое существует. Содержимое может быть специализировано с помощью
параметров, содержащих текст, который предшествует сообщению,
и следующий за ним, а также имя CSS-класса.
Этот метод обычно применяется, когда в пользовательском интерфейсе
необходимы отдельные сообщения для некоторых полей формы (как
в следующем примере, взятом из жизни):

*
Имя

34, :tabindex => 1
%>

error_messages_for(*params)
Возвращает тег DIV, содержащий все сообщения об ошибках для всех
объектов, которые хранятся в переменных экземпляра, переданных
в качестве параметров. Этот метод применяется для обстраивания
(scaffolding) в Rails, но редко встречается в реальных приложениях.
В документации по Rails API рекомендуется использовать реализацию
данного метода как образец для решения собственных задач:
Это готовый фрагмент для представления ошибок, в который встроены некоторые строки и HTML-разметка. Если вам нужно нечто,
сильно отличающееся от готового представления, имеет смысл самостоятельно работать с объектом object.errors. Загляните в исходный код и убедитесь, насколько это просто.

Модуль ActiveRecordHelper

335

Мы последуем совету и воспроизведем здесь исходный код данного метода, но предупреждаем, что использовать его в качестве образца следует, только если вы хорошо владеете языком Ruby! Если же у вас есть
время изучить реализацию, то вы, безусловно, узнаете много нового
о реализации среды Rails, в котором весьма своеобразно преломляются
возможности Ruby.
def error_messages_for(*params)
options = params.last.is_a?(Hash) ? params.pop.symbolize_keys : {}
objects = params.collect { |object_name|
instance_variable_get("@#{object_name}")
}.compact
count = objects.inject(0) {|sum, object| sum + object.errors.count }
unless count.zero?
html = {}
[:id, :class].each do |key|
if options.include?(key)
value = options[key]
html[key] = value unless value.blank?
else
html[key] = 'errorExplanation'
end
end
header_message = "#{pluralize(count, 'error')} prohibited this
#{(options[:object_name] || params.first).to_s.gsub('_', ' ')}
from being saved"
error_messages = objects.map {|object|
object.errors.full_messages.map {|msg| content_tag(:li, msg)}
}
content_tag(:div,
content_tag(options[:header_tag] || :h2, header_message)

Заголовок

Тело

И снова на штурм горы!

Внутри этот метод вызывает record.new_record?, чтобы понять, какое
действие необходимо для формы: create или update. С помощью параметра :action можно задать действие и явно.
Если вам нужно, чтобы атрибут enctype формы был равен multipart (необходимо для загрузки файлов), задайте параметр options[:multipart]
равным true.
Можно также передать параметр :input_block, воспользовавшись идиомой Ruby Proc.new для создания анонимного блока кода. Заданный блок
будет вызван для каждой контентной колонки модели, а возвращенное им значение подставлено в форму.
> form("entry", :action => "sign",
:input_block => Proc.new { |record, column|
"#{column.human_name}: #{input(record, column.name)}" })
=>
Сообщение:

Приведенный в этом примере блок построителя (builder block), как он
называется в документации по Rails API, пользуется методом-помощ-

Модуль ActiveRecordHelper

337

ником input, который также является частью модуля и подробно рассматривается в следующем разделе.
Наконец, можно включить в форму дополнительное содержимое, передав при вызове метода form блок, как показано ниже:
form("entry", :action => "sign") do |s|
s true })
=>
>> tag(„img", { :src => „open.png" })
=>

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

auto_link(text, link = :all, href_options = {}, &block)
Преобразует все URL и электронные почтовые адреса внутри строки
text в гиперссылки. Параметр link позволяет уточнить, что именно следует преобразовывать; он может принимать значения :email_addresses
или :urls. В порождаемые теги a можно добавить атрибуты, задав их
в хеше href_options.
Если по какой-то причине вас не устраивает способ, которым Rails превращает почтовые адреса и URL в ссылки, можете указать в этом методе
блок. Блоку передается каждый обнаруженный адрес, а возвращенное
блоком значение подставляется в генерируемый текст в виде ссылки:
>> auto_link("Go to http://obiefernandez.com and say hello to
obie@obiefernandez.com")
=> "Зайдите на http://www.rubyonrails.org
поздоровайтесь с obie@obiefernandez.com"

и

>> auto_link("Заходите на мой блог по адресу http://www.myblog.com/. Пишите
мне на адрес me@email.com.", :all, :target => '_blank') do |text|
truncate(text, 15)
end
=> "Заходите на мой блог по адресу http://www.m....
Пишите мне на адрес me@email.com."

concat(string, binding)
Предпочтительный способ вывода текста в представлении – применение
ERB-конструкции . Обычные методы puts и print рабо-

379

Модуль TextHelper

тают в коде, написанном на eRuby, не так, как ожидается (если, конечно, вы ожидаете, что печатаемый с их помощью текст появится в броузере). Если вам необходимо выводить код в обход блока ,
можете воспользоваться методом concat. В моей практике он оказывался
особенно полезен при написании собственных помощников.

cycle(first_value, *values)
Создает объект Cycle, в котором метод to_s при каждом обращении возвращает следующий элемент массива values (переходя от последнего
снова к первому). Это можно использовать, например, для попеременного назначения CSS-класса строкам таблицы.
В следующем примере CSS-класс меняется для четных и нечетных
строк (предполагается, что в переменной @items находится массив чисел от 1 до 4):

item

Как видно из этого примера, необязательно сохранять ссылку на объект cycle в локальной переменной или еще где-то – достаточно просто
повторно вызывать метод cycle. Это удобно, но означает, что для вложенных циклов необходимы идентификаторы. Решение – передать
методу cycle последним параметром имя :name => cycle_name. Кроме того, можно вручную перевести цикл в исходное состояние методом reset_cycle, которому передается имя цикла.
Ниже приведен набор данных, который надо обойти:
# Меняем CSS-классы
строке
@items = [{:first =>
{:first =>
{:first =>

соседних строк и цвет текста для значений в каждой
'Robert', :middle => 'Daniel', :last => 'James'},
'Emily', :last => 'Hicks'},
'June', :middle => 'Dae', :last => 'Jones'}]

А вот код шаблона. Поскольку количество выводимых ячеек может
быть различно, перед началом обхода мы устанавливаем цикл в исходное состояние:

">

380

Глава 11. Все о помощниках

excerpt(text, phrase, radius = 100, excerpt_string = “...”)
Извлекает из текста фрагмент, соответствующий первому вхождению
фразы phrase. Параметр radius задает количество дополнительных символов по обе стороны выдержки (по умолчанию равен 100). Если при
этом достигается начало или конец текста, с соответствующей стороны
добавляется строка excerpt_string. Если искомая фраза не найдена, метод возвращает nil:
>> excerpt('This is an example', 'an', 5)
=> "...s is an examp..."
>> excerpt('This is an example', 'is', 5)
=> "This is an..."
>> excerpt('This is an example', 'is')
=> "This is an example"
>> excerpt('This next thing is an example', 'ex', 2)
=> "...next t..."
>> excerpt('This is also an example', 'an', 8, ' ')
=> " is also an example"

highlight(text, phrases, highlighter = ‘\1’)
Выделяет во всем тексте одну или несколько фраз, вставляя заданную
строку-выделитель highlighter. Выделитель может быть задан в виде
заключенной в одиночные кавычки строки, где встречается последовательность \1. Вместо нее будет поставлена выделяемая фраза:
>> highlight('You searched for: rails', 'rails')
=> You searched for: rails
>> highlight('You searched for: ruby, rails, dhh', 'actionpack')
=> You searched for: ruby, rails, dhh
>> highlight('You searched for: rails', ['for', 'rails'],
'\1')
=> You searched for: rails
>> highlight('You searched for: rails', 'rails', "\1")
=> You searched for: > current_page?(:action => 'process')
=> false
>>current_page?(:action => 'checkout') # контроллер неявно подразумевается
=> true
>> current_page?(:controller => 'shop', :action => 'checkout')
=> true

link_to(name, options = {}, html_options = nil)
Один из самых важных методов-помощников. Создает тег ссылки с заданным текстом name, который ведет на URL, порождаемый с помощью
параметров в хеше options. Допустимые параметры описаны в разделе,
посвященном методу url_for. Вместо хеша options можно передать
строку, которая станет значением атрибута href. Если name = nil, текстом ссылки становится сам URL:
• :confirm => 'question?' добавляет JavaScript-сценарий с предложением ответить на указанный вопрос. Если пользователь отвечает утвердительно, ссылка обрабатывается нормально, в противном случае не предпринимается никаких действий;
• :popup => true открывает ссылку во всплывающем окне. Можно также задать строку параметров, передаваемых методу JavaScript window.open;
• :method => symbol задает альтернативный глагол HTTP для данного
запроса (отличный от GET). Этот модификатор приводит к динами-

386

Глава 11. Все о помощниках

ческому созданию HTML-формы и отправке ее серверу указанным
методом (:post, :put, :delete или специализированным, заданным
в виде строки, например "HEAD").
Вообще говоря, GET-запросы должны быть идемпотентными, то есть
не модифицировать состояние ресурса на сервере. Поэтому их можно
вызывать многократно без негативных последствий. Запросы, которые
модифицируют ресурсы сервера или приводят к выполнению таких
опасных действий, как удаление записи, не должны ассоциироваться
с нормальной гиперссылкой, поскольку поисковые роботы и так называемые акселераторы броузеров могут переходить по таким ссылкам
во время посещения вашего сайта, оставляя за собой хаос.
Если пользователь отключил JavaScript, запрос будет выполнен методом GET вне зависимости от того, что указано в параметре :method. Достигается это путем включения корректного атрибута href. Если приложению необходимо, чтобы запрос был отправлен каким-то конкретным методом, контроллер должен проверить этот факт, пользуясь методами post?, delete? или put? объекта request.
Как обычно, в хеше html_options можно передать HTML-атрибуты тега a:
>> link_to "Перейти на другой сайт", "http://www.rubyonrails.org/",
:confirm => "Вы уверены?"
=> "Перейти на другой сайт"
>> link_to "Справка", { :action => "help" }, :popup => true
=> "Справка"
>> link_to "Показать картинку", { :action => "view" }, :popup =>
['new_window_name', 'height=300,width=600']
=> "Показать
картинку"
>> link_to "Удалить картинку", { :action => "delete", :id => @image.id },
:confirm => "Вы уверены?", :method => :delete
=> Удалить картинку

Модуль UrlHelper

387

link_to_if(condition, name, options = {},
html_options = {}, &block)
Создает тег ссылки с теми же параметрами, что метод link_to, если усло­
вие condition равно true. В противном случае выводит только значение
name (или значение, вычисленное блоком block, если он задан).

link_to_unless(condition, name, options = {},
html_options = {}, &block)
Создает тег ссылки с теми же параметрами, что метод link_to, если усло­
вие condition не равно true. В противном случае выводит только значение name (или значение, вычисленное блоком block, если он задан).

link_to_unless_current(name, options = {},
html_options = {}, &block)
Создает тег ссылки с теми же параметрами, что метод link_to, если URI
текущего запроса совпадает с URI этой ссылки. В противном случае
выводит только значение name (или значение, вычисленное блоком
block, если он задан).
Этот метод иногда оказывается весьма кстати. Подчеркнем, что переданный ему блок вычисляется, если текущее действие совпадает с указанным. Поэтому, если бы на странице комментариев мы захотели вывести ссылку «Назад» вместо ссылки на ту же самую страницу комментариев, то могли бы поступить следующим образом:
'comments',
:action => 'new}) do
link_to("Назад", { :controller => 'posts', :action => 'index'
})
end %>

mail_to(email_address, name = nil, html_options = {})
Создает тег ссылки mailto, ведущий на указанный почтовый адрес
email_address, который одновременно является текстом ссылки, если
не указан параметр name. В хеше html_options можно передать дополнительные параметры тега.
Помощник mail_to поддерживает несколько способов противодействия
сборщикам постовых адресов и модификации самого почтового адреса.
Все они управляются параметрами в хеше html_options:
• :encode. Этот ключ может принимать в качестве значений строки
"javascript" или "hex". В случае строки "javascript" динамически создается и кодируется ссылка mailto:, а затем вызывает метод eval
для вставки ее в DOM страницы. Если пользователь отключил Java­
Script, то созданная таким способом ссылка не показывается вовсе.

388

Глава 11. Все о помощниках

В случае строки "hex" адрес email_address перед выводом в ссылку
mailto: представляется в шестнадцатеричном виде;
• :replace_at. Если параметр name не задан, в качестве текста ссылки
фигурирует email_address. Эта опция позволяет замаскировать email_
address путем подстановки указанной строки вместо знака @;
• :replace_dot. Если параметр name не задан, в качестве текста ссылки
фигурирует email_address. Эта опция позволяет замаскировать email_
address путем подстановки указанной строки вместо точки в почтовом адресе;
• :subject. Тема почтового сообщения;
• :body. Тело почтового сообщения;
• :cc. Получатели копии почтового сообщения;
• :bcc. Получатели слепой копии почтового сообщения.
Ниже приведены примеры использования метода:
>> mail_to "me@domain.com"
=> me@domain.com
>> mail_to "me@domain.com", "My email", :encode => "javascript"
=> eval(unescape('%64%6f%63...%6d%65'))

>> mail_to "me@domain.com", "My email", :encode => "hex"
=> My email
>> mail_to "me@domain.com", nil, :replace_at => "_at_", :replace_dot =>
"_dot_", :class => "email"
=> me_at_domain_dot_com
>> mail_to "me@domain.com", "My email", :cc => "ccaddress@domain.com",
:subject => "This is an example email"
=> My email

url_for(options = {})
Метод url_for возвращает URL, построенный по указанным в хеше
options параметрам. Сами параметры такие же, как для метода url_for
из класса ActionController (он подробно обсуждался в главе 3 «Маршрутизация»).
Отметим, что по умолчанию параметр :only_path равен true, так что получается относительный путь /controller/action, а не полностью квалифицированный URL вида http://example.com/controller/action.
Если метод url_for вызван из представления, то он возвращает URL,
к которому применено экранирование HTML. Если вам необходим не­

Модуль UrlHelper

389

экранированный URL, передайте в хеше options параметр :escape =>
false.
Ниже приведен полный список параметров, которые можно задать
в хеше options для метода url_for:
• :anchor. Задает якорь (#anchor), добавляемый в конец пути;
• :only_path. Говорит, что надо генерировать относительный URL
(опустив протокол, имя хоста и номер порта);
• :trailing_slash. Добавляет завершающую косую черту, например
"/archive/2005/". Отметим, что задавать этот параметр не рекомендуется, так как он вступает в конфликт с кэшированием;
• :host. Переопределяет подразумеваемое по умолчанию (текущее)
имя хоста;
• :protocol. Переопределяет подразумеваемый по умолчанию (текущий) протокол;
• :user. Встроенная аутентификация HTTP (необходимо задать также параметр :password);
• :password. Встроенная аутентификация HTTP (необходимо задать
также параметр :user);
• :escape. Определяет, надо ли применять к возвращаемому URL экранирование HTML.
>> url_for(:action => 'index')
=> /blog/
>> url_for(:action => 'find', :controller => 'books')
=> /books/find
>> url_for(:action => 'login', :controller => 'members', :only_path =>
false, :protocol => 'https')
=> https://www.railsapplication.com/members/login/
>> url_for(:action => 'play', :anchor => 'player')
=> /messages/play/#player
>> url_for(:action => 'checkout', :anchor => 'tax&ship')
=> /testing/jump/#tax&amp;ship
>> url_for(:action => 'checkout', :anchor => 'tax&ship', :escape =>
false)
=> /testing/jump/#tax&ship

Связь с именованными маршрутами
Если любому методу из модуля UrlModule, который принимает те же параметры, что url_for, передать не хеш, а экземпляр модели ActiveRecord
или ActiveResource, будет сгенерирован путь для маршрута к этой запи-

390

Глава 11. Все о помощниках

си (если таковой существует). Поиск производится по имени класса,
причем алгоритм достаточно «умен», чтобы, вызвав метод new_record?
для переданной модели, определить, нужна ли ссылка на маршрут
к набору или отдельному члену.
Например, при передаче объекта Timesheet будет предпринята попытка
использовать маршрут timesheet_path. Если маршрут к этому объекту
вложен в другой маршрут, придется вызывать помощник маршрута
явно, так как Rails не в состоянии определить это автоматически:
>> url_for(Workshop.new)
=> /workshops
>> url_for(@workshop) # existing record
=> /workshops/5

Написание собственных модулей
При разработке приложения для Rails нужно пользоваться любой возможностью вынести дублирующийся код в методы-помощники. Специализированные помощники помещаются в один из модулей, находящихся в папке app/helpers вашего приложения.
Написание эффективных методов-помощников – искусство сродни
разработке эффективных API. По существу, помощник – это специализированный API для кода представления, существующий на уровне одного приложения. Очень трудно научить проектированию API
в книге. Это знание приобретается в результате обучения у более опытных программистов и многочисленных проб и ошибок. Тем не менее
в данном разделе мы рассмотрим несколько сценариев и стилей реализации в надежде, что вы найдете что-то подходящее для вашего
приложения.

Мелкие оптимизации: помощник Title
Здесь рассматривается простой метод-помощник, который я использовал во многих своих проектах. Называется он page_title и объединяет
две простые функции, необходимые в любом добротном HTML-документе:
• установку заголовка страницы title в элементе head документа
• краткое описание назначения страницы в элементе h1
Предполагается, что вы хотите сделать элементы title и h1 одинаковыми и увязать метод с шаблоном приложения. В листинге 11.6 приведен код помощника, который следует включить в файл app/helpers/
application_helper.rb, поскольку он должен быть доступен всем представлениям.

Написание собственных модулей

391

Листинг 11.6. Помощник title
def page_title(name)
@title = name
content_tag("h1", name)
end

Сначала метод устанавливает значение переменной @title, а затем выводит элемент h1, содержащий тот же самый текст. Во второй строке
можно было бы применить интерполяцию в строку вида "#{name}
", но мне кажется, что это хуже, чем воспользоваться встроенным
в Rails помощником content_tag.
Мой шаблон приложения ищет переменную @page_title и, если находит, выводит ее перед названием сайта:

Название сайта

Понятно, что метод page_title следует вызывать в месте шаблона представления, где должен появиться элемент h1:

users_path) do |f| %>
...

Инкапсуляция логики представления:
помощник photo_for
Вот еще один сравнительно простой пример. На этот раз мы не просто
будем выводить данные, а инкапсулируем некую логику для определения выводимых данных: фотографии из профиля пользователя или
изображения-заглушки. В противном случае этот кусок пришлось бы
повторять в разных местах приложения.
Контракт с данным помощником заключается в том, что с объектом,
представляющим пользователя, ассоциирован объект profile_photo,
который является экземпляром модели вложения, основанной на подключаемом модуле attachment_fu Рика Олсона. Код в листинге 11.7 понятен и без детального описания этого модуля. Чтобы не усложнять
пример, я решил присваивать значение переменной src в предложении
if/else; а вообще-то здесь просто напрашивается тернарный оператор
Ruby.
Листинг 11.7. Помощник photo_for, инкапсулирующий логику, общую
для разных представлений
def photo_for(user, size=:thumb)
if user.profile_photo

392

Глава 11. Все о помощниках
src = user.profile_photo.public_filename(size)
else
src = 'user_placeholder.png'
end
link_to(image_tag(src), user_path(user))
end

Более сложное представление: помощник breadcrumbs
Во многих веб-приложениях встречается идея хлебных крошек
(breadcrumbs). Под этим понимается расположенный в верхней части
страницы список ссылок, показывающий, насколько далеко пользователь углубился в иерархически организованный сайт. Мне кажется,
что имеет смысл вынести логику построения «хлебных крошек» в отдельный метод-помощник, а не оставлять ее в шаблоне макета.
Идея нашей реализации (листинг 11.8) заключается в том, чтобы воспользоваться наличием переменных экземпляра (зависящих от принятых в вашем приложении соглашений) для определения того, нужно
ли добавлять элементы в массив ссылок-крошек.
Листинг 11.8. Помощник breadcrumbs для корпоративного справочника
1 def breadcrumbs
2 return if controller.controller_name == 'homepage'
3

html = [link_to('Home', home_path)]

4
5
6

# первый уровень
html 3 }

Догадываюсь, что многие опытные программисты писали подобные
подшаблоны и думали, как включить значения по умолчанию для некоторых параметров. В данном случае было бы хорошо не задавать
каждый раз значение :columns, так как в большинстве случаев мы хотим иметь три колонки.
Проблема в том, что, поскольку параметры передаются в хеше :locals
и становятся локальными переменными, не существует простого способа вставить значение по умолчанию в самом подшаблоне. Если опустить часть :columns => n при вызове подшаблона, то Rails возбудит исключение, сообщив, что не существует ни локальной переменной, ни
метода columns. Это вам не переменная экземпляра, с которой можно
обращаться беззаботно, потому что по умолчанию она равна nil.
Опытные «рубисты», вероятно, знают о методе defined?, который позволяет выяснить, есть ли в текущей области видимости указанная локальная переменная, но при его использовании код получается уж
очень уродливым. Следующий вариант можно было бы счесть элегантным, но он не работает!1

1

Если вы знакомы с Ruby, то, наверное, в курсе, что метод Proc.new и его
синоним proc тоже позволяют создавать анонимные блоки кода. Из-за некоторых тонких различий я предпочитаю лямбда-выражения. В блоке лямбда-выражения проверяется арность переданного при вызове списка аргументов, а явный возврат из блока работает корректно.

396

Глава 11. Все о помощниках

Вместо того чтобы рассказывать о непростых идиомах Ruby, я просто
покажу, как данная проблема решается в Rails, и именно с этого места
мы начнем обсуждать технику обертывания помощников.

Написание метода-помощника
Во-первых, я добавлю в модуль CitiesHelper приложения новый методпомощник (листинг 11.10). Поначалу он будет довольно простым. Когда я размышлял, как этот метод назвать, мне пришла в голову мысль,
что tiled(@cities) читается лучше, чем tiles(@cities), поэтому я и выбрал имя tiled.
Листинг 11.10. Метод Tiled из модуля CitiesHelper
module CitiesHelper
def tiled(cities, columns=3)
render :partial => "cities/tiles",
:locals => { :collection => cities, :columns => columns }
end
end

О значении параметра columns по умолчанию я подумал с самого начала, и задал его в параметрах помощника. Это стандартная возможность
языка Ruby.
Теперь, вместо того чтобы вызывать в шаблоне представления метод
render :partial, я могу просто написать , что намного элегантнее и короче. Кроме того, я тем самым разорвал связь между
реализацией таблицы изразцов и представлением. Если в будущем мне
потребуется изменить способ рендеринга таблицы, делать это придется
только в одном месте – в методе-помощнике.

Обобщение подшаблонов
Подготовив сцену, можем начинать шоу. Сначала перенесем помощник
в модуль ApplicationHelper, чтобы он стал доступен всем шаблонам представлений. Файл _tiled_table.html.erb с подшаблоном также перенесем –
в каталог app/views/shared/, чтобы подчеркнуть, что он не связан ни
с каким конкретным представлением. В интересах хорошего стиля
я еще пройдусь по реализации и дам идентификаторам более общие
имена. Ссылка на массив cities теперь будет называться collection,
а переменная блока city – item. Новый код подшаблона представлен
в листинге 11.11.
Листинг 11.11. Код подшаблона изразца с измененными именами
1
2
3

Обертывание и обобщение подшаблонов

397

4

5

6

7

8

9

10

11

12

13

14
15
16
17

Еще остался вопрос о контракте между этим кодом подшаблона и объектами, которые он выводит. Объекты обязаны отвечать на сообщения
main_photo, name и description. Критический анализ других моделей
в приложении показал, что мне необходима большая гибкость. У одних сущностей есть имена, у других – заголовки. Иногда необходимо,
чтобы под именем представленного объекта располагалось его описание, а иногда требуется вставить дополнительные данные об объекте
плюс некоторые ссылки.

Последний штрих: лямбда-выражение
Ruby позволяет сохранять ссылки на анонимные методы (они называются также Proc-объектами, или лямбда-выражениями) и вызывать
их в любой момент времени7. И что нам дает эта возможность? Для начала можно воспользоваться лямбда-выражением, чтобы передать
блок кода, который динамически сформирует части подшаблона.
Например, сейчас написать код показа уменьшенных изображений
проблематично. Этот код сильно зависит от обрабатываемого объекта,
а я хотел бы передавать инструкции о получении миниатюры, не прибегая к огромному предложению if/else и не внося в модели логику,
относящуюся к уровню представления. Сделайте небольшую паузу
и осмыслите только что описанную проблему, а потом посмотрите, как
она решена в листинге 11.12. Подсказка: в переменных thumbnail, link,
title и description хранятся лямбда-выражения!
Листинг 11.12. Подшаблон изразца, переработанный с использованием
лямбда-выражений
1
2
3
4
5
6

7

398

Глава 11. Все о помощниках
8
9

Отметим, что содержимое левого и правого DIV берется из переменных,
содержащих лямбда-выражения. В строке 2 мы вызываем метод link_
to, причем оба его аргумента вычисляются динамически. Аналогичная
конструкция в строке 6 обеспечивает порождение ссылки title. В обоих случаях первое лямбда-выражение должно вернуть результат обращения к методу image_tag, а второе – URL. Переменная item всюду содержит текущий выводимый объект, который передается лямбда-выражению как переменная блока.

Говорит Уилсон…
Вместо link.call(item) можно было бы написать link[item]. Это выглядит еще круче, только есть опасность свихнуться. (Proc#[] – синоним Proc#call.)

Новый метод-помощник Tiled
Если теперь вы посмотрите на листинг 11.13, то увидите, что метод
tiled претерпел существенные изменения. Чтобы список позиционных
аргументов не слишком разрастался, я решил передавать последним
параметром хеш опций options. Это полезный подход, аналогичный
принятому во всех стандартных помощниках Rails.
Одна из опций, :link, уникальна для каждого передаваемого объекта,
и значения по умолчанию у нее нет, поэтому я проверяю наличие данной опции в строке 3. Для всех остальных параметров имеются зна­
чения по умолчанию, и они передаются методу render :partial в хеше
:locals.
Листинг 11.13. Метод-помощник Tiled с лямбда-выражениями
в качестве параметров
1 module ApplicationHelper
2 def tiled(collection, opts={})
3 raise 'link option is required' unless opts[:link]
4

opts[:columns] ||= 3

5
6
7

opts[:thumbnail] ||= lambda do |item|
image_tag(item.photo.public_filename(:thumb))
end

8
9

opts[:title] ||= lambda {|item| item.to_s }
opts[:description] ||= lambda {|item| item.description }

Заключение

399

10 render :partial => "shared/tiled_table",
11
:locals => { :collection => collection,
12
:columns => opts[:columns] || 3,
13
:thumbnail => opts[:thumbnail],
14
:title => opts[:title],
15
:description => opts[:description] }
16 end
17 end

И, чтобы завершить пример, покажем, как вызывается новый помощник tiled из шаблона:
lambda {|city| city_path(city)}) %>

Метод city_path доступен блоку лямбда-выражения, поскольку это замыкание, наследующее контекст исполнения, в котором создано.

Заключение
Это была длинная глава, которую можно использовать в качестве подробного справочника по методам-помощникам, предоставляемым Rails,
а также как источник идей для написания собственных. Эффективное
применение помощников позволяет создавать элегантные шаблоны,
удобные для сопровождения.
Прежде чем завершить рассмотрение ActionPack (это название объединяет ActionController и ActionView), мы отправимся в путешествие по
миру Ajax и Javascript. Пожалуй, одной из основных причин популярности Rails является поддержка двух этих ключевых для Web 2.0 технологий.

По договору между издательством «Символ-Плюс» и Интернет-магазином
«Books.Ru – Книги России» единственный легальный способ получения данного файла с книгой ISBN 978-5-93286-137-0, название «Путь Rails. Подробное
руководство по созданию приложений в среде Ruby on Rails» – покупка в Интернет-магазине «Books.Ru – Книги России». Если Вы получили данный файл
каким-либо другим образом, Вы нарушили международное законодательство
и законодательство Российской Федерации об охране авторского права. Вам необходимо удалить данный файл, а также сообщить издательству «Символ-Плюс»
(piracy@symbol.ru), где именно Вы получили данный файл.

12
Ajax on Rails
Ajax – это не технология, а сплав нескольких технологий,
которые полезны сами по себе, но в сочетании открывают
совершенно новые возможности.
Джесси Дж. Гарретт, изобретатель термина

Акроним Ajax расшифровывается как Asynchronous JavaScript and
XML (асинхронный JavaScript и XML). Он охватывает технологии, которые позволяют оживить веб-страницы за счет действий, происходящих вне нормального жизненного цикла HTTP-запроса (без полного
обновления страницы).
Вот некоторые применения техники Ajax:
• асинхронная отправка данных формы;
• непрерывная навигация по представленным в веб картам, как, например, на сайте Google Maps;
• динамическое обновление списков и таблиц, как в Gmail и других
почтовых веб-сервисах;
• электронные таблицы в Сети;
• формы, допускающие редактирование «на месте»;
• немедленный просмотр отформатированного текста.
Идея Ajax стала возможной благодаря API XMLHttpRequestObject
(XHR), реализованному во всех современных броузерах. Он позволяет
JavaScript-сценарию на стороне броузера обмениваться данными с сер-

Библиотека Prototype

401

вером и использовать полученные данные для изменения пользовательского интерфейса приложения «на лету», без полного обновления
страницы. Написать код, который напрямую работает с XHR во всех
броузерах, мягко говоря, нелегко. Именно поэтому развелось так много библиотек с открытыми исходными текстами, которые поддерживают Ajax.
Кстати, Ajax, особенно в Rails, имеет очень мало общего с XML, несмотря на присутствие буквы X в акрониме. Полезная нагрузка асин­
хронных запросов и ответов от сервера может быть произвольной. Часто серверу отправляются просто параметры формы, а в ответ возвращаются фрагменты HTML, вставляемые в DOM страницы. Нередко сервер
посылает данные, представленные в формате JavaScript Object Notation
(JSON) – упрощенной разновидности языка JavaScript.
В задачу этой книги не входит изучение основ JavaScript или Ajax. Мы
не будем также вдаваться в вопросы проектирования, касающиеся добавления Ajax в приложение. Это долгая и противоречивая история.
Для надлежащего рассмотрения этих тем потребовалась бы целая книга, и такие книги на рынке есть. Поэтому в оставшейся части главы
просто предполагается, что вы понимаете смысл технологии Ajax и причины использования ее в своих приложениях.
Ruby on Rails до предела упрощает включение Ajax в приложение
благодаря изобретательной интеграции с библиотеками Prototype
и Scriptaculous. В начале этой главы мы поговорим об идеологии и реализации этих JavaScript-библиотек, а потом перейдем к справочному
разделу, в котором описаны методы-помощники из ActionPack, поддер­
живающие Ajax on Rails. Мы рассмотрим также имеющийся в Rails
механизм RJS, позволяющий вызывать JavaScript с помощью серверного кода на Ruby.
Чтобы извлечь из этой главы максимум пользы, вы должны быть хотя
бы немного знакомы с программированием на языке JavaScript.

Библиотека Prototype
Библиотеку Prototype (находится по адресу http://prototype.conio.net)
написал и активно сопровождает Сэм Стефенсон (Sam Stephenson) –
участник команды разработчиков ядра Rails. Автор описывает эту библиотеку как «уникальный, простой в употреблении инструментарий
для разработки на базе классов» и «самую лучшую библиотеку для
поддержки Ajax».
Библиотека Prototype входит в дистрибутив Ruby on Rails и копируется во все вновь создаваемые проекты под именем public/javascripts/
prototype.js. Она насчитывает примерно 2000 строк кода на языке
JavaScript и закладывает фундамент для организации любых видов Ajax-

402

Глава 12. Ajax on Rails

взаимодействий с сервером и программирования визуальных эффектов
на стороне клиента. Фактически, несмотря на тесную связь с Ruby on
Rails, библиотека Prototype исключительно полезна и сама по себе.

Подключаемый модуль FireBug
FireBug1 – это чрезвычайно мощное расширение для броузера Firefox,
которое обязательно должны поставить все желающие разрабатывать
Ajax-приложения. Оно позволяет инспектировать Ajax-запросы, детально исследовать DOM страницы и даже изменять на лету элементы
и CSS-стили, причем эффект изменений сразу же показывается в окне
броузера. Кроме того, это еще и «могучий» отладчик JavaScript, кото-

Рис. 12.1. FireBug – необходимая вещь для разработчиков Ajax-приложений

рый дает возможность задавать наблюдаемые выражения и устанавливать точки прерывания (рис. 12.1).
Встроенный в FireBug инспектор DOM можно использовать для исследования поведения библиотеки Prototype во время выполнения на странице броузера. FireBug обладает также интерактивной консолью, позволяющей экспериментировать с JavaScript в броузере точно так же,
как irb делает это с Ruby.
Некоторые примеры кода данной главы скопированы из консоли
FireBug, которая выдает приглашение >>>. Так, при инспектировании
объекта Prototype в консоли получается следующий результат:
>>> Prototype
Object Version=1.5.0_rc2 BrowserFeatures=Object

Рассказывая об Ajax on Rails, я в шутку наставлял своих студентов:
«Даже если вы не слышите больше ничего из того, что я говорю, используйте FireBug! Повышение вашей продуктивности очень быстро
окупит затраты на мой гонорар».
1

Для установки подключаемого к Firefox модуля FireBug необходимо зайти
на сайт http://www.getfirebug.com/.

Библиотека Prototype

403

Prototype API
Понимать принципы устройства и функционирования Prototype API
для работы с Ajax on Rails необязательно, но это будет весьма полезно,
когда вы захотите выйти за пределы простых примеров и начнете писать собственные функции на JavaScript.
Значительная часть кода в файле prototype.js посвящена определению
нетривиальных объектно-ориентированных языковых конструкций
сверх уже имеющихся в JavaScript. Например, функция extend открывает дорогу к наследованию. Многие части библиотеки Prototype покажутся программистам на Ruby удивительно знакомыми, например метод
inspect класса Object и метод gsub класса String. Поскольку в JavaScript
функции работают как замыкания аналогично блокам Ruby, то для работы с массивами, манипуляций с итераторами и во многих других аспектах API Prototype берет Ruby за образец.
В общем, по духу код Prototype очень близок к Ruby, что позволяет
знатокам Ruby и Rails комфортно себя чувствовать и продуктивно работать. Вы даже можете полюбить язык JavaScript (если этого еще не
произошло), который, несмотря на изначальную непритязательность
и дурную репутацию, на самом деле является чрезвычайно мощным
и выразительным языком программирования. Не обращайте внимания на повсеместно встречающееся ключевое слово function, в конце
концов оно просто станет неразличимой деталью фона.

Функции верхнего уровня
Следующие функции определены в контексте Prototype верхнего
уровня.

$(id[, id2...])
Функция $ – это одновременно сокращенная запись и расширение одной из самых употребительных функций при программировании на
JavaScript в броузере: document.getElementByID. Поскольку данная функ­
ция используется очень часто, для нее выбрано предельно короткое
имя, что согласуется с одним из основных принципов проектирования
эффективного API.
Функции $() можно передать одну или несколько строк, а в ответ она
вернет либо один соответствующий элемент, либо массив элементов
в предположении, что на странице есть элементы с указанными атрибутами ID. Для удобства функция $ не возбуждает исключения, когда
ей передан экземпляр элемента, а не строка. Она просто возвращает
этот же элемент или добавляет его в результирующий массив.
Если элемента с указанным ID не существует, для него возвращается
значение undefined, что согласуется с поведением исходной функции
document.getElementByID. Попытка получить более одного элемента с од-

404

Глава 12. Ajax on Rails

ним и тем же ID, скорее всего, успеха иметь не будет, хотя это зависит от
реализации конкретного броузера. Лучше формировать разметку правильно, чтобы элементов с одинаковыми идентификаторами не было.

$$(expr[, expr2...])
Функция $$ принимает один или несколько CSS-селекторов и возвращает массив соответствующих элементов DOM. Способность искать элементы по CSS-селекторам – одна из наиболее важных особенностей
Prototype.

$A(var)
Функция $A – синоним Array.from. Она преобразует свой параметр в объект Array, включающий функции объекта Enumerable (см. раздел «Объект
Enumerable» ниже в этой главе).
Внутри Prototype функция $A используется главным образом для преобразования списков аргументов и узлов DOM в массивы. Отметим, что
в последних версиях Prototype функции объекта Enumerable примешиваются прямо к встроенному в JavaScript объекту Array, поэтому
пользы от функции уже немного.

$F(id)
Функция $F – синоним Form.Element.getValue. Она возвращает значение
поля формы с заданным ID. Это полезный вспомогательный метод, по­
скольку работает он вне зависимости о того, является ли запрошенное
поле текстовым, списком select или текстовой областью (TEXTAREA).

$H(obj)
Функция $H расширяет простой объект JavaScript из объектов Enumerable
и Hash, делая его похожим на хеш в понимании Ruby (см. раздел «Объект Hash» ниже в этой главе).

$R(start, end, exclusive)
Функция $R – сокращенная запись конструктора ObjectRange (см. раздел «Объект ObjectRange» ниже в этой главе).

Try.these(func1, func2[, func3...]
Строго говоря, это не функция верхнего уровня, но мне показалось
удобным включить ее в этот раздел, так как these – единственная функ­
ция объекта Try.
При выполнении операций, которые по-разному реализованы в разных
броузерах, часто приходится пробовать различные способы, пока не
найдется подходящий. В объекте Try определена функция these; ей пе-

Библиотека Prototype

405

редается список функций, которые выполняются поочередно, пока не
найдется функция, не возбуждающая исключение.
Классический пример, взятый из кода самой библиотеки Prototype, –
способ получения ссылки на объект XMLHttpRequest, который существенно различен для Firefox и Internet Explorer:
var Ajax = {
getTransport: function() {
return Try.these(
function() {return new XMLHttpRequest()},
function() {return new ActiveXObject(‘Msxml2.XMLHTTP’)},
function() {return new ActiveXObject(‘Microsoft.XMLHTTP’)}
) || false;
},
activeRequestCount: 0
}

Объект Class
В объекте Class определена функция create, применяемая для объявления новых экземпляров Ruby-подобных классов. Далее эти классы могут объявить функцию initialize, которая будет выступать в роли кон­
структора при вызове new для создания нового экземпляра.
Ниже в качестве примера приведена реализация объекта ObjectRange:
ObjectRange = Class.create();
Object.extend(ObjectRange.prototype, Enumerable);
Object.extend(ObjectRange.prototype, {
initialize: function(start, end, exclusive) {
this.start = start;
this.end = end;
this.exclusive = exclusive;
},
...
});
var $R = function(start, end, exclusive) {
return new ObjectRange(start, end, exclusive);
}

Сначала для ObjectRange создается класс (который будет вести себя похоже на class ObjectRange в Ruby). Затем объект prototype объекта
ObjectRange расширяется с целью добавить методы экземпляра. К нему
подмешиваются функции объекта Enumerable, а затем – функции анонимного JavaScript-объекта, определенного с помощью фигурных
скобок, внутри которых находится функция initialize и прочие методы экземпляра.

406

Глава 12. Ajax on Rails

Расширения класса JavaScript Object
Одна из причин, по которым код, написанный с применением Prototype,
может выглядеть так чисто и кратко, – тот факт, что функции подмешиваются непосредственно в базовые классы JavaScript (в частности
Object).

Object.clone(object)
Возвращает копию объекта object, переданного в качестве параметра.
Делается это путем использования объекта-параметра для расширения
нового экземпляра Object:
clone: function(object) {
return Object.extend({}, object);
}

Object.extend(destination, source)
Статическая функция extend в цикле перебирает все свойства переданного объекта source, включая функции, и копирует их в объект destination.
Тем самым она служит основой механизмов наследования и клонирования (в языке JavaScript нет встроенной поддержки наследования).
Исходный код настолько поучителен и прост, что я решил включить
его целиком:
Object.extend = function(destination, source) {
for (var property in source) {
destination[property] = source[property];
}
return destination;
}

Object.keys(obj) и Object.values(obj)
Объекты в JavaScript ведут себя почти так же, как ассоциативные массивы (или хеши) в других языках, и повсеместно используются в таком
духе. Статическая функция keys возвращает список свойств, определенных в объекте. Статическая функция values возвращает список значений свойств.

Object.inspect(param)
Если параметр не определен (в JavaScript это не соответствует равенству null), статическая функция inspect возвращает строку ‘undefined’.
Если же параметр равен null, возвращается строка ‘null’. Если в объекте-параметре определена функция inspect(), она вызывается и возвращается ее результат. В противном случае вызывается функция
toString().

Библиотека Prototype

407

Расширения класса JavaScript Array
Помимо определенных в объекте Enumerable, для массивов доступны
также методы, перечисленные ниже.

array.clear()
Удаляет из массива все элементы и возвращает его. Интересно, что реализация этого метода просто устанавливает длину массива равной
нулю:
clear: function() {
this.length = 0;
return this;
}

array.compact()
Удаляет из массива все элементы, равные null и undefined, и возвращает
его. Обратите внимание на употребление функции select в реализации:
compact: function() {
return this.select(function(value) {
return value != undefined || value != null;
});
}

array.first() и array.last()
Возвращают первый и последний элементы массива соответственно.

array.flatten()
Принимает массив и рекурсивно «разглаживает» его, копируя элементы в новый массив, который и возвращает. Иными словами, функция
перебирает все элементы исходного массива и для каждого элемента,
который сам является массивом, копирует его элементы в возвращаемый массив. Обратите внимание на употребление функции inject в реализации:
flatten: function() {
return this.inject([], function(array, value) {
return array.concat(value && value.constructor == Array ?
value.flatten() : [value]);
});
}

array.indexOf(object)
Возвращает индекс элемента object в массиве или -1, если элемент не
найден:

408

Глава 12. Ajax on Rails
indexOf: function(object) {
for (var i = 0; i < this.length; i++)
if (this[i] == object) return i;
return -1;
});
}

array.inspect()
Переопределяет функцию inspect из объекта Object, так что она печатает элементы массива через запятую:
indexOf: function() {
return ‘[‘ + this.map(Object.inspect).join(‘, ‘) + ‘]’;
}

array.reverse(inline)
Изменяет порядок элементов в массиве на противоположный. Если аргумент inline равен true (это значение подразумевается по умолчанию),
модифицируется исходный массив, в противном случае он остается неизменным, и возвращается копия.

array.shift()
Удаляет из массива последний элемент и возвращает его. В результате
размер массива уменьшается на 1.

array.without(obj1[, obj2, ...])
Удаляет из массива элементы, перечисленные в аргументах. Принимает множество удаляемых элементов в виде массива или списка, причем
единообразие достигается за счет применения к аргументам функции
$A. Обратите внимание на употребление функции select в реализации:
without: function() {
var values = $A(arguments);
return this.select(function(value) {
return !values.include(value);
});
}

Расширения объекта document
Метод document.getElementsByClassName(className [, parentElement]) возвращает список элементов DOM, для которых имя CSS-класса равно
className. Необязательный параметр parentElement позволяет ограничить поиск конкретной ветвью DOM, а не просматривать весь документ,
начиная с элемента body (режим по умолчанию).

Библиотека Prototype

409

Расширения класса Event
Для удобства в класс Event добавлены следующие константы:
Object.extend(Event, {
KEY_BACKSPACE: 8,
KEY_TAB:
9,
KEY_RETURN: 13,
KEY_ESC:
27,
KEY_LEFT:
37,
KEY_UP:
38,
KEY_RIGHT:
39,
KEY_DOWN:
40,
KEY_DELETE: 46
});

Их наличие упрощает программирование обработчиков событий клавиатуры. В следующем примере мы применяем в обработчике события
onKeyPress предложение switch, чтобы проверить, не нажал ли пользователь клавишу Escape.
onKeyPress: function(event) {
switch(event.keyCode) {
case Event.KEY_ESC:
alert(‘Отменено’);
Event.stop(event);
}
}

Event.element()
Возвращает элемент, являющийся источником события.

Event.findElement(event, tagName)
Обходит дерево DOM снизу вверх, начиная с элемента – источника события. Возвращает первый встретившийся элемент, для которого имя тега равно tagName (без учета регистра). Если подходящих элементов не
найдено, функция возвращает сам элемент-источник, а не завершается
с ошибкой, что может приводить к некоторой путанице.

Event.isLeftClick(event)
Возвращает true, если событие вызвано щелчком левой кнопкой мыши.

Event.observe(element, name, observer, useCapture) и Event.
stopObserving(element, name, observer, useCapture)
Функция observe обертывает встроенную в броузер функцию addEvent­
Lis­tener, включенную в спецификацию DOM Level 2. Она устанавлива­-

410

Глава 12. Ajax on Rails

ет отношение «наблюдаемый-наблюдатель» между указанным элементом element и функцией observer. Параметр element может быть как
строковым идентификатором ID, так и самим элементом. В случае событий мыши и клавиатуры в этом качестве часто выступает элемент
document.
Функция stopObserving, обертывающая встроенный метод DOM remo­
veEvent­Listener, разрывает связь между элементом и обработчиком события.
Параметр name должен быть названием события (в виде строки), определенного в спецификации DOM для броузеров (blur, click и т. д.)1.
Параметр observer должен быть ссылкой на функцию, то есть именем
функции без скобок (частый источник путаницы!). Почти всегда в сочетании с observe используется функция bindAsEventListener, чтобы обработчик события исполнялся в правильном контексте (см. ниже раздел «Расширения класса JavaScript Function»).
Необязательный параметр useCapture позволяет указать, что обработчик следует вызывать на фазе погружения (capture), а не всплытия
(bubbling), по умолчанию он равен false.
Следующий пример взят из реализации объекта AutoCompleter, входящего в библиотеку Scriptaculous:
addObservers: function(element) {
Event.observe(element, “mouseover”,
this.onHover.bindAsEventListener(this));
Event.observe(element, “click”,
this.onClick.bindAsEventListener(this));
}

Event.pointerX(event) and Event.pointerY(event)
Возвращает координаты x и y курсора мыши в момент возникновения
события.

Event.stop(event)
Останавливает распространение события и отменяет поведение по умолчанию, как бы оно ни было определено.

Расширения класса JavaScript Function
Следующие две функции примешиваются к встроенному классу
Function.

1

Исчерпывающее описание событий DOM и способов работы с ними см. на
странице http://www.quirksmode.org/dom/w3c_events.html.

Библиотека Prototype

411

function.bind(obj)
Используется для привязывания функции к контексту объекта, переданного в качестве параметра. Почти всегда этот параметр равен this,
то есть привязка происходит к контексту текущего объекта, поскольку
основное назначение bind – гарантировать, что функция, определенная
в каком-то другом месте, будет исполняться в том контексте, где вы
сейчас находитесь.
Вот, например, как реализована функция registerCallback в объекте
PeriodicalExecuter:
registerCallback: function() {
setInterval(this.onTimerEvent.bind(this), this.frequency * 1000);
}

Необходимо привязать функцию onTimerEvent к контексту объекта, для
которого регистрируется обратный вызов, а не к объекту-прототипу самого PeriodicalExecuter.
Концепцию привязки нелегко усвоить, если вы не являетесь опытным
программистом на JavaScript и не имеете склонности к функциональному программированию, поэтому не переживайте, если с первого раза
вам не удалось ее понять.

function.bindAsEventListener(obj)
Применяется для присоединения данной функции в качестве обработчика события DOM таким образом, что объект event будет передан ей как
параметр. Используйте так же, как bind, если хотите гарантировать,
что некоторый метод будет выполняться в контексте конкретного экземпляра, а не прототипа класса, в котором определен. В самой библиотеке Prototype этот метод не используется, но широко применяется
в библиотеке Scriptaculous и в JavaScript-приложениях, когда необходимо создать класс-наблюдатель, содержащий обработчики событий,
которые привязаны к элементам страницы.
В следующем примере приведен код класса, призванного извещать об изменениях в некотором поле ввода, выдавая настраиваемое сообщение:
var InputObserver = Class.create();
InputObserver.prototype = {
initialize: function(input, message) {
this.input = $(input);
this.message = message;
this.input.onchange = this.alertMessage.bindAsEventListener(this);
},
alertMessage: function(e) {
alert(this.message + ‘ (‘ + e.type + ‘)’);
}
};
var o = new InputObserver(‘id_поля ввода’, ‘Поле ввода’);

412

Глава 12. Ajax on Rails

Расширения класса JavaScript Number
Следующие функции примешаны к встроенному классу Number.

number.toColorPart()
Возвращает шестнадцатеричное представление целочисленного RGBкода цвета:
toColorPart: function() {
var digits = this.toString(16);
if (this < 16) return ‘0’ + digits;
return digits;
},

Напомню, что числа в JavaScript автоматически не «обертываются
в объект». Вы должны присвоить числовое значение переменной, самостоятельно обернуть число в экземпляр класса Number или просто заключить его в круглые скобки – только тогда можно будет вызывать для
него методы. Не убедил? Можете проверить сами в консоли FireBug:
>>> 12.toColorPart();
missing ; before statement 12.toColorPart();
>>> n = new Number(12)
12
>>> n.toColorPart();
“0c”
>>> n = 12
12
>>> n.toColorPart();
“0c”
>>> (12).toColorPart();
“0c”
>>> 12.toColorPart
missing ; before statement 12.toColorPart
>>> (12).toColorPart
function()

number.succ()
Возвращает следующее по порядку число:
succ: function() {
return this + 1;
},

number.times()
Подобно методу times, который существует в Ruby для числовых объектов, метод number.times() принимает блок кода и вызывает его number
раз (соответствующих значению числа, для которого вызван метод).
Обратите внимание на использование функции $R, которая позволяет

Библиотека Prototype

413

легко создать диапазон, и функции each для вызова указанной функции iterator:
times: function(iterator) {
$R(0, this, true).each(iterator);
return this;
}

Ниже приведен простой пример, в котором пять раз вызывается функция alert. Напомню, что вызывать JavaScript-функцию напрямую для
необернутого числа нельзя, поскольку синтаксический анализатор
ожидает скобки:
>>> (5).times(new Function(“alert(‘yeah’)”))
5

Расширения класса JavaScript String
Следующие функции примешаны к встроенному классу String.

string.camelize()
Преобразует строки с разделителями-дефисами к виду lowerCamelCase:
>>> “about-to-be-camelized”.camelize()
“aboutToBeCamelized”

string.dasherize()
Преобразует строки с разделителями-подчерками в строки с разделителями-дефисами:
>>> “about_to_be_dasherized”.dasherize()
“about-to-be-dasherized”

string.escapeHTML() и string.unescapeHTML()
Функция экземпляра escapeHTML экранирует HTML и XML-разметку
в строке, преобразуя угловые скобки в соответствующие компоненты:
>>> ‘’.escapeHTML()
“&lt;script src=”http://evil.org/bad.js”/&gt;”

Функция unescapeHTML выполняет обратную операцию.

string.evalScripts() и string.extractScripts()
Функция экземпляра evalScripts выполняет содержимое всех тегов
, встречающихся в строке.
Функция экземпляра extractScripts возвращает массив строк с содержимым тегов , встречающихся в строке. Отметим, что сами открывающие и закрывающие теги не включаются, извлекается
только JavaScript-код.

414

Глава 12. Ajax on Rails

string.gsub(pattern, replacement) и string.sub (pattern,
replacement, count)
Функция экземпляра gsub возвращает копию строки, в которой все
вхождения образца pattern заменены строкой replacement. Исходная
строка не модифицируется. Образец должен быть литеральным регулярным выражением JavaScript, заключенным между символами «/».
Функция sub аналогична gsub, но выполняет не более count замен, причем по умолчанию count равно 1.

string.scan(pattern, iterator)
Функция экземпляра scan очень похожа на gsub, но вместо строки замены принимает итератор.

string.strip()
Функция экземпляра strip удаляет начальные и хвостовые пробелы.
Обратите внимание на сцепленные вызовы replace в реализации:
strip: function() {
return this.replace(/^\s+/, ‘’).replace(/\s+$/, ‘’);
}

string.stripScripts() и string.stripTags()
Функция экземпляра stripScripts удаляет из строки все теги
(вместе с содержимым), а функция экземпляра stripTags – все HTMLи XML-теги.

string.parseQuery() и string.toQueryParams()
Обе функции преобразуют строку запроса (в формате, принятом в URL)
в объект JavaScript:
>>> “?foo=bar&da=da+do+la”.toQueryParams()
Object foo=bar da=da+do+la

string.toArray()
Возвращает массив символов, составляющих строку.

string.truncate(length, truncationString)
Работает, как метод truncate, который Rails подмешивает к строкам.
Если длина строки больше length, она усекается, и в конец добавляется
строка truncationString (по умолчанию равная “...”).
>>> “Mary had a little lamb”.truncate(14)
“Mary had a ...”

Библиотека Prototype

415

string.underscore()
Прямой перенос метода underscore, который Rails подмешивает к строкам. Преобразует строки, записанные в верблюжьейНотации в строки
с разделителями-подчерками. Изменяет :: на / с целью преобразования
пространств имен Ruby в пути:
>>> “ActiveRecord::Foo::BarCamp”.underscore()
“active_record/foo/bar_camp”

Объект Ajax
Объект Ajax сам по себе обладает полезным поведением, а также служит корневым пространством имен для других относящихся к Ajax
объектов в библиотеке Prototype.

Ajax.activeRequestCount
Содержит количество исполняемых в данный момент Ajax-запросов.
Поскольку они выполняются асинхронно, это значение может быть
больше единицы. Используется для реализации индикаторов активности – маленьких анимированных картинок, извещающих пользователя о том, что сейчас происходит обращение к серверу:
Ajax.Responders.register({
onCreate: function() {
if($(‘busy’) && Ajax.activeRequestCount > 0)
Effect.Appear(‘busy’, { duration:0.5, queue:’end’ });
},
onComplete: function() {
if($(‘busy’) && Ajax.activeRequestCount == 0)
Effect.Fade(‘busy’, {duration:0.5, queue:’end’ });
}
});

Ajax.getTransport()
Возвращает ссылку на объект XMLHttpRequestObject, предоставляемый
броузером. Обычно вам не нужно обращаться к этой функции самостоятельно – ее вызывают другие функции, относящиеся к Ajax.

Объект Ajax.Responders
Объект Responders управляет списком обработчиков, заинтересованных
в получении извещений о событиях, которые касаются Ajax. В предыдущем примере показано, как