Сегодня мы поговорим о важнейшем элементе фреймворка Ruby on Rails — маршрутизации, а также посмотрим на принцип, вокруг которого она построена — архитектурный принцип REST.
Маршрутизация — это программное связывание элементов HTTP-запроса с конкретными элементами программного обеспечения сервера, которые выполняют этот запрос. Например, в ответ на определенный глагол и путь запроса вызывается определенный метод (action) определенного контроллера, внутри которого производится обработка запроса.
Краткий обзор маршрутов
Чтобы понять это определение, необходимо вспомнить, из чего состоит стандартный HTTP-запрос. Вот его структура:
PATCH http:// enterprise.ru /employees/42
| глагол | протокол | хост | путь (маршрут) -> ...
Здесь мы видим обозначение протокола, по которому происходит запрос, обозначение хоста, то есть имя сервера, которому передаётся запрос, и далее через слэш маршрут. Именно маршрут разбирается сервером приложения, чтобы доставить запрос в требуемый обработчик. Дополнительно для определения обработчика запроса, то есть для маршрутизации, используется так называемый глагол — в нашем случае PATCH. Глаголы иногда называют методами. Глаголы — это часть HTTP-запроса, определяющая смысл действия, которое будет произведено: чтение, запись, удаление данных и т.д.
Давайте рассмотрим частные примеры подобных запросов, а также, как фреймворк Ruby on Rails будет воспринимать их. Для краткости сейчас мы опустим имя хоста и частные параметры запроса — работать будем только с путями.
Итак, первый пример:
GET /employees
Этот запрос передаётся экшену EmployeesController#index
для отображения списка сотрудников мероприятия — на структурном уровне это значит, что в классе EmployeesController
Ruby on Rails находит для нас метод index
.
Следующий пример:
POST /employees
Здесь мы видим глагол POST и тот же самый маршрут. Такой запрос Ruby on Rails передаст EmployeesController
, но уже экшену create
, чтобы создать запись о сотруднике.
Рассмотрим еще один пример:
PATCH /employees/42
Здесь маршрут тот же, однако после слэша указан целочисленный идентификатор. В таком виде Ruby on Rails воспримет запрос как параметр для экшена edit
контроллера EmployeesController
, чтобы редактировать запись о конкретном сотруднике.
Наконец, при помощи того же самого маршрута и глагола DELETE мы можем отправить серверу сообщение об удалении записи о конкретном сотруднике:
DELETE /employees/42
Как устроены маршруты?
Во-первых, вы видите описание некоторых ресурсов, в нашем случае — employees
. То есть мы говорим о некоторых сущностях, называемых сотрудниками. Также мы говорим об идентификаторах конкретных сотрудников, а еще видим, что глагол формирует определенное представление, что предстоит сделать с конкретным ресурсом или совокупностью ресурсов.
Такой подход, иначе — архитектурный принцип расшифровки или интерпретации запроса, получил название REST. По принятому в разработке Ruby on Rails соглашению, а мы помним, что для нас соглашения важнее конфигурации, маршрутизация, то есть переправка запросов определенным экшенам в определенные контроллеры, происходит с соблюдение архитектурного стиля REST.
REST — это аббревиатура от REpresentational State Transfer, архитектурный стиль, предназначенный для программирования взаимодействий клиента и сервиса по протоколу HTTP с передачей состояния по представлению. REST в общем случае ограничивает конструирование запросов и ответов определенными правилами. Эти правила делают систему взаимодействия клиент-сервис производительной, масштабируемой, простой, удобной для модификаций, переносимой, отслеживаемой и надежной.
Из чего «вырос» REST?
Теперь немного углубимся в архитектуру REST — подумаем, для чего она создавалась и что отражает.
В первую очередь, REST отвечает определенным требованиям, предпосылкам и обстоятельствам, в которых происходят HTTP-запросы. Например, в протоколе HTTP отсутствует состояние — то есть сервер не выполняет промежуточного хранения информации о статусе объектов между запросами. Разумеется, сервер хранит информацию в базе данных, но в ней хранится некоторая статичная информация о ресурсе между запросами, а вот состояния обработки и процесса трансформации объектов между запросами не сохраняются — это называется stateless-протокол.
Далее мы знаем, что в современном вебе необходимо кэшировать передаваемые данные. Запросы иногда достигают больших размеров, как и передаваемые данные (ими могут быть как текстовые файлы, так и графические, видеопотоки и множество иных разнообразных данных). Всё это необходимо предварительно буферизовать для быстрой выдачи клиентскому приложению. Кэширование — это сложная многоуровневая система, реализованная на базе разнородных сетей в интернете, и для нее важно иметь однородное представление о ресурсах.
Кроме всего прочего, мы знаем, что в мире множества существующих архитектурных решений сформировалась потребность на унификацию интерфейса HTTP-запросов. Например, в запросе желательно иметь однозначную идентификацию ресурса и действия с его участием. Важно иметь всю полноту данных для операций на конкретном ресурсе, а также — полноту метаданных, дополнительных данных о том, как необходимо осуществлять ответ на заданный запрос.
Представим, что нам необходимо сообщить удаленной стороне, что мы передаём или принимаем данные в формате HTML, XML, JSON. В REST мы работаем с понятием ресурса — так что же такое ресурс? Это любой целостный объект для взаимодействия в веб-приложении. Иначе говоря, это совокупность данных о целом предмете взаимодействия.
В примерах вы видели слово employees — значит, мы взаимодействовали с ресурсом сотрудника — с множеством или с конкретным представителем, имеющим идентификатор 42. В архитектуре MVC ресурс определяется моделью данных, хранящейся в базе и трансформирующейся в приложении, а также множеством операций, выполняемых в контроллере. Далее к совокупности операций, объектов, взаимодействующих с ресурсом, относятся view, то есть отображение моделей в нужном пользователю виде, и любые специальные объекты, реализующие бизнес-логику приложения.
Архитектурный стиль REST позволяет сильно упростить задачу компоновки и поддерживаемости кода сложного веб-приложения. Во фреймворке Ruby on Rails именно REST используется как основное, базовое соглашение для разбивки кода на соответствующие ресурсам группы моделей-view-контроллеров.
REST характеризуется тем, что определяет смысл составных частей запроса. Давайте еще раз вернёмся к схеме запроса, чтобы посмотреть, как он трактуется соглашением REST:
PATCH http:// enterprise.ru /employees/42
| глагол | протокол | хост | путь (маршрут) -> ...
Глагол запроса используется для того, чтобы однозначно определить метод взаимодействия с ресурсом. В данном случае нам необходимо изменить ресурс, поэтому мы используем глагол PATCH. Далее определяется протокол, по которому передаётся запрос. В нашем случае протокол HTTP. Затем появляется хост, то есть имя сервера, на котором выполнится запрос, и уже после — маршрут, то есть обозначение ресурса или ресурсов, с которыми мы взаимодействуем.
Разбираем механизмы маршрутизации в RoR на примерах
Теперь узнаем, с помощью каких механик эти маршруты, собранные в соответствии с REST-соглашением, переводятся в вызов конкретных методов и объектов.
Для разбора механик маршрутизации смоделируем студенческое приложение, иллюстрирующее работу в воображаемой интернет-академии. В директории /config
находится файл routes.rb
. Этот файл содержит написанные на специальном прикладном языке, или DSL, правила маршрутизации.
Правила маршрутизации — это описание соответствия запросов экшенам контроллеров. Все запросы в RoR проходят через контроллеры (за редким исключением т.н. запросов к статическим файлам), а правила описывают маршруты в терминах ресурсов или, реже, соответствия конкретных путей url конкретным экшенам. Именно в файле routes.rb
находится специфический язык, специфические объявления правил маршрутизации.
Мы будем вносить изменения в файл маршрутизации и проверять образовавшиеся маршруты командой bin/rails routes
. Команда выполняется в корневой директории приложения. Сразу замечу: свежесгенерированное RoR-приложение может иметь до пары дюжин маршрутов по умолчанию — они образованы интегрированными компонентами фреймворка (системой пересылки сообщений, хранения файлов и.т.п.). Их мы для краткости будем игнорировать.
Примеры, которые мы сейчас разберём, сгенерированы при помощи команды rails routes
с ключом “-g
”, позволяющим сокращать вывод программы до определенных маршрутов, соответствующих описанным в виде регулярного выражения масок.
Итак, начинаем с чистого routes.rb
. В нем определён лишь блок, внутри которого будут помещаться все маршрутные правила:
Rails.application.routes.draw do
end
Добавим корневой маршрут, запустим вывод маршрутов и посмотрим на результат:
Rails.application.routes.draw do
root to: 'home#index'
end
> bin/rails routes
Prefix Verb URI Pattern Controller#Action
root GET / home#index
# далее массив маршрутов по умолчанию
Как видите, rails routes
вывел нам таблицу, в которой отражены поля т.н. префикса, далее — глагол веб-запроса, затем — URL, в этом поле располагается путь до необходимого места в приложении.
Заметим, что пути не включают в себя спецификацию протокола и названия хоста — только лишь относительный путь внутри самого приложения. В специальной нотации мы видим структуру «слово#слово
». Так в Ruby-документации обозначается вызов метода экземпляра объекта класса, прописанного перед #
. В нашем случае перед #
находится нормализованное название контроллера, а после #
— action.
Получается, что в случае нашего примера в файле, определяющем класс Home Controller
, находится метод index
— экшен, запускаемый по достижении конкретного маршрута. Итак, мы сгенерировали корневой маршрут приложения, при обработке которого будет запущен экшен index
контроллера Home Controller
.
Теперь посмотрим, как будет выглядеть использование хелпера маршрута. В области видимости контроллеров и view образовался новый глобальный видимый метод — root_path
. При его вызове будет генерироваться строка, содержащая URL-паттерн:
= link_to 'Academy', root_path
Созданный нами главный маршрут приложения — пример нересурсного маршрута: он отражает некоторое общее соглашение о формировании веб-страницы по корневому пути, но при этом не является указанием на специфический ресурс, т.е. совокупность конкретной модели и бизнес-логики вокруг нее. Обратите внимание, что в нересурсных маршрутах мы явно указываем имя контроллера.
Перейдем к ресурсным маршрутам, ведь именно они составляют основную «суперсилу» Ruby on Rails. Пользуясь правилом «convention over configuration» и архитектурными правилами REST, RoR реализует выразительный и лаконичный DSL маршрутизации ресурсов.
Создадим группу маршрутов для управления ресурсом студентов:
Rails.application.routes.draw do
root to: 'home#index'
resources :students
end
Теперь выведем команду rails routes
:
Prefix Verb URI Pattern Controller#Action
root GET / home#index
students GET /students(.:format) students#index
POST /students(.:format) students#create
new_student GET /students/new(.:format) students#new
edit_student GET /students/:id/edit(.:format) students#edit
student GET /students/:id(.:format) students#show
PATCH /students/:id(.:format) students#update
PUT /students/:id(.:format) students#update
DELETE /students/:id(.:format) students#destroy
Одна строка DSL-маршрута образовала целое семейство маршрутов. В данном случае мы видим полное множество маршрутов для объявленного ресурса. Ещё маршруты, служащие тому, чтобы видеть полный список студентов и создавать нового конкретного студента. Более того, здесь есть целый ряд маршрутов, управляющих записями о студентах, в том числе формы редактирования или удаления студента, а также пути загрузки полностью или частично измененной информации.
Вот подробная расшифровка действий сгенерированных хелперов:
GET http://academy.edu/students # students_path — показать список студентов
POST http://academy.edu/students # students_path — создать ресурс "студент"
GET http://academy.edu/students/17 # student_path — показать ресурс "студент" с id 17
PATCH http://academy.edu/students/17 # student_path — изменить ресурс "студент" с id 17
PUT http://academy.edu/students/17 # student_path — ЗАменить ресурс "студент" с id 17
DELETE http://academy.edu/students/17 # student_path — удалить ресурс "студент" с id 17
Названия контроллеров и экшенов заранее предсказываются фреймворком, он ожидает, что в файлах лежат классы, именованные соответствующим образом.
Пример хелпера маршрута к странице редактирования студента выглядит так:
= link_to "Edit student ${@student.id}", edit_student_path(@student)
Давайте представим, что RoR-процесс отображает не HTML-страницы, а, например, JSON или JSON API. В таком случае нам не нужен весь массив возможных маршрутов — нужно лишь ограниченное множество маршрутов. Мы можем уменьшить количество генерируемых RoR-маршрутов при помощи специального указания. Управлять списком ресурсов можно, добавляя к методу формирования маршрутов именованные аргументы :only
и :except
. Из соображений улучшения стиля, я рекомендую использовать именно only
. Он создаёт четкий white list необходимых действий. Это важно при разработке больших приложений. Возьмём only
и передадим ему список экшенов, которые необходимо сгенерировать. Соответственно, маршруты будут сгенерированы лишь для тех экшенов, которые мы опишем:
Rails.application.routes.draw do
root to: 'home#index'
resources :students, only: %i[show create edit]
end
Итак, мы запросили генерацию маршрутов ресурса students
для только трех экшенов из всех возможных. Что же из этого вышло?
⟩ bin/rails routes -g student
Prefix Verb URI Pattern Controller#Action
students POST /students(.:format) students#create
edit_student GET /students/:id/edit(.:format) students#edit
student GET /students/:id(.:format) students#show
Обратите внимание, что RoR автоматически генерирует маршруты с использованием определенных глаголов — согласно существующему соглашению. Например, для экшена students create
образовался глагол POST, это значит, что только такой глагол будет обрабатываться на этом маршруте. Если мы употребим GET вместо него — получим ошибку 404.
Иногда необходимо объявлять маршруты для действий над всей коллекцией ресурсов. Экшены, к которым будут создаваться маршруты, не входят в множество стандартных. Вернемся к студентам — допустим, нам необходимо отправлять студентов на удаленку или назначать каждому из них курс по Ruby. Выглядеть это будет так:
Rails.application.routes.draw do
root to: 'home#index'
resources :students, only: %i[show create edit] do
post :assign_to_ruby_course, on: :member
post :send_to_remote, on: :collection
end
end
Мы открываем блок в выражении объявления ресурсов и добавляем необходимые действия. Это делается при помощи глагола, обозначающего действие, префикса и пометки элемента коллекции ресурсов, на котором необходимо выполнить действие. Как видите, курс по Ruby мы назначаем индивидуально, а на удаленку отправляем всю коллекцию.
С этим уточнением у нас образовался новый маршрут:
Prefix Verb URI Pattern Controller#Action
assign_to_ruby_course_student POST /students/:id/assign_to_ruby_course(.:format) students#assign_to_ruby_course
students POST /students(.:format) students#create
edit_student GET /students/:id/edit(.:format) students#edit
student GET /students/:id(.:format) students#show
Если нам нужно ввести в приложение несколько ресурсов, над которыми возможны одинаковые действия, на помощь приходят concerns
— обобщения маршрутов. Например, у нас в академии появились преподаватели, к которым применимы все действия, уже описанные для студентов — т.е. нужны те же специальные маршруты. Во избежание избыточного кода мы можем формировать concerns
. Преобразуем код в concerns
:
Rails.application.routes.draw do
root to: 'home#index'
resources :students, only: %i[show create edit] do
post :assign_to_ruby_course, on: :member
post :send_to_remote, on: :collection
end
resources :trainers, only: %i[show create edit] do
post :assign_to_ruby_course, on: :member
post :send_to_remote, on: :collection
end
end
Эту длинную запись при помощи консёрна можно сократить вот так:
Rails.application.routes.draw do
root to: 'home#index'
concern :assignable do
post :assign_to_ruby_course, on: :member
post :send_to_remote, on: :collection
end
resources :students, only: %i[show create edit],
concerns: :assignable
resources :trainers, only: %i[show create edit],
concerns: :assignable
end
Мы выделили в консёрн assignable
— т.е. повторяющиеся маршруты, а затем объявили использование обобщения для множеств ресурсов студентов и преподавателей. Вывод выглядит следующим образом:
⟩ bin/rails routes -g "(student)|(trainer)"
Prefix Verb URI Pattern Controller#Action
assign_to_ruby_course_student POST /students/:id/assign_to_ruby_course(.:format) students#assign_to_ruby_course
send_to_remote_students POST /students/send_to_remote(.:format) students#send_to_remote
students POST /students(.:format) students#create
edit_student GET /students/:id/edit(.:format) students#edit
student GET /students/:id(.:format) students#show
assign_to_ruby_course_trainer POST /trainers/:id/assign_to_ruby_course(.:format) trainers#assign_to_ruby_course
send_to_remote_trainers POST /trainers/send_to_remote(.:format) trainers#send_to_remote
trainers POST /trainers(.:format) trainers#create
edit_trainer GET /trainers/:id/edit(.:format) trainers#edit
trainer GET /trainers/:id(.:format) trainers#show
Время от времени, а на деле — довольно часто, у нас возникает потребность обращаться к ресурсам опосредованно, т.е. через другие ресурсы. В таком случае ресурсы, находящиеся внутри других ресурсов, называются вложенными.
Например, у каждого из наших студентов может быть собственный массив оценок, для которых необходимо реализовать RESTfull API. Здесь мы уже не будем приводить весь файл — он становится достаточно толстым. В этом и последующих примерах будем пользоваться только фрагментами:
resources :students, only: %i[show create edit] do
concerns :assignable
resources :marks, only: %i[create destroy]
end
Итак, мы чуть-чуть переписали использование обобщения, и заодно внутри множества студентов объявили множество ресурсов оценки — ограничили действия с ними до постановки и удаления. Сгенерировались следующие маршруты:
Prefix Verb URI Pattern Controller#Action
student_marks POST /students/:student_id/marks(.:format) marks#create
student_mark DELETE /students/:student_id/marks/:id(.:format) marks#destroy
Мы видим, что внутри маршрута студентов образовался маршрут к оценке конкретного экземпляра.
В образованных нами маршрутах есть слова, начинающиеся с двоеточия — это подстановочные параметры. Таким образом RoR сообщает, что в образованном экшене будет передан параметр с конкретным именем, куда RoR подставит идентификатор конкретного студента, внутри маршрута которого мы вложили ресурс оценки.
Обратите внимание, что во втором случае у нас передаются два подстановочных параметра. Первый — student id
, идентификатор студента, второй — mark id
, идентификатор оценки.
Двинемся дальше. Если нам необходимо по логике приложения работать с вложенным ресурсом со ссылкой к родительскому ресурсу, тогда RoR использует полную форму обращения, но если нужно работать только с вложенным ресурсом — мы воспользуемся сокращением. Чтобы сократить строку вложенного маршрута до необходимого приложению минимума, нужно использовать аргумент shallow
:
resources :students, only: %i[show create edit] do
concerns :assignable
resources :marks, only: %i[create destroy], shallow: true
end
Мы пометили ресурс оценки как мелкий ресурс и теперь посмотрим, как выглядят маршруты:
Prefix Verb URI Pattern Controller#Action
student_marks POST /students/:student_id/marks(.:format) marks#create
mark DELETE /marks/:id(.:format) marks#destroy
Благодаря такому решению, маршрут оценки сократился — в нем отсутствует длинный префикс, свидетельствующий непосредственно о вложении. Полная и сокращенная маршрутизация используются по усмотрению — в зависимости от требования заказчика или логики приложения.
В RoR предусмотрено выделение маршрутов в пространство имен. Например, нам нужно создать админку с отдельными маршрутами и контроллерами для них. Бизнес-логика админки отличается от логики простого приложения, и для админки потребуются как специальные маршруты, так и специальные контроллеры. Функционально они должны быть похожи с уже созданными, но по факту отличаться, чтобы не путать логику двух разных частей приложения. Физически это будет реализовано через модули и вложенные в них классы контроллеров. Опишем мы это так:
namespace :admin do
resources :students do
resources :marks
end
resources :trainers
end
Мы объявили namespace
и поместили в него ресурсы с необходимой вложенностью. Маршруты и область имен у нас получаются следующие:
Prefix Verb URI Pattern Controller#Action
admin_student_marks GET /admin/students/:student_id/marks(.:format) admin/marks#index
POST /admin/students/:student_id/marks(.:format) admin/marks#create
new_admin_student_mark GET /admin/students/:student_id/marks/new(.:format) admin/marks#new
edit_admin_student_mark GET /admin/students/:student_id/marks/:id/edit(.:format) admin/marks#edit
Префиксы наших хелпер-методов пропорционально усложняются — они находятся слева в таблице. У нас появился общий для всех сгенерированных маршрутов префикс admin
. Также мы видим в колонке ControllerAction
нотацию со слэшем, означающую, что marks controller
будет находиться внутри namespace admin
. Таким образом, мы создали отдельный контроллер общей логики.
Если нужно выделить специальный префикс для маршрута, контроллер которого нет смысла помещать в собственное пространство имен, можно использовать метод scope
в следующем виде:
scope :statistics do
resources :marks, only: %i[] do
get :statistics, on: :collection
end
end
Например, мы сделали префикс для маршрутов отображения статистики, поместили в него хорошо знакомый ресурс оценок, соответствующий контроллер, определили для него что-то ограниченное, запретив формировать маршруты по умолчанию. Давайте посмотрим, что в таком случае образуется в маршруте:
Prefix Verb URI Pattern Controller#Action
statistics_marks GET /statistics/marks/statistics(.:format) marks#statistics
В URL появляется префикс маршрута statistics
, при этом marks controller
находится в общем пространстве, корневом или иначе — top level
.
Если контроллер наоборот нужно убрать в выделенное пространство имен, но не формировать префикс, используется другая форма выражения — scope module
:
scope module: :accounting do
resources :trainers, only: %i[] do
get :salary
end
end
Мы видим, что scope
объявлен для модуля с названием accounting
. Внутри него находится контроллер для преподавателей — например, с целью управления их зарплатой. Подобное описание формирует следующие маршруты:
Prefix Verb URI Pattern Controller#Action
trainer_salary GET /trainers/:trainer_id/salary(.:format) accounting/trainers#salary
В правой колонке для контроллера образуется пространство имен, в которое нужно поместить trainers controller
— этот модуль отличается от предыдущих, для него нужны специальные контроллеры, и бизнес-логика в нем будет соответствующая. В то же самое время, в паттернах маршрута приложения мы не видим специального обособления — лишь то, что в маршрутах для преподавателей появились уточняющие моменты.
Напоследок, RoR позволяет определять маршруты для ресурсов в единственном числе. Добавим новый маршрут к информации о ректоре:
resource :rector, only: :show
Учтите, что теперь все слова мы используем в единственном числе. В итоге образуется новый маршрут:
Prefix Verb URI Pattern Controller#Action
rector GET /rector(.:format) rectors#show
Учтите, что даже в этом случае соглашение RoR диктует необходимость именования контроллера во множественном числе ресурса — несмотря на то, что в логике приложения и в маршрутах ректор будет присутствовать в единственном числе.
Не стесняйтесь задавать вопросы в комментариях, мы обязательно ответим.