Продолжается подписка на наши издания! Вы не забыли подписаться?

Создание собственных Web-приложений с помощью Ruby on Rails

Автор: Патрик Ленз
Опубликовано: 20.10.2008
Главы из книги Патрика Ленза Build Your Own Ruby On Rails Web Applications, SitePoint, 2007

Представляем Ruby on Rails

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

Во-первых, что же такое Ruby on Rails?

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

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

Фреймворк предоставляет программисту классы, реализующие функции, общие для всех Web-приложений, включая:

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

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

ПРИМЕЧАНИЕ

На что похож Ruby?

Если у вас есть опыт программирования на других языках, например, PHP or Java, для вас, возможно, будет иметь смысл следующий Ruby-код, хотя некоторые части его могут показаться новыми:

>> "What does Ruby syntax look like?".reverse

=> "?ekil kool xatnys ybuR seod tahW"

>> 8 * 5 => 40

>> 3.times { puts "cheer!" } cheer! cheer! cheer!

>> %w(one two three).each { |word| puts word.upcase } ONE TWO THREE

История

Корни Ruby on Rails лежат в приложении Basecamp (http://www.basecamphq.com), решении для управления проектами, созданного датским Web-разработчиком для студии дизайна 37signals (http://www.37signals.com/). Во многом благодаря успеху Basecamp 37signals перешла от Web-дизайна к разработке приложений, а Heinemeier Hansson стал совладельцем фирмы.

Говоря о корнях, я имею в виду, что Rails исходно не создавался как отдельный фреймворк. Его извлекли из реального приложения, которое уже использовалось, чтобы использовать в других приложениях, замышлявшихся 37signals. Heinemeier Hansson увидел возможность упростить свою работу (и жизнь) при помощи помещения такой общей функциональности, как абстрагирование от СУБД и использование шаблонов, в то, что позже стало первой версией Ruby on Rails.

Он решил выпустить Rails как ПО с открытым кодом, чтобы "фундаментально изменить способы создания Web-сайтов". Первая бета-версия Rails была выпущена в июле 2004 года, а первый релиз – в декабре 2005. На момент написания этой книги скачано более 300.000 копий Rails, и это число продолжает расти.

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

Heinemeier Hansson начал разработку Rails и по-прежнему руководит ей, но фреймворк много выиграл от открытости кода. Со временем Rails-разработчики внесли тысячи расширений и исправлений в репозиторий кода Rails. Репозиторий находится под контролем команды Rails, в которую входят около 12 высококвалифицированных профессиональных разработчиков, выбранных из толпы участников, под руководством Heinemeier Hansson.

Итак, теперь вы знаете, что такое Rails, и откуда он взялся. Но с чего бы вам тратить драгоценное время на его изучение?

Хорошо, что вы спросили.

Принципы разработки

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

Благодаря этим принципам, Ruby on Rails является фреймворком, который реально экономит силы и время разработчика. Посмотрим повнимательнее на эти принципы, чтобы понять, как он это делает.

Соглашение по конфигурации

Концепция "соглашения по конфигурации" относится к тому, что Rails предлагает набор установок по умолчанию для способа построения типичного Web-приложения.

Знаете, многие другие фреймворки (такие, как основанный на Java Struts или основанный на Python Zope) требуют, чтобы вы прошли длинный процесс конфигурирования, прежде чем сможете приступить к созданию хотя бы простейшего приложения. Конфигурационная информация обычно сохраняется в куче XML-файлов, и эти файлы могут стать реально большими и сложными в поддержке. Во многих случаях вам приходится проходить этот процесс при начале каждого нового проекта.

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

Другие соглашения, принятые в Rails, включают именование сущностей, связанных с БД, и процесс поиска контроллерами соответствующих моделей и представлений.

development: 
adapter: mysql 
database: rails_development 
username: root 
password: 
host: localhost

test: 
adapter: mysql 
database: rails_test 
username: root 
password: 
host: localhost

production: 
adapter: mysql 
database: rails_production 
username: root 
password: 
host: localhost

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

Не повторяйтесь

Rails поддерживает принцип DRY (Don’t Repeat Yourself, не повторяйтесь). Когда вы решаете изменить поведение приложения, построенного на этом принципе, вам не приходится править код приложения более чем в одном месте.

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

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

Rails также придерживается принципа DRY, когда дело доходит до реализации ультрасовременных техник, типа Ajax (Asynchronous JavaScript and XML). Ajax – это подход, который позволяет Web-приложению заменять содержание в браузере пользователя динамически, или обмениваться данными формы с сервером, не перезагружая страницу. При создании Ajax-приложений разработчики часто занимаются дублированием кода: в конце концов, сайт должен функционировать в браузерах, которые не поддерживают Ajax, так же, как в тех, которые поддерживают, и код, нужный для вывода результатов в обоих типах браузерах, в основном, идентичен. Rails облегчают работу с разными поколениями браузера без дублирования кода.

Быстрая разработка (Agile Development)

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

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

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

Вот несколько примеров, иллюстрирующих, как Rails использует гибкие методологии:

Если у вас от всех этих принципов голова идет кругом, не волнуйтесь – мы собираемся повторять их снова и снова, на каждом шаге создания нашего Web-приложения на Ruby on Rails!

Создание примера Web-приложения

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

Создаваемое нами приложение будет действующим клоном популярного блог-сайта digg.com.

Что такое digg?

digg.com (или просто “digg” ) описывает себя так:

Digg – это направляемый пользователями Web-site с социальным контентом. ОК, что значит вся эта чертовщина? Все, что есть на digg, предоставлено сообществом пользователей (на их месте могли бы быть вы). После того, как вы отправите контент, другие пользователи оценивают в нем то, что им больше нравится.

Если ваш рассказ набирает популярность, он помещается на первую страницу сайта – для показа миллионам посетителей digg.

Когда количество оценок, выставленных рассказу, превышает некий предел, он автоматически продвигается на заглавную страницу сайта, где привлекает куда большее число посетителей. На рисунке 1.1 показана заглавная страница сайта digg.com.


Рисунок 4.1.

Сайт digg открылся в декабре 2004 года и с тех пор вошел в Top 200 сайтов Интернета по трафику (по рейтингу Alexa)

Я решил показать вам, как создать клон digg не потому, что этот сайт популярен у пользователей; набор возможностей сайта digg не очень сложен, но достаточен, чтобы продемонстрировать наиболее важные и полезные грани фреймворка Ruby on Rails.

Возможности приложения-примера

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

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


Рисунок 1.2.

Что такое теги?

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

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

Теги широко используются на таких сайтах, как, например, Flickr, показанный на рисунке 1.2.

Глава 4, Найденные рельсы

Как вы могли понять из главы 1, Introducing Ruby on Rails, в основу кода, составляющего фреймворк Rails, было заложено всего несколько идей. С течением времени многие части кода были переписаны, что повысило быстродействие и эффективность, а также позволило реализовать дополнительные возможности, но исходная архитектура в основном осталась неизменной. Эта глава прольет свет на внутренности Rails.

Три окружения (environments)

Rails поощряет использование разных окружений для каждого из этапов жизненного цикла приложения – разработки, тестирования и использования. Если вы уже имеете опыт в разработке Web-приложений, возможно, вы так и поступаете; Rails просто формализует эти окружения.

Разработка

В окружении, предназначенном для разработки, изменения в исходном коде приложения видны сразу; все, что нужно – обновить страницу в браузере. В таком окружении скорость не критична; вместо этого фокус делается на обеспечении максимального понимания разработчиком компонентов, отображаемых на каждой странице. Если ошибка происходит в окружении, предназначенном для разработки, разработчик может сразу сказать, какая строка кода отвечает за ошибку, и как эта конкретная строка была вызвана. Эта возможность обеспечивается стеком вызовов (полным списком всех вызовов методов, приведших к ошибке), которая выводится в случае непредвиденной ошибки.

Тесты

При тестировании мы обычно обновляем БД бессмысленными данными перед каждым повторением теста – это гарантирует корректность результатов теста и повторяемость результатов. Процедуры функционального и юнит-тестирования в Rails полностью автоматизированы.

При тестировании Rails-приложения мы не видим его в традиционном браузере. Тесты вызываются из командной строки и могут исполняться как фоновый процесс. Окружение тестирования поддерживает это.

Исполнение

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

Поскольку требования всех трех окружений совершенно различны, Rails хранит данные для каждого окружения в отдельной БД. Поэтому в любой момент у вас есть:

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

Посмотрим, как сконфигурировать БД для каждого из окружений.

Конфигурирование БД

Сконфигурировать БД для Rails-приложения до ужаса просто – вся критичная информация содержится всего в одном файле. Сейчас мы с этим разберемся, а затем создадим несколько БД для использования нашим приложением.

Конфигурационный файл БД

Разделение окружений отражено в конфигурационном файле базы данных Rails database.yml. Пример такого файла был приведен выше. Посмотрите на него! Он расположен в подкаталоге config нашего приложения Shovell.

Если удалить комментарии, файл будет выглядеть так (в зависимости от конфигурации MySQL [15] в качестве адреса может потребоваться 127.0.0.1, а не localhost [16]):

Example 4.1. 01-database.yml 

development: 
 adapter: mysql 
 database: shovell_development 
 username: root 
 password: 
 host: localhost 
test: 
 adapter: mysql 
 database: shovell_test 
 username: root 
 password: 
 host: localhost 
production: 
 adapter: mysql 
 database: shovell_production 
 username: root 
 password: 
 host: localhost

Этот файл содержит необходимый минимум информации для подключения к серверу БД каждого из окружений. Используя настройки MySQL (который мы устанавливали в главе 2) по умолчанию, мы можем уверенно продолжить разработку, используя пользователя root с пустым паролем – вся разработка будет проводиться на локальной машине, так что нет причин беспокоиться о том, что кто-то получит доступ к нашему суперсекретному приложению Shovell.

Параметр database задает имена БД, используемых в каждом из окружений. Как предлагает конфигурационный файл, Rails может параллельно поддерживать несколько БД. Заметьте, что мы сейчас говорим о разных БД, а не о разных таблицах – каждая БД может содержать произвольное количество таблиц (см. рисунок 4.1).


Рисунок 4.1.

Однако в нашей текущей конфигурации пропущен один жизненно важный аспект: баз данных, на которые ссылается конфигурационный файл, пока что не существует! Давайте создадим их.

Создать эти БД можно, используя один из многочисленных графических интерфейсов для MySQL, а можно из командной строки. Поскольку команды очень просты, давайте пока что создадим БД из командной строки; к графическим клиентам мы еще вернемся позже в этой главе.

Создание баз данных

Чтобы запустить интерфейс командной строки MySQL, введите в командной строке mysql -u root (на Mac-ах вместо mysql используется команда mysql5 – пользователи Mac любят быть не такими, как все).

$ mysql -u root 
mysql>

Команда создания новой БД довольно проста: create database newdatabasename.

Мы используем ее для создания трех БД – по одной для каждого из окружений – как показано ниже.

CREATE DATABASE shovell_development; 
CREATE DATABASE shovell_test; 
CREATE DATABASE shovell_production;
ПРИМЕЧАНИЕ

Безопасность БД и пользователь root

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

Конфигурация MySQL по умолчанию такова, что подключение к серверу БД возможно только с той же самой машины. Это значит, что никто – сидящий рядом или работающий на другом краю света – не сможет внести хаос в вашу среду Rails-разработки.

Командная строка MySQL и система разграничения прав – сложные и мощные средства, а безопасность БД – это тема, определенно лежащая за пределами этой книги.

Конечно, это не та конфигурация, которую я рекомендовал бы для среды исполнения, но к этому мы еще вернемся в главе 12, Deployment and Production Use. Если вы заинтересованы в безопасности БД при разработке, руководство по MySQL содержит некоторые инструкции [17], способные вам помочь.


Рисунок 4.2. Создание БД для каждого окружения.

Теперь, когда БД созданы, их можно использовать для хранения данных нашего приложения.

По умолчанию все Rails-приложения используют окружение для разработки, если не указано другое. Поэтому любая Rails-команда, исполняемая из командной строки, будет, на данный момент, воздействовать только на данные в БД этого окружения. В главе 12, Deployment and Production Use, будет рассказано, как переключиться в среду исполнения.

Архитектура Model-view-controller

Архитектура model-view-controller (MVC), которой мы уже касались в главе 1, не уникальна для Rails. На самом деле она возникла задолго до Rails и языка Ruby. Однако Rails реально поднимает идею разделения данных, пользовательского интерфейса и управляющей логики на новый уровень.

Давайте посмотрим на концепции, лежащие за созданием приложений, использующих архитектуру MVC. Разобравшись с теорией, мы посмотрим, как она воплощается в Rails-коде.

MVC в теории

MVC – это архитектурный паттерн программных приложений. Он разделяет приложение на следующие три компонента:

Такое разделение приводит к следующему порядку обработки пользовательских запросов:

  1. Клиентский браузер запрашивает страницу у контроллера на сервере.
  2. Чтобы ответить на запрос, контроллер выбирает нужные данные из модели.
  3. Контроллер создает страницу и отправляет ее представлению.
  4. Представление отправляет страницу обратно клиенту для отображения в браузере.

Этот процесс показан на рисунке 4.3.


Рисунок 4.3. Обработка запроса в архитектуре MVC.

Разделение приложения на такие три компонента – хорошая идея по многим причинам, в частности:

Если вам так и не удалось пока уложить в голове концепцию MVC, не переживайте. Пока важно только помнить, что Ruby-приложение разделено на три компонента.

MVC в Rails

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


Рисунок 4.4. Подкаталог app.

Здесь в игру вступает структура каталогов, которую мы создали в главе 2. Пришла пора рассмотреть ее поподробнее. Если вы заглянете в каталог app, изображенный на рисунке 4.4, то увидите несколько папок с именами, которые могут показаться вам знакомыми.

Как видите, каждому компоненту архитектуры MVC в каталоге apps отведено свое место – подкаталоги models, views и controllers, соответственно (о каталоге helpers будет рассказано в главе 6).

Это разделение продолжается в коде самого фреймворка. Классы, формирующие основную функциональность Rails, находятся в следующих модулях:

Посмотрим на эти компоненты повнимательнее.

ActiveRecord (модель)

ActiveRecord служит для выполнения всех задач приложения, связанных с БД, включая:

Соединение с сервером БД.

Выборку данных из таблиц.

Сохранение новых данных в БД.

У него есть несколько занятных трюков в рукаве. Посмотрим на некоторые из них.

БД-абстракция

ActiveRecord поставляется с большим набором адаптеров для подключения ко множеству популярных СУБД, например, MySQL, PostgreSQL, Oracle и Microsoft SQL Server.

Модуль ActiveRecord основан на концепции БД-абстракции. Как говорилось в главе 1, абстракция БД – это такой способ написания приложения, при котором оно не зависит ни от какой СУБД. Код, специфичный для конкретного сервера, надежно спрятан в ActiveRecord, откуда и вызывается при нужде. В результате Rails-приложение не привязано ни к какой конкретной СУБД. Если понадобится поменять СУБД, никаких изменений в коде приложения делать не придется.

Примеры кода, очень различающегося у разных вендоров, но абстрагируемого ActiveRecord, включают:

Однако прежде чем показать в действии магию ActiveRecord, придется немножко заняться хозяйством.

Таблицы БД

Мы уже создали по БД для каждого из окружений (разработки, тестирования и исполнения), но в них пока что нет таблиц. Таблицы в БД – это контейнеры, в которых структурировано хранятся данные. Они состоят из строк и колонок. Строка отображается на индивидуальные объекты, а колонка – на атрибуты этих объектов. Коллекция всех таблиц БД и отношений между ними называется схемой БД. Пример таблицы показан на рисунке 4.5.


Рисунок 4.5.

В Rails именование классов и таблиц БД следует интуитивному паттерну: если есть таблица с названием stories, состоящая из 5 строк, значит, в ней хранятся пять объектов Story. В отображении классов на таблицы хорошо то, что вам не нужно писать для этого никакого кода – все просто случается само, поскольку ActiveRecord выводит имя таблицы из имени класса. Заметьте, что имя нашего класса в Ruby стоит в единственном числе (Story), а имя таблицы – во множественном (stories).

Если задуматься, такая связь имеет смысл: когда мы ссылаемся на объект Story в Ruby, мы работаем с одним рассказом. Но таблица MySQL содержит множество рассказов, так что ее имя должно иметь множественное число. Можно обойти эти соглашения (а при работе со старыми СУБД иногда необходимо), но гораздо проще смириться.

Тесная связь между таблицами и объектами означает еще большее: если наша таблица stories имеет колонку link, как показано на рисунке 4.5, данные из этой колонки будут автоматически отображены на атрибут link объекта Story. А добавление новой колонки в таблицу приведет к тому, что атрибут с таким именем появится у всех соответствующих таблице объектов.

Итак, давайте создадим таблицы для хранения рассказов.

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

USE shovell_development; 
CREATE TABLE `stories` ( 
 `id` int(11) NOT NULL auto_increment, 
 `name` varchar(255) default NULL, 
 `link` varchar(255) default NULL, 
 PRIMARY KEY (`id`) 
);

Не пытайтесь запомнить эти SQL-команды для дальнейшего использования. Лучше дочитайте до главы 5, Models, Views, and Controllers, где будем показано нечто, называемое перемещениями (migrations), то есть специальные классы Ruby, которые можно написать, чтобы создавать таблицы БД, не используя никакого SQL вообще.

Использование консоли Rails

Теперь, когда таблица stories на месте, закройте консоль MySQL и откройте консоль Rails. Консоль Rails очень похожа на интерактивную консоль Ruby (irb), но имеет одно коренное отличие: из консоли Rails можно обращаться ко всем переменным среды и классам, доступным приложению во время исполнения. Из стандартной консоли irb они недоступны.

Чтобы открыть консоль Rails, перейдите в каталог shovell и введите команду ruby script/console, как показано ниже. Появится приглашение командной строки >>.

$ cd shovell 
$ ruby script/console 
Loading development environment. 
>>

Сохранение объекта

Чтобы начать использовать ActiveRecord, просто определите класс-наследник класса ActiveRecord::Base (в 3 главе упоминался оператор ::, мы использовали его, чтобы ссылаться на константы. Его можно также использовать, чтобы ссылаться на классы, имеющиеся в модуле, что здесь и делается).

Рассмотрим следующий код:

class Story < ActiveRecord::Base 
end

Эти две строки кода определяют кажущийся пустым класс Story. Однако этот класс совсем не пустой, как вы сейчас увидите.

Создайте из консоли Rails класс Story и экземпляр этого класса, story, с помощью следующих команд:

>> class Story < ActiveRecord::Base; end 
=> nil 
>> story = Story.new 
=> #<Story:0x2642900 @attributes={"link"=>nil, "name"=>nil}, 
@new_record=true> 
>> story.class 
=> Story

Как видите, синтаксис создание нового объекта ActiveRecord идентичен синтаксису создания обычных Ruby-объектов. Сейчас мы создали новый объект Story. Но этот объект существует только в памяти – мы еще не сохранили его в БД.

О том, что объект еще не сохранен, можно узнать, проверив значение атрибута new_record, используя свойство:

>> story.new_record? 
=> true

Поскольку объект не сохранен, при выходе из консоли Rails он будет утрачен. Чтобы сохранить его в БД, нужно вызвать у объекта метод Save.

>> story.save 
=> true

Теперь, когда объект сохранен (возвращенное значение true показывает, что все прошло хорошо), наш рассказ больше не является новой записью. У него даже есть уникальный ID, как показано ниже:

>> story.new_record? 
=> false 
>> story.id 
=> 1

Определение отношений между объектами

Кроме уже показанной базовой функциональности, ActiveRecord делает процесс определения отношений (или ассоциаций) между объектами максимально простым. Конечно, некоторые СУБД позволяют полностью определить такие отношения в схеме БД. Однако, чтобы выявить все возможности ActiveRecord, посмотрим, как он определяет такие отношения в Rails.

Отношения объектов можно определить разными способами; основное различие между этими отношениями состоит в количестве записей, указанных в отношении. Главные виды ассоциаций в БД, это:

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

Предположим, что в нашем приложении имеются следующие ассоциации:

У автора может быть один блог:

     class Author < ActiveRecord::Base 
       has_one :weblog 
     end

Автор может опубликовать много рассказов:

     class Author < ActiveRecord::Base 
       has_many :stories 
     end

Рассказы принадлежат автору:

     class Story < ActiveRecord::Base 
       belongs_to :author 
     end

История принадлежит ко многим темам.

     class Story < ActiveRecord::Base 
       has_and_belongs_to_many :topics 
     end 
     class Topic < ActiveRecord::Base 
       has_and_belongs_to_many :stories 
     end

Вам, без сомнения, надоело печатать в консоли определения классов только для того, чтобы они исчезли при закрытии консоли. Поэтому мы больше ничего не будем делать с отношениями объектов – подробнее к модулю ActiveRecord мы вернемся в главе 5.

Модуль ActionPack

ActionPack – это название библиотеки, которая содержит части MVC-архитектуры, соответствующие контроллеру и представлению. В отличие от модуля ActiveRecord, эти модули имеют чуть более интуитивно понятные названия ActionController и ActionView.

В исследовании логики приложения и презентационной логики из командной строки смысла немного (в конце концов, представления и контроллеры должны взаимодействовать с браузером!). Поэтому я приведу здесь краткий обзор компонентов ActionPack, а практикой мы займемся в главе 5.

ActionController (контроллер)

Контроллер управляет прикладной логикой программы, действуя как связующее звено между данными, презентационным слоем и Web-бразером. При этом контроллер выполняет ряд задач, включая:

Из приведенного выше рисунка 4.3 вы, возможно, не поняли, что в Rails-приложении может быть много разных контроллеров. А ведь может! Каждый контроллер отвечает за свою часть приложения.

Для приложения Shovell мы создадим:

Оба контроллера будут наследниками класса ActionController::Base, но с разной функциональностью, реализованной в виде методов экземпляров. На самом деле будет еще и промежуточный класс между этим классом и классом ActionController::Base; однако это не меняет того факта, что ActionController::Base является базовым колассом, от которого наследуются все контроллеры. Вот пример определения класса StoryController:

class StoryController < ActionController::Base 
 def index 
 end 
 def show 
 end 
end

Это простое определение создает класс StoryController с двумя пустыми методами - index и show.

Именование классов и файлов

Вы могли заметить, что имена классов и файлов следуют разным соглашениям:

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

Каждый контроллер находится в отдельном Ruby-файле (с расширением .rb), лежащем в каталоге app/controllers. Только что определенный класс StoryController, например, будет лежать в файле app/controllers/story_controller.rb.

ActionView (представление)

Как уже говорилось, один из принципов MVC заключается в том, что представление должно содержать только презентационную логику. Это значит, что код в представлении должен выполнять только действия, относящиеся к выводу страниц приложения – этот код не должен содержать никакой сложной логики приложения, и не должен выбирать никаких данных из БД. В Rails представление обрабатывает все, что отправляется браузеру.

Как можно догадаться, представления хранятся в каталоге app/views.

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

ERb похож на PHP или JSP [18] тем, что он позволяет раскидать серверный код по HTML-файлу, оборачивая этот код в специальные теги. Например, в PHP можно сделать что-нибудь такое:

<strong><?php echo 'Hello World from PHP!' ?></strong>

ERb-эквивалент будет выглядеть так:

<strong><%= 'Hello World from Ruby!' %></strong>

Есть две формы ERb-тегов: со знаком равенства и без него:

<%= ... %>

Эта пара тегов служит для обычного вывода. Результат Ruby-выражения, заключенного в эти теги, будет выведен в браузере.

<% ... %>

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

Примеры использования этих тегов приведены выше:

<%= 'This line is displayed in the browser' %> 
<% 'This line executes silently, without displaying any output' %>

В эти теги можно поместить любой Ruby-код – хоть простой, хоть сложный.

Создание экземпляра представления отличается от создания экземпляра модели или контроллера. Хотя ActionView::Base и является одним из базовых классов для представлений в Rails, создание экземпляра представления полностью обрабатывается модулем ActionView. Единственное, что Rails-разработчик должен изменить – это шаблон, то есть файл, содержащий презентационную логику приложения. Как вы можете предположить, такие шаблоны хранятся в каталоге app/views.

Как и большинство вещей в Rails, файлы шаблонов имеют свои соглашения по именованию и хранению:

Расширение файла шаблона зависит от типа этого шаблона. По умолчанию в Rails используется три типа шаблонов:

Это соглашение только выглядит сложно, на самом деле оно вполне интуитивно. Рассмотрим, например, класс StoryController, определенный нами ранее. Вызов метода read у этого контроллера по умолчанию приведет к попытке показать шаблон ActionView из каталога app/views/story. Если это стандартная HTML-страница (с некоторым ERb-кодом), то имя данного шаблона будет read.rhtml.

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

Связь между контроллерами и представлениями производится через поля, которые получают значения в результате действий контроллера. Давайте для иллюстрации этого развернем класс StoryController (пока не нужно ничего писать в консоли):

class StoryController < ActionController::Base 
 def index 
   @variable = 'Value being passed to a view' 
 end 
end

Как видите, в результате действий контроллера поле @variable получает строковое значение. Благодаря магии ActionView на эту переменную теперь можно ссылатся напрямую из соответствующего представления:

<p>The instance variable @variable contains: <%= @variable %></p>

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

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

Кодогенерация

Чтоб не приходилось писать весь код приложения с нуля, Rails позволяет относительно просто сгенерировать базовую структуру приложения. Так же, как мы создавали структуру каталогов приложения, можно создавать новые модели, контроллеры и представления, используя всего одну команду.

Для кодогенерации в Rails используется скрипт generate, который находится в каталоге script. Испытайте его: введите ruby generate без каких-либо параметров. Rails выведет список возможных параметров и перечислит доступные генераторы, как показано на рисунке 4.6.


Рисунок 4.6.

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

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

Другой пример – генерация базового Web-интерфейса для модели.

ActionMailer

Электронная почта не является непосредственной частью Web, однако составляет значительную часть онлайн-активности, так что встроенная в Rails поддержка электронной почты заслуживает упоминания. Web-приложения часто используют электронную почту для задач наподобие отправки подтверждений пользователям или замены паролей.

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

Хотя создание сообщений и обработка входящей почты – сложные задачи, ActionMailer прячет эту сложность и выполняет работу за вас. Это значит, что создание исходящей почты, благодаря применению шаблонов и очень небольшого количества Ruby-кода, сводится к указанию темы, тела сообщения и получателей. Аналогично, ActionMailer обрабатывает входящую почту, предоставляя Ruby-объект, инкапсулирующий сообщение легким для доступа способом.

Использование почтовой функциональности выходит за рамки этой статьи. Подробнее об этом можно узнать в Ruby on Rails wiki.

Глава 5. Модели, представления и контроллеры

В главе 4 мы говорили о принципах, лежащих в основе паттерна MVP, и разобрались, как каждый из компонентов реализован во фреймворке Rails. В этой главе мы используем эти знания, применяя кодогенерацию Ruby для создания компонентов приложения Shovell.

Генерирование модели

Поскольку наше приложение служит для публикации ссылок на рассказы в Web, Story – это фундаментальный объект, вокруг которого и будет строиться наше приложение. Здесь мы используем генератор Rails-моделей для создания модели Story, а затем построим вокруг нее все остальное

Генератор моделей

Генератор моделей – это на самом деле запускаемый из командной строки скрипт, который мы уже встречали в главе 4: скрипт generate. Он находится в каталоге script, и делает генерирование модели Story очень простым.

Запуск скрипта generate

Скрипт generate запускается из командной строки с несколькими параметрами. Первый параметр – тип компонента, который должен быть сгенерирован. Мы создаем модель, так что значение передаваемого параметра – просто model. Посмотрим, что происходит при передаче скрипту этого параметра:

$ cd shovell$ ruby script/generate model

Результат показан на рисунке 5.1.


Рисунок 5.1.

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

Тем не менее, двинемся дальше, и исполним скрипт. Мы создадим модель Story, и рассмотрим каждый из сгенерированных файлов.

Из каталога shovel выполните следующее:

$ ruby script/generate model Story

Вывод этой команды точно перечислит все, что было сделано, см. рисунок 5.2.


Рисунок 5.2.

Разбираемся с результатом

Прежде всего, generate пропустил три уже существующих каталога. Скрипт показывает, что он пропускает каталог, выводя слово exists с именем каталога. Пропущенные каталоги были сгенерированы, когда мы запускали команду rails в главе 2.

Далее, generate создал несколько файлов (на что указывает слово create, за которым следует имя созданного файла) и каталог. Рассмотрим их по порядку:

story.rb – этот файл содержит определение класса для модели Story. Найдите файл (он лежит в каталоге app/models) и посмотрите на его содержание в текстовом редакторе – определение совершенно идентично тому, что мы вводили в главе 4:

class Story < ActiveRecord::Base
end

ОК, возможность сгенерировать пару строк кода – это не ахти какой прорыв. Однако не торопитесь!

story_test.rb – этот файл значительно интереснее; это автоматически сгенерированный юнит-тест для нашей модели. Подробно мы будем разбирать его в главе 6, но вкратце, создав содержимое этого файла, мы можем гарантировать, что весь код нашей модели покрывается юнит-тестом. Как уже говорилось в главе 1, если у нас есть все юнит-тесты, мы можем автоматизировать процесс проверки того, что наш код ведет себя так, как предполагалось.

stories.yml – для помощи в юнит-тестировании генерируется файл stories.yml. Этот файл - fixture. Fixture – это файл, который содержит примеры данных для целей юнит-тестирования — при запуске набора тестов Rails очистит БД и заполнит таблицы данными из таких файлов. Таким образом, fixture-файлы позволяют обеспечить исполнение всех прогонов юнит-тестов приложения на одной и той же основе.

По умолчанию файл stories.yml вставит две строки в таблицу stories. Расширение .yml показывает, что это YAML-файл. Что это значит, будет сказано в следующем разделе.

001_create_stories.rb - это так называемый migration-файл. Чуть позже мы займемся этим вопросом.

Что такое YAML?

YAML (иронический рекурсивный акроним YAML Ain’t Markup Language, YAML – не язык разметки) – это легковесный формат представления данных. YAML-файлы имеют расширение .yml. Поскольку они не используют сбивающих с толку тегов, их гораздо проще читать людям, при этом они не менее эффективно читаются и компьютерами.

Rails широко использует YAML-файлы для указания fixtures. Выше уже приводилось несколько примеров YAML-файлов: один из них – файл database.yml, который мы использовали для настройки подключения к БД, другой – файл stories.yml, который мы только что создали с помощью скрипта generate.

Разберем файл stories.yml – откройте его в текстовом редакторе:

first:
  id: 1
another:
  id: 2

YAML-файл представляет две отдельных записи (first и another). Каждая запись содержит одно поле данных: колонку id. Поле id соответствует колонке id в таблице stories.

Давайте развернем эти записи, добавив поля name и link. Исправьте файл, чтобы он выглядел так:

first:
 id: 1
 name: My shiny weblog
 link: http://poocs.net/
another:
 id: 2
 name: SitePoint Forums
 link: http://www.sitepoint.com/forums/

Как видите, каждая запись в YAML-файле начинается с уникального имени без отбивки. Это не имя записи или какого-либо поля в БД; оно просто используется для идентификации записи в файле (это также используется в тестировании). В файле stories.yml такими идентификаторами являются first и another.

После уникального имени мы видим ряд пар ключ/значение, каждая из которых отбита на один или несколько пробелов (мы будем использовать два пробела, в соответствии с упоминавшимися соглашениями по форматированию Ruby-кода). В каждом случае ключ отделен от значения двоеточием.

Теперь посмотрим на последний из сгенерированных файлов – migration-файл. Если ваш опыт работы с БД ограничен использованием SQL, следующая глава будет для вас истинным прорывом, готовьтесь!

Изменение схемы с использованием migrations

Как уже говорилось, последний из четырех файлов, созданных скриптом generate – 001_create_stories.rb – это migration-файл. Migration-файл – это особый файл, предназначенный для изменения схемы БД (migration здесь – это каждое прописанное в файле изменение схемы).

Migrations могут быть удобным способом внесения изменений в БД по мере развития вашего приложения. Они не просто предоставляют возможности итеративного изменения схемы БД, они позволяют использовать для этого Ruby-код вместо SQL. Как вы могли понять к этому моменту, я не большой любитель написания SQL, а migrations – отличный способ избежать этого.

Migration-файлы нумеруются так, что они могут исполняться последовательно. В нашем случае файл, создающий stories – это первый migration-файл, поэтому в его имени присутствует номер 001.

Как и SQL-скрипты, migrations могут основываться один на другом, что усиливает потребность в исполнении этих файлов по порядку. Последовательное исполнение устраняет возможность ситуаций, когда, например, производится попытка добавить колонку к еще не созданной таблице.

Рассмотрим сгенерированный для нас migration-файл.

Создание скелетного migration-файла.

Откройте файл 001_create_stories.rb в текстовом редакторе (он находится в db/migrate). Он должен выглядеть так:

class CreateStories < 
  ActiveRecord::Migration
  
  def self.up
    create_table :stories do |t|
      # t.column :name, :string
    end
  end
  
  def self.down
    drop_table :stories
  end
end

Как видите, migration-файл состоит из определений классов, наследуемых от класса ActiveRecord::Migration. Классу, определенному в migration-файле, присваивает имя скрипт generate, основываясь на параметрах, передаваемых ему. В данном случае migration получает имя CreateStories, что является довольно точным описанием выполняемой задачи – мы генерируем новую модель (Story), и код в migration-файле создает таблицу stories.

Класс содержит два метода:

Эти методы взаимодополняющи – задача, выполняемая методом down в migration-файле, прямо противоположна выполняемой методом up.

На наше счастье, метод down вполне завершен и не требует доработки напильником. Его назначение – отменить изменения, внесенные методом up; все, что от него требуется – удалить таблицу из БД, и именно это он и делает.

С другой стороны, от метода up требуется некоторая работа. На данный момент он не очень трудолюбив – он создает таблицу, но полей в этой таблице нет (пока). Кроме того, он содержит блок, но единственная строка кода в этом блоке закомментирована! Код в этом комментарии подсказывает, что нужно добавить – Rails создает за нас скелет, и позволяет уточнить детали самостоятельно. Займемся же этим.

Конопатим щели

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

Кроме создания совершенно новых таблиц, migrations можно использовать для изменения уже существующих. Если вы завтра решите, что таблица stories должна хранить время и дату добавления каждого рассказа, вам придется потратить немало усилий на пересоздание всей таблицы ради добавления одной колонки. Конечно, старый добрый SQL можно эффективно использовать для выполнения этой задачи, но, чтобы его использовать, вам придется выучить еще одну неудобную SQL-команду. С помощью migrations вы можете добавить колонку к таблице, не теряя данных, содержащихся в таблице

Использовать migrations для изменения таблицы stories мы будем в главе 6. На данный момент метод up будет выглядеть следующим образом:

def self.up
  create_table :stories, :force => true do |t|
    t.column :name, :string
    t.column :link, :string
  end
end

:force => true в начале блока обычно не нужно – здесь мы включили его, учитывая тот факт, что мы создали таблицу для этой модели в главе 4, используя SQL. Без этого вызов create_table будет неудачным, поскольку таблица уже существует.

Этот код создаст таблицу stories с тремя колонками:

Такой подход отражает “чистый” Rails-метод создания и изменения таблиц БД, о котором мы говорили в главе 4.

Теперь, имея законченный migration-файл с методами создания и удаления схемы, мы должны выполнить собственно migration. Для этого используется утилита rake.

Используем rake для переноса данных

Если вы когда-либо писали или компилировали программу на С, вы уже пересекались с утилитой make. Для Ruby-кода rake – это то же самое, что make для С-кода. Пакет rake, который написал Jim Weirich, позаимствовал соглашения по именованию и основную функциональность у make, но на этом их сходство заканчивается.

Задания, которые должен выполнить rake, можно определить в файле Rakefile, написанном на Ruby. На самом деле Rails использует собственный Rakefile, чтобы снабдить Вас множеством удобных задач (загляните в папку shovel – вы найдете их там). Посмотреть список задач, доступных Rails-приложению, можно также, введя из корневого каталога приложения rake –tasks. Некоторые из задач перечислены на рисунке 5.3.

Rake-задачи могут принимать в качестве параметра пространство имен, namespace, концептуальный контейнер, позволяющий группировать связанные задачи.

Примером этого может быть пространство имен db, группирующее все задачи, связанные с БД. Пространства имен отделяются от списка задач двоеточием, то есть перед задачами из пространства имен db используется префикс db.

Вот задачи, которые вы можете использовать в повседневной работе с Rails:

Как показывает последний из примеров, не каждая задача принадлежит к пространству имен – некоторые бродят сами по себе.

Как было показано на рисунке 5.3, Rakefile для нашего приложения содержит предопределенные задачи, каждая из которых предлагает уникальную функциональность. Пока нас интересует только задача db:migrate (то есть задача migrate из пространства имен db). Позже мы рассмотрим и другие rake-задачи.

У команды rake имеется ряд параметров (чтобы увидеть их список, введите rake --help). В простейшем случае она имеет следующий формат:

$ rake namespace:task

Например, чтобы применить migrations из созданного выше migration-файла, нужно ввести:

$ rake db:migrate

При исполнении без каких-либо дополнительных аргументов эта команда делает следующее:

  1. Она смотрит в БД уникальный номер последнего примененного migration.
  2. Она выполняет по очереди еще не примененные migrations.
  3. Для каждого migration она исполняет метод up этого migration-класса, чтобы привести БД в соответствие со структурой, описанной в migration-файлах.

Двинемся дальше и исполним migrations из каталога shovel. Результат показан на рисунке 5.4.


Рисунок 5.4.

Как показано на рисунке, выполнение этой задачи привело к применению созданного нами migration CreateStories к нашей БД. Если все прошло успешно, в БД shovell_development должна (еще раз) появиться таблица stories – если хотите, проверьте с помощью MySQL Query Browser.

Откат тоже несложен!

Вместе с нашей БД развиваются и представляющие ее migration-файлы.

Откат к предыдущей версии схемы благодаря migrations очень прост. Для этого нужно ввести следующее (n – номер версии, до которой нужно откатиться):

$ rake db:migrate -VERSION=n

Например, следующая команда отменит создание таблицы stories и вернет нас к пустой БД, с которой мы начинали:

$ rake db:migrate -VERSION=0

Управление данными с помощью консоли Rails

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

Верно – на помощь опять приходит консоль Rails.

Создание записей

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

$ ruby script/consoleLoading development environment.
>> s = Story.new
  => #<Story:0x285718c @new_record=true,@attributes={"name"=>nil, "link"=>nil}
>>> s.name = 'My shiny weblog' => "My shiny weblog"
>> s.link = 'http://poocs.net/'=> "http://poocs.net/"
>> s.save                      => true

Посмотрим, что здесь происходит.

После загрузки консоли Rails мы создаем новый объект Story. Мы присваиваем этот объект переменной s (s – от Story, я знаю, это название не возьмет призов за креативность). Затем мы присваиваем значения каждой из колонок, существующих в объекте Story. Наконец, мы вызываем метод save, и объект Story сохраняется в БД.

Как убедиться, что данные успешно сохранены? Можно посмотреть на данные с помощью верного MySQL Query Browser, но мы пытаемся держаться подальше от SQL. Мы можем проверить корректность сохранения рассказа, проверив его id (уникальный идентификатор, автоматически генерируемый БД при сохранении объекта). Это можно сделать из консоли Rails:

>> s.id=> 1

Id нашего объекта – не nil, так что мы знаем, что он был успешно сохранен. Конечно, есть и другой путь убедиться, что данные успешно сохранены – использовать метод new_record?, который вы можете помнить по главе 4.

>> s.new_record?=> false

Ура! Поскольку этот метод возвращает false, мы знаем наверняка, что объект был записан в БД. Если вы все еще не убеждены, есть еще один способ проверки: метод count класса Story. Этот метод позволяет запросить у БД количество хранящихся в ней рассказов:

>> Story.count=> 1

Что ж, в этом есть смысл. Создадим еще один объект Story, на этот раз используя более краткую технику:

>> Story.create(:name => 'SitePoint Forums',:link => 'http://www.sitepoint.com/forums/')
=> #<Story:0x279d474 @new_record=false, @errors=#<ActiveRecord::Errors:0x279c72c @errors={}, @base=#<Story:0x279d474 
…
>>, @attributes={"name"=>"SitePoint Forums", "id"=>2, "link"=>"http://www.sitepoint.com/forums/"}>

Метод create делает то же, что и приведенный выше длинный способ, но использует только одну строку (не учитывая переносов). Этот метод также (причем очень удобно) сохраняет запись в БД при создании объекта. Кроме того, он позволяет присвоить значения колонкам записи (в данном случае колонкам name и link) в момент создания записи.

Выборка записей

Это здорово, что можно создавать и сохранять новую информацию, но что в этом хорошего, если мы не можем получить эту информацию? Один подходов к получению объектов может состоять в том, чтобы каким-то образом узнать (угадать) его идентификатор (id). В принципе id получается путем приращения, таким образом, мы можем предположить, что номер следующей записи (по отношению к некоторой записи) будет на единицу больше. Это позволяет использовать метод find, чтобы получить запись по ее id:

>> Story.find(2)
=> #<Story:0x2796ca0 @attributes={"name"=>"SitePoint Forums", 
                     id"=>"2", "link"=>"http://www.sitepoint.com/forums/"}>

Этот подход хорош для тестирования, но после удаления некоторых записей он работать не будет.

Еще один подход – выбрать все строки таблицы. Это можно сделать, передав :all методу find в качестве аргумента:

>> Story.find(:all)
=> [#<Story:0x2788f9c @attributes={"name"=>"My shiny weblog", 
  "id"=>"1", "link"=>"http://poocs.net/"}>, #<Story:0x2788e70 
  @attributes={"name"=>"SitePoint Forums", "id"=>"2", 
  "link"=>"http://www.sitepoint.com/forums/"}>]

Это вернет объект класса Array, содержащий все строки таблицы stories.

У массивов есть также методы first и last для выборки (сюрприз!) первого и последнего элемента массива:

>> Story.find(:all).last
  => #<Story:0x277f80c @attributes={"name"=>"SitePoint Forums", 
            "id"=>"2", "link"=>"http://www.sitepoint.com/forums/"}>

Использование аргумента :all и методов first и last дает нам некоторую гибкость, но этот подход не назовешь ресурсосберегающим – особенно при работе с большими объемами данных. Как и следует из его имени, использование :all означает передачу всех записей из БД в память Ruby. Это может оказаться не самым эффективным решением – особенно если приложению нужна всего одна запись.

Лучшим вариантом будет возложить процесс выборки записей на саму БД. Для этого можно передать методу find два аргумента

Аргумент :order должен содержать немного SQL, который сообщает БД порядок сортировки записей. Чтобы получить, например, последний элемент, нужно присвоить :order значение id DESC, которое указывает, что записи нужно сортировать по колонке id в убывающем порядке.

>> Story.find(:first, :order => 'id DESC')
    => #<Story:0x2779100 @attributes={"name"=>"SitePoint Forums", 
               "id"=>"2", "link"=>"http://www.sitepoint.com/forums/"}>

Возвращенный объект идентичен тому, который мы получили с помощью :all в сочетании с функцией last, но такой подход требует гораздо меньше ресурсов.

Теперь, когда все методики выборки уже поработали на нас, нужно сказать, что любой подход, который восстанавливает object на основе его id, фундаментально некорректен. Он предполагает, что БД больше никто не использует, чего, конечно, не будет, когда наше приложение выйдет в свет!

Нам нужен более надежный метод выборки записей – такой, который выбирает объекты по критерию, отличному от id. Что, если выбирать Story по имени? Это просто:

>> Story.find_by_name('My shiny weblog')
  => #<Story:0x2773bd8 @attributes={"name"=>"My shiny weblog", 
  "id"=>"1", "link"=>"http://poocs.net/"}>

На самом деле можно сделать запрос и по колонке link или любой другой колонке таблицы stories. Rails автоматически создает методы dynamic finder, если дать имени колонки префикс find_by_. В данном случае у класса Story есть динамические методы find_by_name и find_by_link (find_by_id будет излишен, поскольку делает то же, что и простой find).

Изменение записей

Мы знаем, как добавлять записи в БД, но что происходит, когда некто отправляет нашему приложению Shovell рассказ, наполненный опечатками или (вздох!) фактическими ошибками? Нам нужна возможность изменять существующие рассказы, чтобы обеспечить целостность и качество информации в Shovell, а также сохранность незапятнанной репутации сайта.

Прежде чем мы сможем изменить объект, его надо получить. В этом примере мы выберем объект Story из БД по имени, но сгодится и любой из перечисленных выше способов:

>> s = Story.find_by_name('My shiny weblog') => #<Story:0x272965c 
...
>> s.name                                    => "My shiny weblog"
>> s.name = 'A weblog about Ruby on Rails'   => "A weblog about Ruby on Rails"

Как видите, задача изменения значения атрибута (в данном случае name) так же просто, как присвоить ему новое значение. Конечно, это еще не постоянное изменение – мы просто изменили атрибут объекта в памяти. Чтобы сохранить изменения в БД, нужно вызвать метод save, так же, как это делалось выше при создании нового объекта.

>> s.save => true

Существует сокращенная запись – update_attribute – позволяющая изменить атрибут и сохранить результат в БД одним махом:

>> s.update_attribute :name, 'A weblog about Ruby on Rails' => true

Удаление записей

Чтобы удалить запись из БД, просто вызовите метод destroy объекта ActiveRecord. Это немедленно удалит запись из БД:

>> s.destroy=> #<Story:0x272965c …>

Если попробовать использовать метод find для поиска удаленного или несуществующего объекта, Rails выдаст ошибку:

>> Story.find(1)=> ActiveRecord::RecordNotFound: Couldn't find Story with ID=1

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

Где SQL?

При создании, изменении и удалении записей мы практически не встречались с SQL.

Если вы хотите посмотреть на SQL-выражения, от создания которых спас вас Rails, загляните в лог-файлы, расположенные в каталоге log. Там вы найдете файлы, названные соответственно окружениям. Мы работали в окружении для разработки, так что откроем development.log. Содержимое моего лог-файла показано на рисунке 5.5.

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


Рисунок 5.5.

Генерируем контроллер

Теперь, когда модель готова, давайте создадим контроллер. Так же, как и модель, мы будем генерировать контроллер с помощью вызова скрипта generate из корневого каталога приложения.

Запуск скрипта generate

Запустите еще раз из командной строки скрипт generate, на этот раз с первым параметром controller:

$ ruby script/generate controller

Вывод этого скрипта показан на рисунке 5.7.

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

Попробуем. Введите:

$ ruby script/generate controller Story index

Результат работы скрипта показан на рисунке 5.6. Проанализируем его.


Рисунок 5.6.

Значения сообщений, выдаваемых генератором контроллеров, к данному моменту уже должны быть вам знакомы:

story_controller.rb

Этот файл хранит определение класса StoryController. Оно почти пустое, но в нем есть-таки определение метода index, - впрочем, тоже пустое.

Не волнуйтесь – скоро оно перестанет быть пустым.

Новое содержимое файла story_controller.rb будет выглядеть так:

class StoryController < ApplicationController
  def index
  end
end

Самые мудрые читатели заметят, что наш StoryController не унаследован класс ActionController::Base так, как можно ожидать. Класс ApplicationController, показанный здесь, – это класс, напрямую унаследованный от ActionController::Base. Если вам интересно, этот класс определен в файле application.rb, который находится в каталоге app/controllers. Результирующий StoryController имеет точно те же атрибуты и методы, как если бы он был прямым наследником ActionController::Base. Использование промежуточного класса дает возможность разместить переменные и части функциональности, общие для всех контроллеров.


Рисунок 5.7.

story_controller_test.rb – этот файл содержит функциональный тест нашего контроллера.

story_helper.rb – это пустой класс helper для контроллера (хелперы – это куски кода, которые можно использовать в приложении многократно).

index.rhtml – это шаблон, соответствующий действию index, которое мы передали в качестве параметра скрипту generate. На данный момент он единственный в каталоге app/views/story, но когда мы создадим другие, они будут сохранены рядом с index.rhtml и получат названия, соответствующие их действиям (например, действие read окажется в шаблоне read.rhtml).

Вооружась этими знаниями, мы наконец сможем вдохнуть жизнь в нашего некрупного Rails-монстра – в духе истинного Франкенштейна.

Следите за именами классов контроллеров!

Как вы заметили, класс контроллера, созданный скриптом generate, называется StoryController, хотя первым параметром, который мы указали в командной строке, был просто Story. Если ввести параметр StoryController, класс назывался бы StoryControllerController!

Запускаем приложение... заново!

Пора заново начать наше приложение.

Запустите Web-сервер командой:

$ ruby script/server

Когда сервер завершит свою стартовую последовательность, введите в адресной строке браузера адрес http://localhost:3000/story

Если все идет по плану, вы увидите страницу, похожую на приведенную на рисунке 5.8.


Рисунок 5.8.

Что здесь сказано? Эта простая (и не особо красивая) страница говорит, что:

  1. Маршрутизация между контроллерами и представлениями работает корректно – Rails нашел и создал экземпляр класса StoryController, основываясь на модели story, которую мы у него запросили.
  2. Действие index – это действие по умолчанию, вызываемое, когда в URL явно не указано никаких действий (все, что мы указали – это имя контроллера, с помощью пути /story). Если вспомнить, что большинство Web-серверов обычно загружает файл с именем index по умолчанию (index.html, index.php, index.jsp, index.aspx и т.д.), это кажется вполне нормальным поведением.
  3. Контроллер смог найти свои представления – HTML для страницы, которую мы видим в браузере, содержится в файле, упоминаемом на экране (app/views/story/index.rhtml).

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

Чтобы закончить картину, вытянем данные из модели в index.

Создание представления

Для создания представлений в Rails-приложениях можно использовать два подхода. Один состоит в использовании scaffolding; второй – “действовать в одиночку.”

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

После этого мы закатаем рукава и начнем с нуля строить наши представления.

Генерирование представлений с помощью scaffolding

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

Что же такое scaffolding?

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

Есть два пути использования scaffolding:

Временный scaffolding

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

Мы будем использовать этот тип scaffolding для взаимодействия с данными в приложении Shovell.

Постоянный scaffolding

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

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

Независимо от используемого подхода, нужно помнить, что scaffolding – это средство для быстрого взаимодействия с моделью, и его нужно использовать именно с этой целью. Его ни в коем случае не нужно рассматривать как полностью автоматизированное средство генерирования Web-приложений (или хотя бы интерфейсов для администрирования).

Вернемся к нашему примеру. Отредактируйте файл story_controller.rb из каталога app/controllers, чтобы он выглядел так:

class StoryController < ApplicationController
  scaffold :story
end

Эти изменения временно удалят наше последнее действие index, сослужившее нам службу в предыдущем разделе. Не волнуйтесь, мы вернем его на место позднее.

В данном случае команда scaffold :story дает Rails возможность снабдить StoryController функциональностью, необходимой для создания, обновления и удаления рассказов. Перезагрузите страницу http://localhost:3000/story. На этот раз вы увидите что-то похожее на рисунок 5.9.

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


Рисунок 5.9.

Как видите, scaffolding позволяет быстро начать работать с данными модели. Вы можете также воспользоваться этим интерфейсом для создания в БД записей-заглушек, для упрощения дальнейшей разработки.

Однако, как уже говорилось, у scaffolding есть свои ограничения. Например, оно не может справиться с ассоциациями ActiveRecord, показанными в Главе 4, например, “рассказ принадлежит пользователю”. Кроме того, поскольку большинству приложений нужен серьезный интерфейс администрирования, зачастую проще сделать нормальную вещь, чем возиться с интерфейсом-заглушкой.

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

Создание статических страниц

В главе 4 мы уже встречались с модулем ActionView, но коснулись только командной строки. Давайте создадим собственные представления, которые реально можно увидеть через браузер.

Напомню, в архитектуре MVC ActionView отвечает за представления. Файлы, используемые для отрисовки представлений, называются шаблонами, и обычно состоят из HTML с добавлением Ruby-кода. Эти файлы называют ERb-шаблонами.

Один из этих шаблонов (хотя и убогий) уже создан – это файл index.rhtml, расположенный в app/views/story:

<h1>Story#index</h1>
<p>Find me in app/views/story/index.rhtml</p>

Выглядит знакомо, не так ли? Это HTML, который мы уже видели в браузере. Как видите, это статическая страница (в том смысле, что она не содержит Ruby-кода). Динамические страницы (страницы, вытягивающие данные из БД или другого источника) гораздо интереснее!

Создание динамических страниц

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

Откройте файл шаблона в текстовом редакторе и удалите все, что в нем содержится. Вместо удаленного содержимого введите:

<%= Time.now %>

Здесь мы вызываем метод now класса Time, который входит в стандартную библиотеку Ruby. Вызов метода обернут в ERb-теги (стартовый <%= и закрывающий %>).

Вы, возможно, помните, что знак равенства в открывающем ERb-теге заставляет вывести возвращаемое значение Time.now на страницу.

Поскольку мы изменили код контроллера для использования scaffolding, нам нужно откатить эти изменения, чтобы Rails опять отображало нормальное представление. Откройте файл app/controllers/story_controller.rb и измените его так:

class StoryController < ApplicationController
  def index
  end
end

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


Рисунок 5.10.

Передача данных туда и обратно

В том, что мы до сих пор делали, есть одна фундаментальная проблема. Вы заметили, в чем она состоит?

Чтобы придерживаться архитектуры MVC, мы избегаем выполнения любых объемистых вычислений в любом из представлений - это задача контроллера. Строго говоря, наш вызов Time.now – тоже такое вычисление, таким образом, оно должно производиться в пределах контроллера. Но что толку в результатах вычислений, если мы не можем их показать?

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

Как говорилось в главе 4, любое поле, объявленное в контроллере, автоматически становится доступной представлению как поле. Воспользуемся этим: отредактируйте /app/controllers/story_controller.rb, чтобы в нем содержалось:

class StoryController < ApplicationController
  def index
    @current_time = Time.now
  end
end

Теперь заместите содержимое app/views/story/index.rhtml следующим:

<%= @current_time %>

Я уверен, что вы можете предположить, что здесь происходит:

  1. Мы перенесли "вычисление" текущего времени из представления в контроллер.
  2. Результат вычисления сохраняется в поле @current_time.
  3. Значение этой переменной автоматически становится доступным представлению.

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

Вуаля! Логика приложения и логика представления четко разделены.

Вытягиваем данные модели

Все, что осталось – вытянуть данные из модели в представление, и тему MVC можно считать закрытой!

Если вы удалили из модели записи во время экспериментов со scaffolding, создайте хотя бы один рассказ, введя следующее из консоли Rails:

>> Story.create(
:name => 'SitePoint Forums',

:link => 'http://www.sitepoint.com/forums/')

Чтобы отобразить данные из модели в представлении, нужно выбрать их в контроллер:

class StoryController < ApplicationController
  def index
    @story = Story.find_by_name('SitePoint Forums')
  end
end

Соответственно нужно изменить и представление (index.rhtml):

A random link:
<a href="<%= @story.link %>"><%= @story.name %></a>

Перезагрузите страницу, чтобы увидеть результат (рисунок 5.11).


Рисунок 5.11.

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

<%= link_to @story_name, @story_link %>

Еще одно: я буду первым, кто признает, что текст на странице немного вводит в заблуждение. Наша ссылка не совсем случайна – она просто выбирает одно и то же из БД снова и снова.

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

@story = story.find(:first, :order => 'RAND()')

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

Вот так мы и положили начало нашего приложения. Возможно, показ случайной истории из БД – невеликое достижение, но это только начало!

Глава 6. Хелперы, формы и раскладки

В главе 5 мы заложили основу архитектуры нашего приложения – модель, представление и контроллер – и смогли показать ссылки на рассказы, хранящиеся в БД. Однако, несмотря на солидность этой основы, пользователи пока не могут работать с нашим приложением.

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

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

Позовем испытанных помощников

Нет, речь не о маленьких помощниках Санта-Клауса. Позвольте мне объяснить.

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

Из-за этого существует еще один структурный компонент – хелперы. Хелпер – это многократно используемый в приложении кусок кода, хранящийся в хелпер-файле. Хелпер обычно содержит относительно сложную или многократно используемую презентационную логику; поскольку все представления, использующие хелпер, не содержат этих сложностей, код представлений остается простым и легко читаемым, что отражает приверженность принципу DRY. В Rails встроено множество хелперов, но вы, конечно, можете создать свой собственный для использования в своих приложениях.

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

Как говорилось в главе 5, когда мы генерируем контроллер (используя скрипт generate, который мы уже успели узнать и полюбить), один из создаваемых файлов – это новый хелпер, controllername_helper.rb. В случае нашего StoryController хелпер-файлом, ассоциированным с этим контроллером, является story_helper.rb, живущий в app/helpers.

Хелперы, ассоциированные с конкретным контроллером, доступны только представлениям этого конкретного контроллера. Однако есть одна "особая" группа хелперов – они определены в файле app/helpers/application_helper.rb. Эти хелперы называются глобальными и доступны любому представлению в приложении.

Мы будем использовать некоторые встроенные хелперы Rails в интерфейсе публикации рассказов, создаваемом в этой главе.

Публикация рассказов

В нашем кратком набеге в мир scaffolding в Главе 5 мы видели, что в Rails можно создать быстрый (и грязный) фронт-энд для наших данных, хотя этот подход не обязательно даст наилучшие результаты.

В этом разделе мы создадим Web-интерфейс для публикации рассказов на сайте Shovell без использования scaffolding. Сперва мы создадим шаблон представления, содержащий реальную форму представления, а затем добавим в StoryController метод для сохранения присланных рассказов в БД. Мы также реализуем глобальную раскладку приложения и некоторый ответ, выводимый пользователям при заполнении формы и после отправки рассказа.

Создаем форму

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

хелпер form_for

В Rails есть несколько функций-хелперов для создания форм. form_for – это недавнее пополнение семейства, которое рекомендовано для использования при генерировании формы, привязанной к одному типу объекта. Под "привязанной" я имею в виду отображение каждого поля формы на соответствующий атрибут одного объекта. Использование хелпера form_for для привязки простой формы к объекту Story может выглядеть так:

<% form_for :story do |f| %>
 <%= f.text_field :name %>
 <%= f.text_field :link %>

<% end %>

В этом синтаксисе имеет смысл рассмотреть несколько пунктов:

В обмен на использование этого синтаксиса мы получаем следующие преимущества

Как видите, использование form_for и объекта FormBuilder – это способ создания форм с минимальными усилиями.

Создаем шаблон

Теперь, когда мы знаем, как использовать form_for, давайте использовать его для создания формы, которую посетители сайта будут использовать для размещения рассказов на Shovell.

Форма – это презентационная концепция, а это значит, что она должна лежать в представлении. Наша форма позволит пользователям отправлять новые рассказы, так что мы назовем ее new. Давайте сделаем шаблон для нее: создайте новый файл new.rhtml в каталоге app/views/story. Он должен содержать следующее:

<% form_for :story do |f| %>

<p>
 name:<br />
 <%= f.text_field :name %>

</p>

<p>
 link:<br />
 <%= f.text_field :link %>


</p>

<p>

 <%= submit_tag %>
</p>
<% end %>

Начнем разбираться с кодом со строки:

<% form_for :story do |f| %>

Как только что говорилось, хелпер form_for создает форму, привязанную к конкретному объекту – в данном случае она привязана к полю @story, как указывает наличие символа :story в начале блока.

<%= f.text_field :name %>

Эта строка создает текстовое поле name, отображаемое на объект @story. В это поле пользователь сможет ввести название публикуемого рассказа.

<%= f.text_field :link %>

Эта строка создает еще одно текстовое поле, link, также отображаемое на объект @story. В это поле пользователь сможет ввести ссылку на публикуемый рассказ.

<%= submit_tag %>

Это хелпер генерирует HTML-код для отображения на форме кнопки Submit. Это отдельный хелпер, а не часть хелпера form_for.

Теперь убедимся, что сервер WEBrick работает (если вы не помните, как запустить сервер, см. главу 2). Откройте Web-браузер и введите следующий URL: http://localhost:3000/story/new. Вы должны увидеть страницу, аналогичную приведенной на рисунке 6.1.


Рисунок 6.1.

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

Вы можете предположить, что мы должны включить метод new в StoryController перед тем, как эта страница успешно отобразится. Нет! Наличие шаблона new.rhtml в папке story – это все, что нужно Rails для вывода страницы, расположенной по адресу /story/new.

Конечно, наша форма на самом деле пока ничего не делает, но мы наконец-то можем посмотреть на нее, готовенькую, в браузере. Вместо того чтобы позволять представлению собирать информацию, вводимую пользователями, (в конце концов, это не ее дело, согласно MVC), давайте создадим "реальное" действие контроллера для этой формы.

Изменяем контроллер

Чтобы создать действие, работающее с формой, нужно изменить файл app/controllers/story_controller.rb, чтобы он выглядел так:

class StoryController < ApplicationController
 def index
 @story = Story.find(:first, :order => 'RAND()')
 end

def new
  @story = Story.new
end

end

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

Добавленный нами код метода new просто создает новый экземпляр объекта Story и сохраняет его в поле @story. Поскольку @story – это поле, оно будет доступно представлению. Это хорошее начало!

Анализируем HTML

Пришла пора посмотреть, что за HTML сгенерировали Rails-хелперы. Если посмотреть HTML этой страницы (с помощью опции браузера View Source), вы увидите следующее:

<form action="/story/new" method="post">

<p>
 name:<br />
 <input id="story_name" name="story[name]" size="30" 

type="text" />
</p>

<p>
 link:<br />
 <input id="story_link" name="story[link]" size="30" 


type="text" />
</p>

<p>

 <input name="commit" type="submit" value="Save changes" />
</p>
</form>

В общем, это то, что ожидалось: два текстовых поля кнопка Submit, и все это обернуто в элемент form. Интересно то, что целевой URL (атрибут action элемента form) указывает на саму форму: /story/new. Эта техника называется postback.

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

ОК, разметка выглядит прекрасно. Но если вы заполните форму в ее текущем состоянии, результаты вас особо не впечатлят – форма будет показана заново, но ничего не сохранится, введенные значения просто исчезнут!

Сохранение данных в БД

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

Для этого мы будем использовать HTTP-метод request для запроса страницы. Отображением формы занимается HTTP GET, а отправкой результатов – HTTP POST. Поскольку создаваемая форма предназначена для отправки информации, мы будем использовать POST.

Протокол HTTP определяет ряд методов связи между сервером и браузером. Наиболее распространенными являются GET и POST. GET используется для запроса информации с Web-сайта (например, страницы, требующей минимального количества информации от браузера). POST, наоборот, используется для отправки информации на сайт (например, содержимого формы).

Чтобы проверить, чем занят браузер пользователя – получением или отправкой информации, можно посмотреть на сырые HTTP-заголовки. Однако встроенный Rails-объект request предоставляет гораздо более простой способ определить, находимся ли мы в режиме POST.

Объект request можно использовать в любом действии контроллера. Он содержит каждый бит информации о запросе, поступившем из пользовательского браузера, включая тип HTTP-метода. Чтобы определить, относится ли пользовательский запрос к типу POST, проверяется метод request.post?. Если это в самом деле POST-запрос, нужно присвоить значения, присланные через форму, объекту @story.

Измените метод new в определении класса StoryController, чтобы он выглядел так:

def new

 @story = Story.new(params[:story])

 if request.post?

 @story.save
 end
end

Объект params в первой строке метода – это хеш-таблица, которая содержит весь контент, присланный пользователем.

Если посмотреть еще раз на HTML-код формы, можно заметить, что у всех элементов input есть префикс story[]. Поскольку каждое поле имеет такой префикс, данные формы, переданные Rails, будут добавлены в хеш-таблицу params.

Мы можем обратиться к этим данным, передавая хеш-таблице символ с тем же именем, что и основной объект, params[:story]. Затем мы можем ссылаться на индивидуальные элементы хеш-таблицы, передавая ему имя атрибута (также символ). Например, значение атрибута name можно получить через params[:story][:name]. Вы поняли идею.

Результатом всего этого будет то, что данные, присланные пользователем с помощью формы, можно легко присвоить объекту. Все, что нам нужно сделать – передать хеш-таблицу params[:story] методу Story.new, и мы получим заполненный объект @story.

Не случайно это точно то, что мы сделали в первой строке нашего метода:

@story = Story.new(params[:story])

Теперь, если ввести данные в форму и нажать Save changes, вы увидите другой результат: форма будет снова выведена, а значения сохранятся. Есть неплохой шанс, что данные сохранились и в БД, но, поскольку страница довольно неинформативна, точно мы этого не знаем.

Чтобы исправить эту ситуацию, давайте скажем форме переадресовывать пользователя к странице index сразу после сохранения объекта:

if request.post?
  @story.save
  redirect_to :action => 'index'
end

Строка, выполняющая переадресацию, выделена в коде – это пример сокращенной нотации Ruby. Вот развернутая версия того же вызова метода:

redirect_to({:action => 'index'});

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

Еще раз запустим форму. В этот раз вы будете перенаправлены на селектор случайных рассказов, который мы создали в главе 5, и расположенный в действии index. Уже лучше! Однако наше приложение выглядит довольно неуклюжим.

Создание раскладки

В Rails раскладка (layout) – это специализированная форма шаблона представления. Раскладки позволяют применять в разных представлениях по всему сайту общие элементы страницы. Примерами таких элементов могут быть HTML-заголовки, CSS-файлы и JavaScript.

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

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

Создаем структуру

Раскладки должны храниться в каталоге app/views/layouts. Шаблон раскладки можно назвать как угодно, при условии, что расширением будет .rhtml. Имя application.rhtml Rails рассматривает как раскладку по умолчанию.

Воспользуемся этим соглашением: создайте файл application.rhtml в каталоге app/views/layouts и внесите в него следующий код:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
    xml:lang="en" lang="en">
<head>
  <meta http-equiv="Content-type" 
      content="text/html; charset=utf-8" />
  <title>Shovell</title>
  <%= stylesheet_link_tag 'style' %>
</head>
<body>
  <div id="content">
  <h1>Shovell</h1>
  <%= yield %>
  </div>
</body>
</html>

Ничего особого здесь не происходит – мы создали обычный XHTML-документ, который содержит соответствующее DOCTYPE-объявление. Но пара ERb-вызовов заслуживает объяснения.

<%= stylesheet_link_tag 'style' %>

Этот код генерирует HTML, включающий все внешние таблицы стилей для страницы. Передавая строку 'style', мы гарантируем, что сгенерированный элемент <link> будет указывать на URL/stylesheets/style.css. Эту таблицу стилей мы вскоре создадим.

Rails поставляется с набором хелперов, аналогичных stylesheet_link_tag, и упрощающих генерирование HTML-страниц. В основном они избавляют вас от ручного ввода (и сопутствующих ему ошибок). В число таких хелперов входят image_tag и javascript_include_tag.

<%= yield %>

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

Вспомните, что наша раскладка будет использована множеством разных шаблонов представлений, каждое из которых отвечает за вывод результата отдельного действия. Когда раскладка получает команду yield, управление передается отрисовываемому в данное время шаблону представления – то есть раскладка уступает место (yields) шаблону представления. После отрисовки этого шаблона управление возвращается раскладке, и продолжается отрисовка остальной части страницы

Раз уж мы связались с таблицей стилей, давайте ее используем.

Добавляем стиль

Чтобы страница хорошо смотрелась, мы будем использовать CSS. Чтобы применить к приложению таблицу стилей, создайте файл style.css в каталоге public/stylesheets и поместите в него следующий код:

body 
{
  background-color: #666;
  margin: 15px 25px;
  font-family: Helvetica, Arial, sans-serif;
}
#content 
{
  background-color: #fff;
  border: 10px solid #ccc;
  padding: 10px 10px 20px 10px;
}


Рисунок 6.2.

Если CSS – не самая сильная ваша сторона, не переживайте – все, что нужно для этого проекта, это ввести (или просто скопировать) приведенные здесь стили. Если вы хотите улучшить свои навыки в CSS, начните с книги Rachel Andrew и Dan Shafer HTML Utopia: Designing Without Tables Using CSS [http://www.sitepoint.com/books/css2/]

Перезагрузите страницу в браузере, Вы увидите слегка похорошевшую версию формы, показанную на рисунке 6.2.

Замечательно! У нас есть корректно функционирующая, хорошо структурированная форма, которая еще и неплохо выглядит.

Однако наше приложение никак не оповещает пользователя об удаче или неудаче отправки рассказа.

Оповещение пользователей и Flash

Да, вы правильно прочли: flash.

Нет, мы не собираемся переключаться на Adobe Flash, чтобы обеспечить отклик приложения. “Flash” – это название внутреннего контейнера (на самом деле подобия хеш-таблицы), который Rails использует для хранения временных данных. В этом разделе мы будем использовать flash для передачи временных объектов между действиями. После этого мы применим некоторые проверки вводимых данных.

Добавление к Flash

Когда я говорю, что flash используется для хранения временных данных, я не имею в виду, что они существуют только в памяти и не сохраняются в БД. Содержимое flash существует только на протяжении одного действия, а затем исчезает.

Что в этом хорошего? Использование flash позволяет удобно передавать информацию между следующими одно за другим действиями без сохранения этой информации в браузере или БД. Flash хорошо подходит для хранения короткоживущих сообщений, например, оповещений, информирующих пользователя об удаче или неудаче при сохранении результатов или входе в систему.

Содержимое flash обычно заполняется из действия контроллера. Использовать flash очень просто: чтобы поместить сообщение во flash, просто передайте ему идентифицирующий символ и соответствующее сообщение. Вот пример:

 flash[:error] = 'Login unsuccessful.'

В нашем приложении мы хотим поместить во flash сообщение сразу после сохранения рассказа, чтобы пользователь знал об успехе или неудаче при отправке данных. Добавьте строку в действие new в файле story_controller.rb:

def new
  @story = Story.new(params[:story])
  if request.post?
    @story.save
  flash[:notice] = 'Story submission succeeded'

  redirect_to :action => 'index'
  end
end

Соглашения по flash

В общем, Rails-приложения используют названия flash-областей, названные по уровням из логов UNIX, указывающим уровень серьезности сообщения. Общепринятые названия областей – :notice, :warning и :error.

В данном случае сообщение не является критичным, и мы используем :notice. Однако выбор названия flash-области остается полностью за вами.

Получение данных из flash

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

Поскольку содержание flash применимо универсально, изменим layout-файл (расположенный в app/views/layouts/application.rhtml) так, чтобы он всегда выводил окно уведомления, за исключением случаев, когда никакого содержания нет. Измените layout-файл следующим образом:

<div id="content">
 <h1>Shovell</h1>

<% unless flash[:notice].blank? %>
<div id="notification"><%= flash[:notice] %></div>
<% end %>

 <%= yield %>
</div>

Добавленное нами условие проверяет, не пуста ли переменная flash[:notice]. Если она не пуста, код отрисовывает простой HTML-элемент div с присоединенным к нему id. Rails считает объект пустым, если он равен nil или пустой строке.

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

#notification 
{
  border: 5px solid #9c9;
  background-color: #cfc;
  padding: 5px;
  margin: 10px 0;

}

Если вы теперь отправите новый рассказ, на следующей странице вам покажут зеленую рамку, сообщающую об успехе мероприятия (рисунок 6.3).


Рисунок 6.3.

Однако, наш процесс отправки формы все еще незавершен – пользователь может отправить рассказ, не вводя название. Или ссылку. Или и то, и другое!

Вводим проверки

Чтобы убедиться перед сохранением, что все присылаемые рассказы содержат название и ссылку, мы будем использовать проверки (validations), функциональность, предоставляемую ActiveRecord.

Есть много разновидностей проверок: простейшая из них – это "проверить, что данный атрибут (или ввод формы) не пуст". Более сложной проверкой может быть, например, "убедиться, что данный атрибут соответствует следующему регулярному выражению". В промежутке существует множество уровней сложности. Более сложные проверки выполняются, например, при проверке email-адреса.

Проверки определяются в модели. Это гарантирует, что проверки применяются всегда, и что перед сохранением данных в БД объект всегда корректен.

Рассмотрим простую проверку. Чтобы добавить проверку к модели Story, отредактируйте класс model в app/models/story.rb, чтобы он выглядел так:

class Story < ActiveRecord::Base
  validates_presence_of :name, :link
end

Как видите, строка, которую мы добавили, довольно многословна и весьма удобочитаема. Эта строка удостоверяется, что name и link имеют значения прежде, чем сохранять модель.

Настройка логики перенаправления

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

  def new
    @story = Story.new(params[:story])
    if request.post? and @story.save
      flash[:notice] = 'Story submission succeeded'
      redirect_to :action => 'index'
    end
  end

Как несложно заметить, мы изменили выражение if, чтобы оно проверяло два условия: во-первых, мы проверяем, является ли запрос страницы POST-запросом. Если так, то мы проверяем, возвращает ли @story.save true.

Проверки будут вызваны перед тем, как метод save запишет объект в БД. Если хотя бы одна из проверок не пройдет, метод вернет false – объект не будет сохранен, а пользователь – перенаправлен.

Общепринято использовать Ruby-выражения с условиями, как мы и поступили в методе save. В общем, многие методы классов ядра Rails возвращают true или false, что делает их прекрасным выбором для использования в условиях.

Однако нам по-прежнему нужно дать пользователю какое-то наставление по исправлению ошибок в случае неудачи проверки.

Улучшаем впечатление пользователя

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

<div class="fieldWithErrors">
  <input id="story_name" name="story[name]" size="30" type="text" value="" />
</div>

Как видите, использование хелпера form_for окупилось – он обернул текстовое поле в элемент div и присвоил класс fieldWithErrors. Мы можем заключить этот div в красную рамку, например, чтобы указать, что это поле порождает ошибку. Давайте так и поступим.

.fieldWithErrors 
{
  border: 5px solid #f66;
}

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

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

Добавьте следующую строку первой в шаблон new.rhtml:

<%= error_messages_for 'story' %>


Рисунок 6.4.

Теперь, если пользователь отправит форму с незаполненными полями, он получит то, что показано на рисунке 6.4:

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

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

Тестируем форму

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

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

Для создания набора тестов можно использовать несколько подходов. Один из них – на самом деле, более радикальный – называется разработкой через тестирование (test-driven development, TDD). При следовании принципам TDD вы сперва пишете тест, убеждаетесь, что он не проходит, а затем уже создаете код, при котором тест проходит. Этот подход работает лучше, если у вас есть опыт в том языке программирования, на котором производится тестирование.

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

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

Юнит-тесты Юнит-тесты покрывают функциональность уровня модели, охватывающую основную бизнес-логику приложения. С помощью юнит-тестов можно тестировать проверки, ассоциации и методы модели.
Функциональные тесты Функциональные тесты в Rails покрывают функциональность уровня контроллера и сопутствующих ему представлений. Функциональный тест может быть весьма специфичен – например, проверить, что определенный HTML-элемент присутствует в представлении, что переменная правильно заполнена, или что после отправки данных формы выполняется правильное перенаправление.
Интеграционные тесты Интеграционное тестирование выходит за рамки относительно ограниченных подходов функционального и юнит-тестирования. Интеграционные тесты позволяют тестировать целые этапы взаимодействия пользователя с приложением. Хорошие кандидаты на интеграционное тестирование – регистрация нового пользователя или процесс публикации рассказа в целом.

В этой главе мы рассмотрим функциональное и юнит-тестирование, а интеграционным займемся в главе 11.

Вообще говоря, тестовые случаи в Rails существуют как классы-наследники Test::Unit::TestCase. Однако когда мы генерировали модели и контроллеры в Главе 5, скрипт generate создал для нас некоторые скелетные файлы. Они расположены в каталоге test, в котором находятся все файлы, составляющие наш набор тестов.

Тестируем модель

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

Анализируем скелетный файл

Скелетный файл теста для модели Story находится в test/unit/story_test.rb. Открыв его, вы увидите следующий код:

require File.dirname(__FILE__) + '/../test_helper'

class StoryTest < Test::Unit::TestCase
 fixtures :stories

 # Replace this with your real tests.
 def test_truth
 assert true
 end
end

За исключением первой строки мы здесь имеем определение класса StoryTest. Название этого класса, созданного при генерировании файла, предполагает, что его назначение – проверить модель Story – и так оно и есть. Строка fixtures :stories проверяет, что фиктивные данные нашего теста загружены в БД до начала теста (фиктивные данные, используемые для тестирования, рассматривались в Главе 5),

Команда require в первой строке – просто пример получения одним файлом доступа к функциональности другого; внешний файл в таких случаях называют include-файлом. Включив этот файл, мы получаем доступ к большому объему связанной с тестированием функциональности.

Конечно, Rails постоянно использует множество файлов, но мы не видим множества разбросанных по коду команд require. Почему? Соглашения Rails позволяют вычислить, что именно нужно, когда оно нужно, и где это взять. Это одна из причин, по которым следующие Rails-соглашения так важны.

Использование Assertion

Код в Rails тестируется с помощью инструкций assert. Assert – это маленькие функции, подтверждающие, что что-то находится в определенном состоянии. Простой assert может только сравнить два значения, чтобы удостовериться, что они идентичны. Более сложный assert может сопоставить значение с регулярным выражением, или проверить наличие в HTML-шаблоне определенного HTML-элемента. В этом разделе мы рассмотрим различные типы инструкций assert.

После написания assert группируются в тесты. Тест – это метод экземпляра с префиксом test_. Примером теста может быть метод test_truth из приведенного выше кода. Такие тесты исполняются один за другим с помощью рассматривавшейся в главе 5 команды rake. Если одно из утверждений теста не срабатывает, тест немедленно прерывается и набор тестов переходит к следующему тесту.

Теперь, зная, кто такие assert и как они работают, давайте напишем одно такое выражение.

Пишем юнит-тест

Метод test_truth в нашем юнит-тесте – просто заглушка, созданная скриптом generate. Давайте заменим ее реальным тестом:

def test_should_require_name
  s = Story.create(:name => nil)
  assert s.errors.on(:name)
end

Наш метод называется test_should_require_name. Как вы можете предположить, этот метод будет тестировать проверку наличия названия. Посмотрим на код метода:

 s = Story.create(:name => nil)

Эта строка создает новый объект Story – как при нормальной работе контроллера. Заметьте, однако, что на этот раз мы намеренно оставляем атрибут name пустым (nil). Поскольку метод create попытается немедленно сохранить новый объект, одновременно будут выполнены и проверки, определенные в модели.

На данный момент мы можем увидеть результат проверки, считав атрибут errors созданного объекта.

 assert s.errors.on(:name)

У каждого объекта модели в Rails есть атрибут errors. Этот атрибут содержит результаты любых примененных проверок – при неудаче проверки, ошибки будут существовать "на" этом атрибуте. В данном случае мы намерено оставили атрибут name пустым; передача символа errors.on должна вернуть true, что и должно подтвердить наше assert-выражение.

Атрибут name – не единственный необходимый атрибут в модели Story – атрибут link тоже должен иметь значение перед сохранением рассказа. Мы уже добавили один тест, так что добавить второй будет нетрудно. Давайте добавим тест проверки атрибута link:

def test_should_require_link
  s = Story.create(:link => nil)
  assert s.errors.on(:link)
end

Правда, просто?

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

def test_should_create_user

 s = Story.create(
 :name => 'My test submission',
 :link => 'http://www.testsubmission.com/')

 assert s.valid?
end

В этом тесте создается новый объект Story и всем необходимым атрибутам присваиваются значения. Assert подтверждает, что созданный объект действительно прошел все проверки все ратификации, вызывая метод valid? – этот метода возвращает true, если при проверках отсутствуют ошибки.

Запускаем юнит-тест

Теперь давайте запустим этот маленький набор тестов. Введите следующую команду из корневого каталога приложения:

$ rake test:units

Эта команда исполнит один за другим все тесты, имеющиеся в каталоге test/unit, и предупредит нас о любых проблемах. Результат успешного выполнения тестов приведен на рисунке 6.5.


Рисунок 6.5.

Как видите, rake хорошо отображает ход выполнения тестов. Результаты говорят, что все три теста и три assert-выражения были выполнены, а это все, что было в нашем наборе тестов.

Между строками Started и Finished находится несколько точек – по одной на каждый пройденный тест. Если не проходит assert, будет показано заглавное F, а если один из тестов содержит ошибку – заглавное Е и описание ошибки.

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

 assert ! s.valid?

Сохраните файл и запустите тесты заново.

$ rake test:units

Вы увидите F, означающее неудачу теста, как показано на рисунке 6.6. Кроме этого, выводится описание assert-выражения, приведшего к провалу теста.


Рисунок 6.6.

Вооружась этой информацией, легко обнаружить и исправить ошибку. Нам дают имя неудачного теста (test_should_create_user), совокупность тестов (test case), к которой он принадлежит (StoryTest), и строку, на которой произошла ошибка (line 18). Таким образом, (в данном случае ложного) виновника легко найти и исправить.

Исправьте испорченную нами выше строку теста test_should_create_user:

 assert s.valid?

Ну вот мы и протестировали модель. Позже, по мере добавления функциональности, мы расширим набор тестов для модели.

Тестируем контроллер

На первый взгляд функциональное тестирование контроллера не слишком отличается от тестирования модели – это всего лишь другая часть MVC. Однако тут придется заняться некоторой настройкой окружения.

Анализируем скелетный файл

Еще раз, скелет функционального теста создается при генерировании StoryController. Этот скелет находится в файле test/functional/story_controller_test.rb:

require File.dirname(__FILE__) + '/../test_helper'
require 'story_controller'

# Повторно сгенерировать сообщения об ошибке,  порожденные контроллером
class StoryController; def rescue_action(e) raise e end; end

class StoryControllerTest < Test::Unit::TestCase

 def setup
 @controller = StoryController.new
 @request = ActionController::TestRequest.new
 @response = ActionController::TestResponse.new

 end
 # Замените это реальными тестами.
 def test_truth

 assert true
 end
end

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

Метод setup вызывается автоматически перед каждым набором функциональных тестов. Он отвечает за настройку "чистого" окружения, что достигается созданием новых объектов @controller, @request и @response.

У нас также есть еще тест test_truth, который мы перепишем еще раз.

Пишем функциональный тест

Чтобы добавить первый тест для StoryController, замените метод test_truth в скелете функционального теста:

def test_should_show_index 
  get :index 
  assert_response :success 
  assert_template 'index'
  assert_not_nil assigns(:story)
end

Рассмотрим этот метод подробнее:

def test_should_show_index

Как вы могли понять по имени метода, здесь мы проверяем, что действие index правильно отображается в браузере пользователя при запросе пути /index:

 get :index

Эта строка симулирует пользовательский запрос действия index у класса StoryController, экземпляр которого создан в методе setup. Он использует HTTP-метод GET; вызов функции post служит для тестирования POST-запросов.

 assert_response :success

assert_response проверяет соответствие HTTP-кода ответа ожидаемому.

 assert_template 'index'

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

assert_not_nil assigns(:story)

Этот последний assert не так интуитивен, как прочие, но все же достаточно понятно. assert_not_nil проверяет, равно ли nil поле @story. Конструкция assigns(:story) делает доступными функциональному тесту все поля, объявленные в действии контроллера. Объект @story – одна из таких переменных, поэтому передача тесту символа :story позволяет использовать поле @story внутри теста.

Для этого теста нам также нужны некоторые fixtures. Как говорилось в главе 5, fixtures – это заглушки для объектов модели, предоставляющие наборы данных на которых будут исполняться тесты. Fixtures связаны с моделью, поэтому для каждого класса модели в приложении нужен свой fixture-файл. Это значит, что нам надо явно указать, какие fixtures нужно загружать для каждого теста.

Это можно сделать, добавив в определение класса функционального теста строку:

fixtures :stories

Запускаем функциональный тест

Запустим набор функциональных тестов. Для этого опять пригодится испытанная утилита rake:

$ rake test:functionals

Результат успешного запуска тестов приведен на рисунке 6.7.


Рисунок 6.7.

Другие функциональные тесты

Есть одно действие, для которого мы еще не написали тестов – действие new. Надо бы создать несколько разных тестов для этой страницы. Давайте займемся этим.

Чтобы проверить работу действия new в режиме GET, мы будем использовать набор тестов с названием test_should_show_new. Добавьте следующий метод после теста test_should_show_index, созданого выше:

def test_should_show_new
 get :new
 assert_response :success
 assert_template 'new'
 assert_not_nil assigns(:story)

end

Это несложно – за исключением нескольких текстовых расхождений, этот тест практически идентичен test_should_show_index. Но это еще не все!

В шаблоне new есть элемент form, и мы, конечно, должны проверить, что он корректно отображается. Вот еще один тест, выполняющий это: хелпер assert_select, который использован здесь – очень гибкое и мощное средство для проверки того, что определенный HTML-документ существует в документе, возвращаемом при запросе. assert_select может даже проверить иерархию HTML-элементов, независимо от глубины вложенности; он может также протестировать атрибуты элемента (например, значение class или id). На самом деле, он настолько гибок, что, в принципе, можно было бы посвятить всю главу только ее возможностям.

def test_should_show_new_form
  get :new
  assert_select 'form p', :count => 3
end

Но здесь мы можем зайти в тупик. Вернемся назад! assert_select проверяет наличие одного элемента form с пятью вложенными элементами p (число сообщается при помощи аргумента :count). Эти пять абзацев содержат поля, составляющие форму отправки рассказа.

Как мы указываем элемент в этой иерархии? Элементарно – следуя простым правилам CSS-селекторов.

В этом примере мы хотим сослаться на элемент, который находится внутри элемента form. Если бы мы писали CSS-правило для выделения этих элементов жирным, оно выглядело бы как-нибудь так:

form p {

 font-weight: bold;
}

Так же, как при ссылке на параграфы в CSS, параметр, который мы используем с assert_select, – это просто 'form p'.

Наконец, чтобы проверить отправку нового рассказа, напишем еще несколько коротких тестов:

  def test_should_add_story
    post :new, :story => {
      :name => 'test story',
      :link => 'http://www.test.com/'
    }
    assert ! assigns(:story).new_record?
    assert_redirected_to :action => 'index'
    assert_not_nil flash[:notice]
  end

assert_select, исходно доступное только как дополнительный модуль, является развитием assert_tag, появившегося в Rails 1.2.

Разберем этот тест строка за строкой.

 post :new, :story => {

 :name => 'test story',

 :link => 'http://www.test.com/'

 }

Как я упоминал, post – это еще один способ программно выполнить HTTP-запрос из теста. post принимает несколько параметров: в данном случае мы симулируем отправку рассказа. Для этого нам нужно передать хеш-таблицу, которая содержит значения обязательных атрибутов рассказа – символы, представляющие атрибуты name и link.

Строка, идущая сразу после вызова post, проверяет результаты:

 assert ! assigns(:story).new_record?

Здесь используется вызов метода new_record? для подтверждения сохранения записи в БД. Поскольку мы хотим, чтобы assert оповестил нас, если запись не была сохранена, мы используем восклицательный знак (!), реверсирующий возвращаемое значение вызова new_record?.

При удачной отправке рассказа приложение производит перенаправление. Это можно протестировать с помощью assert_redirected_to:

 assert_redirected_to :action => 'index'

Наконец, проверяем, что содержимое flash-области notice не равно nil:

 assert_not_nil flash[:notice]

Наш быстрорастущий набор тестов дорос до той точки, когда мы можем быть уверены, что процесс отправки сообщений функционирует нормально.

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

def test_should_reject_missing_story_attribute
    post :new, :story => { :name => 'story without a link' }
    assert assigns(:story).errors.on(:link)
  end

В первой строке этого кода мы пытаемся отправить рассказ без ссылки:

 post :new, :story => { :name => 'story without a link' }

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

 assert assigns(:story).errors.on(:link)

Вот и все! Мы написали все тесты, нужные на текущий момент. Запустим весь набор.

Запускаем весь набор тестов

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

$ rake test

Результат успешного выполнения теста показан на рисунке 6.8.


Рисунок 6.8.

Поздравляю! Мы не только создали полный набор тестов, но и выяснили, что в нашем приложении нет ошибок – открытие, которое может наполнить гордостью даже самого опытного разработчика. Чтобы закончить, давайте обратимся к производительности приложения и рассмотрим логи, сгенерированные ActionPack.

Рассмотрим логи

Вкратце мы рассматривали возможности логирования, предоставляемые ActiveRecord, в пятой главе – когда говорили об автоматизации, то есть генерации SQL, неявно выполняемой в Rails.

Если вас это заинтересовало, вы будете рады узнать, что ActionPack – еще и плодовитый писатель логов. Пример такого лога показан на рисунке 6.9.

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


Рисунок 6.9.

Уровень детализации в логах Rails – это реальное достоинстве при выявлении проблем в коде – логии предоставляют реальный обзор происходящего на запрошенной странице. Тот же уровень детализации обеспечивается для функциональных и юнит-тестов в файле log/test.log.

Временные метки, которые также записываются в лог-файл, особенно интересны. Рассмотрим следующее:

Completed in 0.03430 (29 reqs/sec) | Rendering: 0.00515 (15%) | 
DB: 0.02469 (71%) | 200 OK [http://localhost/story]

Из этой записи лога можно заключить, что:

71% может показаться значительной долей общего времени – и является таковой – но если посмотреть на абсолютные значения, становится ясно, что связь с БД заняла всего 0.02 сек.

Rails также предоставляет оценку того, сколько экземпляров этой конкретной страницы можно было бы потенциально вывести в секунду (в данном случае 29). Однако нужно заметить, что это очень грубая прикидка – мы запускаем приложение в окружении для разработки которое не оптимизировано по скорости. Кроме того, приложение исполняется не на том сервере, где оно будет развернуто во время реального использования. Так что реальные числа могут быть совсем другими.

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

Резюме

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


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

Copyright © 1994-2016 ООО "К-Пресс"