Всё, что нужно знать о routes, params и формах в Rails

Illustration of a person sitting on the ground, leaning against a wall while looking at a tablet device. They have casual attire with orange sneakers and a scarf. Illustration of a person sitting on the ground, leaning against a wall while looking at a tablet device. They have casual attire with orange sneakers and a scarf.

В этой статье мы рассмотрим params в Ruby on Rails: продемонстрируем как контроллер обрабатывает запрос от пользователя, как происходит передача параметров из формы в наш контроллер и узнаем как Rails использует объект params для фильтрации данных перед передачей их в объекты других классов.

Ресурсы и routes

Создадим новое веб-приложение, которое позволит нам создавать семестры и курсы для семестра. Соответственно, в этом приложении существуют модели Semester и Course. Чтобы иметь к ним доступ нам понадобятся контроллеры. С помощью методов в контроллере мы сможем создать, читать, редактировать и удалять семестры или курсы, используя формы в браузере. Приступим!

~ $ rails new sepage && cd sepage

Создадим модели, с атрибутами

~ $ rails g model Semester name:string fullname:string current:boolean
~ $ rails g model Course semester:references name:string description:text

Выполним миграцию, которая создаст таблицы в базе данных:

~ $ rake db:migrate

В моделях укажем ассоциации:

# app/models/semester.rb
class Semester < ActiveRecord::Base
  has_many :courses
end
# app/models/course.rb
class Course < ActiveRecord::Base
  belongs_to :semester
end

Следующим шагом будет настройка маршрутизации нашего веб-приложения. Но сначала поговорим о REST.

Максимально краткое введение в REST

REST (Representational State Transfer) – это стиль архитектуры программного обеспечения для распределенных систем, таких как World Wide Web, который, как правило, используется для построения веб-служб. Системы, поддерживающие REST, называются RESTful-системами.

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

Чтобы продемонстрировать наглядно, создадим маршруты для ресурсов semesters и courses:

# config/routes.rb
resources :semesters, shallow: true do
  resources :courses
end

Опция shallow: true делает короткие маршруты для вложенных ресурсов, именно для экшенов show, edit, update, destroy.

Строго говоря, ресурс != модель. У вас может быть ресурс без модели и модель, не использующаяся как ресурс. Например, регистрация пользователя: здесь ресурсом будет registration с роутами resources :registrations, only: [:new, :create]. Контроллер этого ресурса создаёт новые записи модели User, хотя с точки зрения логики приложения это ресурс "регистрация".

Теперь выполним rake routes, чтобы увидеть нашу новую маршрутизацию.

routes в Rails

Столбец Prefix указывает на короткую ссылку. Если добавить к ссылке префикс _path или _url, то получится route helper, который сгенерирует нам строку-сслыку. Например, course_path, semesters_url – это routes helpers. Их можно использовать с хелпером link_to, чтобы строить теги <a> для навигации внутри приложения.

Столбец Verb определяет HTTP-метод, с которым этот путь будет распознан. Тут можно использовать get, patch, put, post или delete (ну и некоторые другие, использующиеся чуть реже).

URI Pattern – это пример URL для доступа к ресурсу.

Controller#Action показывает название контроллера#экшена.

Для каждой единицы информации (будь то семестры или курсы) определен глобальный идентификатор URL, со строго заданным форматом. Ту же самую информацию о роутах можно увидеть в браузере по адресу http://localhost:3000/rails/info. Узнать подробнее о генерации роутов можно в официальном руководстве.

Управление ресурсами происходит через методы контроллера, которые обрабатывают запросы от пользователя. Контроллер будет доступен по определенному URL и превращен в способ доступа к коллекции ресурсов по url/items или к просмотру одного ресурса с нужным id, например url/items/3. Стоит понимать, что resources внутри файла routes.rb это не более чем синтаксический сахар. Нет никакой разницы между написанием resources :semesters и такой конструкцией:

get "semester" => "semesters#show"
get "semesters" => "semesters#index"
get "edit_semester" => "semesters#edit"
get "new_semester" => "semesters#new"
patch "semesters" => "semesters#update"
post "semesters" => "semesters#create"

Генерация форм и form helpers

Далее мы создадим экшены в нашем SemesterController. Но сначала рассмотрим их предназначение:

  • new рендерит форму для создания нового ресурса.
  • create обрабатывает поступившие из формы данные и создает новый ресурс.
  • show показывает ресурс.
  • index показывает массив всех ресурсов.
  • edit рендерит форму для редактирования ресурса.
  • update обновляет ресурс новыми данными, которые пришли из формы edit.
  • destroy удаляет ресурс.

Я продемонстрирую создание методов в контроллерах вручную, без использования генератора кода (scaffolding).

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

Начнем с SemestersController. В методе new инициализируем наш ресурс:

# app/controllers/semesters_controller.rb
class SemestersController < ApplicationController
  def new
    @semester = Semester.new
  end
end

Создание HTML-форм происходит через хелпер, потому что это уменьшает время на генерирование формы и избавляет от всевозможных дополнительных проблем с получением параметров. Создание тега <form> проходит с помощью хелпера form_for. Никогда не создавайте формы вручную, всегда используйте методы-хелперы! Подробнее о хелперах в документации Rails.

Хэлпер – это, кстати, тоже обычный ruby метод, обычно вызывающийся внутри views. Всё ещё никакой магии, только Ruby.

Приступим к созданию формы. Для этого в views создаем app/semesters/new.html.erb файл и переходим в него:

<!-- app/semesters/new.html.erb -->
<h1> Создание семестра </h1>
<%= form_for @semester, url: semesters_path do |f| %>
  <%= f.text_field :name %><br>
  <%= f.text_field :fullname %><br>
  <%= f.check_box :current %><br>
  <%= f.submit "Создать семестр" %>
<% end %>

Далее запускаем наш сервер: ~ $ rails s и переходим к нашему только что созданному экшену new. Для этого в браузере введем http://localhost:3000/semesters/new.

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

 <h1> Создание семестра </h1>
<form class="new_semester" id="new_semester" action="/semesters" accept-charset="UTF-8" method="post"><input name="utf8" type="hidden" value="✓" /><input type="hidden" name="authenticity_token" value="310BWahxz8JClxIam1J5U2SGmt2SvRgUGfGbAfHekqfH9YMkVffy0ebR/bt+8Gcohpu/rLEemXdjBp3xGnYj5w==" />
  <input type="text" name="semester[name]" id="semester_name" /><br>
  <input type="text" name="semester[fullname]" id="semester_fullname" /><br>
  <input name="semester[current]" type="hidden" value="0" /><input type="checkbox" value="1" name="semester[current]" id="semester_current" /><br>
  <input type="submit" name="commit" value="Создать семестр" />
</form>

Здесь мы видим, что хелпер form_for создал форму и элементы. Форма будет отправляться на action="/semesters", где находится наш еще не написанный экшен create. Почему нам нужно отправлять на этот путь? Смотрим в rake routes.

Важный момент, что наш хелпер сгенерировал название каждого input'a как хэш ресурс[атрибут]. В HTML это просто текст, но это сделано специально, для получения этих данных через params.

Работа с параметрами и strong parameters

Рассмотрим получение данных контроллером. Для это создадим экшен create, где мы получим данные из формы и запишем их в БД.

# app/controllers/semesters_controller.rb
class SemestersController < ApplicationController
...
  def create
    @semester = Semester.create(semester_params)
    if @semester.errors.empty?
      redirect_to @semester
    else
      render 'new'
    end
  end

  private
  def semester_params
    params.require(:semester).permit(:name, :fullname, :current)
  end
end

Здесь мы производим сохранение нового объекта в базу данных, с аргументами, полученными из формы. Процесс получение параметров из формы мы определяем в приватном экшене с названием semester_params. В Rails 4 введен новый способ обработки параметров из формы strong_parameteres, который считается более безопасным. Ранее разрешённые атрибуты приходилось конфигурировать прямо в модели (см. attr_accessible).

Рассмотрим конструкцию более подробно:

params.require(:semester).permit(:name, :fullname, :current)

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

params = { semester: { name: 'Summer2015', fullname: 'Winter-Summer-2015', current: true } }


Что именно пришло из формы в виде параметров всегда можно посмотреть в логах

require – метод, который получает значение хэша по ключу, где ключом в данном случае является наш ресурс, указанный в форме. Если такого ключа нет, то Rails выбросит ошибку.

permit – метод, который определяет разрешенные параметры в нашем ресурсе для передачи их значений в контроллер. Мы указываем только то, что хотим получить!

Далее в экшене мы проверяем на ошибки валидации: if @semester.errors.empty?. Если все хорошо, то отправляем на наш созданный объект, иначе рендерим форму заново.

Теперь рассмотрим, как мы можем получить наш объект из базы данных по его id. Для этого существует метод find, а экшен show просто будет выводить запись:

...
# app/controllers/semesters_controller.rb
def show
  @semester = Semester.find(params[:id])
end
...

Метод params содержит атрибут id. Хотя мы явно не указывали его при создании миграции, в Rails атрибут id создается по умолчанию для каждой таблицы. Он содержит уникальный идентификатор ресурса, по которому его можно найти с помощью метода find. Напомню, что путь в routes к нашему элементу – /semesters/:id.

Если ты делаешь блог на Rails, то скорее всего захочешь вместо числа в ссылке выводить название поста. Для таких случаев отличной подойдёт гем friendly_id. Для более простых случаев – просто перепиши метод to_param.

Создадим views для show, где выведем наши данные, которые записаны в БД:

<!-- views/semesters/show.html.erb -->
<h1>Наш созданный семестр с названием <%= @semester.name %> </h1>
<p>Название <%= @semester.fullname %> </p>
<p>Являеться ли текущим семестром? <%= @semester.current %> </p>

Остальные экшены: edit, update, destroy действуют по такому же принципу, поэтому их созадние остаётся на совести читателя.

Передача параметров для связанных моделей

Ранее мы установили ассоциативную связь (has_many - belongs_to) между моделями. Это способствовало появлению нового метода Semester#courses, который показывает вложенность элементов.

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

# app/controllers/courses_controller.rb
def new
  @semester = Semester.find(params[:semester_id])
  @course = @semester.courses.build
end

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

Ранее, при создании связей между моделями, в таблице courses мы создали атрибут (столбец в БД) под названием semester_id. Теперь настало время присвоить ему значение.

Для этого нам нужен params, чтобы с маршрутизации вытащить semester_id и затем отыскать нужный семестр. Маршрут в нашем случае это /semesters/:semester_id/courses/new(.:format).

Далее нужно инициализировать новый course в нашем semester, методом build. Мы проинициализирум объект прямо в форме, без создания экшена new в CoursesController. Воспользуемся нашим знакомым хелпером form_for и создадим форму для создания курса:

<!-- app/views/semesters/show.html.erb -->
<%= form_for [@semester, @semester.courses.build] do |f| %>
  <%= f.text_field :name %><br>
  <%= f.text_field :description %><br>
  <%= f.submit "Создать курс" %>
<% end %>

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

<form class="new_course" id="new_course" action="/semesters/1/courses" accept-charset="UTF-8" method="post"><input name="utf8" type="hidden" value="✓" />
  <input type="hidden" name="authenticity_token" value="YqBi6Dcld58Y08eSv5+tHiafdp9GE0VOIHSPMWJO1WmIzrq20xrkJGlfR47Ttuamu8+9mqlpe9CxB7AfTkGgCQ==" />
  <input type="text" name="course[name]" id="course_name" /><br>
  <input type="text" name="course[description]" id="course_description" /><br>
  <input type="submit" name="commit" value="Создать курс" />
</form>

Затем создадим в CoursesController экшен create.

# app/controllers/courses_controller.rb
...
def create
  @semester = Semester.find(params[:semester_id])
  @course = @semester.courses.build(course_params)
  if @course.save
    redirect_to @semester
  end
end
...

И добавим в app/views/semesters/show.html.erb вывод всех курсов.

<h2>Все курсы в этом семестре:</h2>
<% @courses.each do |course| %>
  <ul>
   <li><%= course.name %></li>
  </ul>
<% end %>

Обратите внимание на тот момент, что в начале в настройках маршрутизации config/routes.rb мы использовали опцию shallow: true, которая делает экшенам show, edit, update, destroy короткие маршруты, поэтому не нужно возвращать идентификатор семестра (semester_id).

Волшебный мир HTTP запросов

Мы уже рассказывали, что Rails – это просто Ruby. Но есть ещё одна важная деталь, которую нужно всегда держать в голове при работе с Ruby on Rails или любым другим веб-фреймворком: вся их работа построена на отправке и обработке HTTP-запросов. У запросов обычно есть параметры и заголовки, а задача веб-приложений предоставить точки доступа для этих запросов (в случае Rails - через routes), грамотно обрабатывать их параметры и выводить пользователю результат.

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

А если всё ещё остались какие-то непонятки, то мы ждём твои вопросы и пожелания в комментариях :)