Rails для начинающих

Это руководство раскрывает установку и запуск Ruby on Rails.

После его прочтения, вы узнаете:

  • Как установить Rails, создать новое приложение на Rails и присоединить ваше приложение к базе данных.
  • Общую структуру приложения на Rails.
  • Основные принципы MVC (Model, View, Controller - «Модель-представление-контроллер») и дизайна, основанного на RESTful.
  • Как быстро генерировать изначальный код приложения на Rails.

1. Допущения в этом руководстве

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

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

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

2. Что такое Rails?

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

Rails - своевольный программный продукт. Он делает предположение, что имеется "лучший" способ что-то сделать, и он так разработан, что стимулирует этот способ - а в некоторых случаях даже препятствует альтернативам. Если изучите "The Rails Way", то, возможно, откроете в себе значительное увеличение производительности. Если будете упорствовать и переносить старые привычки с других языков в разработку на Rails, и попытаетесь использовать шаблоны, изученные где-то еще, ваш опыт разработки будет менее счастливым.

Философия Rails включает два важных ведущих принципа:

  • Don't Repeat Yourself: DRY — это принцип разработки ПО, который гласит, что "Каждый кусочек информации должен иметь единственное, неизбыточное, авторитетное представление в системе". Не пишите одну и ту же информацию снова и снова, код будет легче поддерживать, и он будет более расширяемым и менее ошибочным.
  • Convention Over Configuration: у Rails есть мнения о наилучших способах делать множество вещей в веб-приложении, и по умолчанию выставлены эти соглашения, вместо того, чтобы заставлять вас по мелочам править многочисленные конфигурационные файлы.

3. Создание нового проекта Rails

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

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

Нижеследующие примеры используют $ для обозначения строки ввода терминала в UNIX-подобных операционных системах, хотя он может быть настроен по-другому. Если используется Windows, строка будет выглядеть наподобие C:\source_code>

3.1. Установка Rails

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

  • Ruby
  • SQLite3
3.1.1. Установка Ruby

Откройте приложения для командной строки. На macOS откройте Terminal.app; на Windows выберите "Run" в меню Start и напишите cmd.exe. Любые команды, начинающиеся со знака доллара $ должны быть запущены в командной строке. Убедитесь, что у вас установлена текущая версия Ruby:

$ ruby --version
ruby 2.7.0

Rails требует, чтобы был установлен Ruby версии 2.7.0 или новее. Предпочтительно использовать последнюю версию Ruby. Если номер версии меньше этой (такие как 2.3.7 или 1.8.7), нужно будет установить новую копию Ruby.

Чтобы установить Ruby на Windows, сначала нужно установить Ruby Installer.

Дополнительные методы установки для большинства операционных систем можно увидеть на ruby-lang.org.

3.1.2. Установка SQLite3

Вам также понадобится установка базы данных SQLite3.

Многие популярные UNIX-подобные ОС поставляются с приемлемой версией SQLite3. Прочие пользователи могут обратиться к инструкциям по установке на сайте SQLite3.

Проверьте, что он правильно установлен и содержится в вашем PATH загрузки:

$ sqlite3 --version

Программа должна сообщить свою версию.

3.1.3. Установка Rails

Для установки Rails используйте команду gem install, представленную RubyGems:

$ gem install rails

Чтобы проверить, что все установлено верно, нужно выполнить следующее в новом терминале:

$ rails --version

Если выводится что-то вроде "Rails 7.0.0", можно продолжать.

3.2. Создание приложения Blog

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

Для использования этого генератора, откройте терминал, войдите в папку, в которой у вас есть права на создание файлов и запустите:

$ rails new blog

Это создаст приложение на Rails с именем Blog в директории blog и установит гемы, зависимости от которых упомянуты в Gemfile при использовании bundle install.

Можно посмотреть все возможные опции командной строки, которые принимает генератор приложения на Rails, запустив rails new --help.

После того, как вы создали приложение blog, перейдите в его папку:

$ cd blog

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

Файл/Папка Назначение
app/ Содержит контроллеры, модели, вью, хелперы, рассыльщики, каналы, задания и ассеты вашего приложения. Мы рассмотрим эту папку подробнее далее.
bin/ Содержит скрипты rails, которые стартуют ваше приложение, также директория может содержать другие скрипты которые вы используете для настройки, обновления, деплоя или запуска.
config/ Содержит конфигурации маршрутов, базы данных вашего приложения, и т.д. Более подробно это раскрыто в Конфигурирование приложений на Rails
config.ru Конфигурация Rack для серверов, основанных на Rack, используемых для запуска приложения. Подробнее о Rack смотрите на сайте Rack.
db/ Содержит текущую схему вашей базы данных, а также миграции базы данных.
Gemfile
Gemfile.lock
Эти файлы позволяют указать, какие зависимости от гемов нужны для вашего приложения на Rails. Эти файлы используются гемом Bundler. Подробнее о Bundler смотрите на сайте Bundler.
lib/ Внешние модули для вашего приложения.
log/ Файлы логов приложения.
public/ Содержит статичные файлы и скомпилированные ассеты. Когда ваше приложение запущено, эта директория будет представлена как есть.
Rakefile Этот файл находит и загружает задачи, которые могут быть запущены в командной строке. Определенная задача доступна во всех компонентах Rails. Вместо изменения Rakefile, можно добавить свои собственные задачи, добавив файлы в директорию lib/tasks приложения.
README.md Это вводный мануал для вашего приложения. Его следует отредактировать, чтобы рассказать остальным, что ваше приложение делает, как его настроить, и т.п.
storage/ Файлы Active Storage для сервиса Disk. Это раскрывается в руководстве Обзор Active Storage.
test/ Юнит-тесты, фикстуры и прочий аппарат тестирования. Это раскрывается в руководстве Тестирование приложений на Rails
tmp/ Временные файлы (такие как файлы кэша и pid)
vendor/ Место для кода сторонних разработчиков. В типичном приложении на Rails включает внешние гемы.
.gitattributes Этот файл определяет метаданные для определенных путей в репозитории git. Эти метаданные могут быть использованы git и другими инструментами для улучшения их поведения. Подробности смотрите в документации gitattributes.
.gitignore Этот файл сообщает git, какие файлы (явно или по шаблону) ему следует игнорировать. Подробнее об игнорировании файлов смотрите GitHub - Ignoring files.
.ruby-version Этот файл содержит дефолтную версию Ruby.

4. Hello, Rails!

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

4.1. Запуск веб-сервера

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

$ bin/rails server

Если вы используете Windows, вы должны передавать скрипты из папки bin непосредственно в интерпретатор Ruby, то есть ruby bin\rails server.

Сжатие ассетов JavaScript требует среды выполнения JavaScript в вашей системе, и его отсутствие приведет к ошибке execjs во время сжатия ассетов. Обычно macOS и Windows поставляются с установленной средой выполнения JavaScript. therubyrhino - рекомендованная среда выполнения для пользователей JRuby, она добавляется в Gemfile, если приложение генерируется под JRuby. Можно узнать все о поддерживаемых средах выполнения в ExecJS

Это запустит Puma, веб-сервер, распространяющийся с Rails по умолчанию. Чтобы увидеть приложение в действии, откройте окно браузера и пройдите по адресу http://localhost:3000. Вы должны увидеть дефолтную информационную страницу Rails:

скриншот стартовой страницы Rails

Когда захотите остановить веб-сервер, нажмите Ctrl+C в терминале, где он запущен. В среде development, Rails в основном не требует остановки сервера; все изменения, которые Вы делаете в файлах, автоматически подхватываются сервером.

Стартовая страница Rails это smoke test для нового приложения на Rails: она показывает, что ваши программы настроены достаточно правильно для отображения страницы.

4.2. Скажите "привет", Рельсы

Чтобы Rails сказал "Привет", нужно создать, как минимум, маршрут, контроллер с экшном и вью (представление). Маршрут связывает запрос с экшном контроллера. Экшн контроллера выполняет необходимую работу для обработки запроса и подготавливает необходимые данные для вью. Вью отображает данные в желаемом формате.

В терминах реализации: Маршруты это правила, написанные на Ruby DSL (предметно-ориентированном языке). Контроллеры — это классы Ruby, и их публичные методы — это экшны. И вью — это шаблоны, обычно написанные на смеси HTML и Ruby.

Давайте начнем с добавления маршрута к нашему файлу маршрутов, config/routes.rb, в самом верху блока Rails.application.routes.draw:

Rails.application.routes.draw do
  get "/articles", to: "articles#index"

  # О подробностях DSL, доступного в этом файле, написано в http://rusrails.ru/routing
end

Вышеприведенный маршрут объявляет, что запросы GET /articles связываются с экшном index в ArticlesController.

Для создания ArticlesController и его экшна index, мы запустим генератор контроллера (с опцией --skip-routes, так как у нас уже есть подходящий маршрут):

$ bin/rails generate controller Articles index --skip-routes

Rails создаст несколько файлов.

create  app/controllers/articles_controller.rb
invoke  erb
create    app/views/articles
create    app/views/articles/index.html.erb
invoke  test_unit
create    test/controllers/articles_controller_test.rb
invoke  helper
create    app/helpers/articles_helper.rb
invoke    test_unit

Наиболее важным из них является файл контроллера, app/controllers/articles_controller.rb. Давайте посмотрим на него:

class ArticlesController < ApplicationController
  def index
  end
end

Экшн index пустой. Когда экшн не рендерит явно вью (или иным способом создает отклик HTTP), Rails автоматически отрендерит вью, соответствующую имени контроллера и экшну. Convention Over Configuration! Вью располагаются в директории app/views. Таким образом, экшн index по умолчанию отрендерит app/views/articles/index.html.erb.

Давайте откроем файл app/views/articles/index.html.erb и заменим его содержимое на:

<h1>Hello, Rails!</h1>

Если до этого вы остановили веб сервер для запуска генератора контроллера, перезапустите его с помощью bin/rails server. Теперь посетите http://localhost:3000/articles, чтобы увидеть наш текст отображенным!

4.3. Настройка домашней страницы приложения

В настоящий момент http://localhost:3000 все еще отображает страницу с логотипом Ruby on Rails. Давайте также отобразим наш текст "Hello, Rails!" на http://localhost:3000. Для этого мы добавим маршрут, соответствующий корневому пути нашего приложения к соответствующему контроллеру и экшну.

Откройте config/routes.rb и добавьте следующий маршрут root вверху блока Rails.application.routes.draw:

Rails.application.routes.draw do
  root "articles#index"

  get "/articles", to: "articles#index"
end

Теперь можно видеть наш текст "Hello, Rails!" при посещении http://localhost:3000, что подтверждает то, что маршрут root также связан с экшном index в ArticlesController.

Чтобы узнать больше о роутинге, обратитесь к руководству Роутинг в Rails.

5. Автоматическая загрузка

Приложения Rails не используют require для загрузки кода приложения.

Вы, возможно, заметили, что ArticlesController наследуется от ApplicationController, но app/controllers/articles_controller.rb не содержит чего-то наподобие

require "application_controller" # НЕ ДЕЛАЙТЕ ТАК.

Классы и модули приложения доступны отовсюду, вам не нужно и не следует загружать что-то из app с помощью require. Эта особенность называется автоматическая загрузка, и о ней можно узнать подробнее в Автозагрузка и перезагрузка констант.

Вызовы require нужны только в двух случаях:

  • Чтобы загружать файлы из директории lib.
  • Чтобы загружать зависимости гемов, у которых require: false в Gemfile.

6. Вы и MVC

Только что мы обсудили маршруты, контроллеры, экшны и вью. Это все типичные части веб приложения, соответствующего паттерну MVC (Model-View-Controller). MVC это шаблон проектирования, разделяющий ответственности приложения для его упрощения. Rails по соглашению следует этому шаблону проектирования.

Поскольку у нас уже есть контроллер и вью, давайте сгенерируем следующую часть: модель.

6.1. Генерация модели

Модель это класс Ruby, используемый для представления данных. Кроме этого, модели могут взаимодействовать с базой данных приложения с помощью особенности Rails, называемой Active Record.

Для определения модели используем генератор модели:

$ bin/rails generate model Article title:string body:text

Имена моделей в единственном числе, так как инициализированная модель представляет единственную запись данных. Чтобы запомнить это соглашение, думайте о том, как вы хотели вызвать конструктор модели: мы хотим писать Article.new(...), а не Articles.new(...).

Это создаст несколько файлов:

invoke  active_record
create    db/migrate/<timestamp>_create_articles.rb
create    app/models/article.rb
invoke    test_unit
create      test/models/article_test.rb
create      test/fixtures/articles.yml

Два файла, на которых мы сфокусируемся, это файл миграции (db/migrate/<timestamp>_create_articles.rb) и файл модели (app/models/article.rb).

6.2. Миграции базы данных

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

Давайте посмотрим на содержимое нового файла миграции:

class CreateArticles < ActiveRecord::Migration[7.0]
  def change
    create_table :articles do |t|
      t.string :title
      t.text :body

      t.timestamps
    end
  end
end

Вызов create_table указывает, как должна быть сконструирована таблица articles. По умолчанию метод create_table добавляет столбец id в качестве автоматически увеличивающегося первичного ключа. Таким образом, у первой записи в таблице будет id 1, у следующей записи id 2, и так далее.

В блоке для create_table определены два столбца: title и body. Они были добавлены генератором, так как мы включили их в команду генерации (bin/rails generate model Article title:string body:text).

В последней строчке блока вызывается t.timestamps. Этот метод определяет два дополнительных столбца с именами created_at и updated_at. Как мы увидим, Rails позаботится о них, устанавливая значения при создании или обновлении объекта модели.

Давайте запустим нашу миграцию с помощью следующей команды:

$ bin/rails db:migrate

Эта команда выведет, что таблица была создана:

==  CreateArticles: migrating ===================================
-- create_table(:articles)
   -> 0.0018s
==  CreateArticles: migrated (0.0018s) ==========================

Чтобы узнать больше о миграциях, смотрите Миграции Active Record.

Теперь мы можем взаимодействовать с этой таблицей с помощью нашей модели.

6.3. Использование модели для взаимодействия с базой данных

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

Давайте запустим консоль с помощью команды:

$ bin/rails console

Вы должны увидеть интерфейс irb наподобие:

Loading development environment (Rails 7.0.0)
irb(main):001:0>

В этом интерфейсе можно инициализировать новый объект Article:

irb> article = Article.new(title: "Hello Rails", body: "I am on Rails!")

Важно отметить, что мы только инициализировали этот объект. Этот объект вообще не сохранен в базе данных. В настоящий момент он доступен только в консоли. Чтобы сохранить его в базу данных, нужно вызвать save:

irb> article.save
(0.1ms)  begin transaction
Article Create (0.4ms)  INSERT INTO "articles" ("title", "body", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["title", "Hello Rails"], ["body", "I am on Rails!"], ["created_at", "2020-01-18 23:47:30.734416"], ["updated_at", "2020-01-18 23:47:30.734416"]]
(0.9ms)  commit transaction
=> true

Вышеуказанный вывод показывает запрос базы данных INSERT INTO "articles" .... Это показывает, что статья была вставлена в нашу таблицу. И если мы снова взглянем на объект article, то увидим, что произошло нечто интересное:

irb> article
=> #<Article id: 1, title: "Hello Rails", body: "I am on Rails!", created_at: "2020-01-18 23:47:30", updated_at: "2020-01-18 23:47:30">

Теперь у объекта установлены атрибуты id, created_at и updated_at. Rails сделал это, когда мы сохранили объект.

Когда мы хотим извлечь эту статью из базы данных, можно вызвать find на модели и передать id в качестве аргумента:

irb> Article.find(1)
=> #<Article id: 1, title: "Hello Rails", body: "I am on Rails!", created_at: "2020-01-18 23:47:30", updated_at: "2020-01-18 23:47:30">

И когда мы хотим извлечь все статьи из базы данных, можно вызвать all на модели:

irb> Article.all
=> #<ActiveRecord::Relation [#<Article id: 1, title: "Hello Rails", body: "I am on Rails!", created_at: "2020-01-18 23:47:30", updated_at: "2020-01-18 23:47:30">]>

Этот метод возвращает объект ActiveRecord::Relation, о котором можно думать, как о массиве с особыми способностями.

Чтобы узнать больше о моделях, смотрите Основы Active Record и Интерфейс запросов Active Record.

Модели это последний кусочек пазла MVC. Далее мы соединим все кусочки вместе.

6.4. Отображение списка статей

Давайте вернемся к нашему контроллеру в app/controllers/articles_controller.rb и изменим экшн index, чтобы он извлекал все статьи из базы данных:

class ArticlesController < ApplicationController
  def index
    @articles = Article.all
  end
end

К переменным экземпляра контроллера можно получить доступ из вью. Это означает, что мы можем ссылаться на @articles в app/views/articles/index.html.erb. Давайте откроем этот файл и заменим его содержимое на:

<h1>Articles</h1>

<ul>
  <% @articles.each do |article| %>
    <li>
      <%= article.title %>
    </li>
  <% end %>
</ul>

Вышеприведенный код это смесь HTML и ERB. ERB это система шаблонирования, которая вычисляет код Ruby, вложенный в документ. Тут мы видим два типа тегов ERB: <% %> и <%= %>. Тег <% %> означает "вычислить заключенный код Ruby". Тег <%= %> означает "вычислить заключенный код Ruby и вывести значение, которое он возвратит". Все, что можно написать в обычной программе на Ruby, можно вложить в эти теги ERB, хотя обычно лучше сохранять содержимое тегов ERB кратким, для читаемости.

Так как мы не хотим вывести значение, возвращаемое @articles.each, мы заключили этот код в <% %>. Но, поскольку мы хотим вывести значение, возвращаемое article.title (для каждой статьи), мы заключили этот код в <%= %>.

Окончательный результат можно увидеть, посетив http://localhost:3000. (Помните, что bin/rails server должен быть запущен!) Вот что произойдет при этом:

  • Браузер сделает запрос: GET http://localhost:3000.
  • Наше приложение Rails получит этот запрос.
  • Роутер Rails свяжет корневой маршрут с экшном index в ArticlesController.
  • Экшн index использует модель Article для извлечения всех статей из базы данных.
  • Rails автоматически отрендерит вью app/views/articles/index.html.erb.
  • Вычисляется код ERB во вью для вывода HTML.
  • Сервер отправит отклик, содержащий HTML, обратно браузеру.

Мы соединили все кусочки MVC вместе, и теперь у нас есть наш первый экшн контроллера! Далее мы двинемся ко второму экшну.

7. Используйте CRUD там, где нужно использовать CRUD

Почти все веб приложения используют операции CRUD (Create, Read, Update, and Delete). Можно обнаружить, что большая часть работы вашего приложения это CRUD. Rails знает об этом и предоставляет много особенностей для упрощения кода, осуществляющего CRUD.

Давайте исследуем эти возможности, добавив больше функционала в наше приложение.

7.1. Отображение отдельной статьи

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

Начнем с добавления нового маршрута, направляющего на новый экшн контроллера (который затем тоже добавим). Откройте config/routes.rb и вставьте последний маршрут, показанный тут:

Rails.application.routes.draw do
  root "articles#index"

  get "/articles", to: "articles#index"
  get "/articles/:id", to: "articles#show"
end

Новый маршрут является еще одним маршрутом get, но есть кое-что дополнительное в его пути: :id. Это означает параметр маршрута. Параметр маршрута захватывает сегмент пути запроса, и кладет это значение в хэш params, доступный в экшне контроллера. Например, при обработке запроса GET http://localhost:3000/articles/1, 1 будет захвачено как значение для :id, и будет доступно как params[:id] в экшне show в ArticlesController.

Теперь давайте добавим этот экшн show, ниже экшна index в app/controllers/articles_controller.rb:

class ArticlesController < ApplicationController
  def index
    @articles = Article.all
  end

  def show
    @article = Article.find(params[:id])
  end
end

Экшн show вызывает Article.find (упомянутый ранее) с ID, захваченным параметром маршрута. Возвращаемая статья хранится в переменной экземпляра @article, поэтому она доступна во вью. По умолчанию экшн show отрендерит app/views/articles/show.html.erb.

Давайте создадим app/views/articles/show.html.erb со следующим содержимым:

<h1><%= @article.title %></h1>

<p><%= @article.body %></p>

Теперь можно увидеть статью при посещении http://localhost:3000/articles/1!

В завершении, давайте добавим удобный способ перейти на страницу статьи. Мы свяжем заголовок каждой статьи в app/views/articles/index.html.erb с ее страницей:

<h1>Articles</h1>

<ul>
  <% @articles.each do |article| %>
    <li>
      <a href="/articles/<%= article.id %>">
        <%= article.title %>
      </a>
    </li>
  <% end %>
</ul>

7.2. Ресурсный роутинг

К этому моменту мы раскрыли "R" (Read) из CRUD. Со временем мы раскроем "C" (Create), "U" (Update) и "D" (Delete). Как вы, возможно, догадались, мы сделаем это, добавив новые маршруты, экшны контроллера и вью. Всякий раз, когда у нас есть такая комбинация маршрутов, экшнов контроллера и вью, работающих вместе для выполнения операций CRUD на сущности, мы называем эту сущность ресурсом. Например, в нашем приложении, мы бы назвали статью ресурсом.

Rails предоставляет маршрутный метод resources, который связывает все общепринятые маршруты для коллекции ресурсов, таких как статьи. Поэтому, до того, как мы перейдем к разделам "C", "U" и "D", давайте заменим два маршрута get в config/routes.rb на resources:

Rails.application.routes.draw do
  root "articles#index"

  resources :articles
end

Можно посмотреть, какие маршруты связаны, запустив команду bin/rails routes:

$ bin/rails routes
      Prefix Verb   URI Pattern                  Controller#Action
        root GET    /                            articles#index
    articles GET    /articles(.:format)          articles#index
 new_article GET    /articles/new(.:format)      articles#new
     article GET    /articles/:id(.:format)      articles#show
             POST   /articles(.:format)          articles#create
edit_article GET    /articles/:id/edit(.:format) articles#edit
             PATCH  /articles/:id(.:format)      articles#update
             DELETE /articles/:id(.:format)      articles#destroy

Метод resources также настраивает вспомогательные методы URL и путей, которые можно использовать для предотвращения зависимостей нашего кода от настроек определенного маршрута. Значения выше в столбце "Prefix" плюс суффикс _url или _path формируют имена этих хелперов. Например, хелпер article_path возвращает "/articles/#{article.id}" для заданной статьи. Его можно использовать для приведения в порядок наших ссылок в app/views/articles/index.html.erb:

<h1>Articles</h1>

<ul>
  <% @articles.each do |article| %>
    <li>
      <a href="<%= article_path(article) %>">
        <%= article.title %>
      </a>
    </li>
  <% end %>
</ul>

Однако, мы сделаем еще один шаг вперед и используем хелпер link_to. Хелпер link_to создает ссылку с первым аргументом в качестве текста ссылки и вторым аргументом в качестве адреса ссылки. Если мы передадим объект модели как второй аргумент, link_to вызовет подходящий хелпер пути для преобразования объекта в путь. Например, если мы передадим статью, link_to вызовет article_path. Таким образом, app/views/articles/index.html.erb станет:

<h1>Articles</h1>

<ul>
  <% @articles.each do |article| %>
    <li>
      <%= link_to article.title, article %>
    </li>
  <% end %>
</ul>

Отлично!

Подробнее о роутинге смотрите Роутинг в Rails.

7.3. Создание новой статьи

Теперь мы подходим к "C" (Create) из CRUD. Обычно, в веб приложениях, создание нового ресурса это многошаговый процесс. Сначала пользователь запрашивает форму для заполнения. Затем пользователь отправляет форму. Если нет ошибок, то ресурс создается, и отображается некоторое подтверждение. Иначе, форма снова отображается с сообщениями об ошибке, и процесс повторяется.

В приложении Rails эти шаги традиционно обрабатываются экшнами контроллера new и create. Давайте добавим типичную реализацию этих экшнов в app/controllers/articles_controller.rb, ниже экшна show:

class ArticlesController < ApplicationController
  def index
    @articles = Article.all
  end

  def show
    @article = Article.find(params[:id])
  end

  def new
    @article = Article.new
  end

  def create
    @article = Article.new(title: "...", body: "...")

    if @article.save
      redirect_to @article
    else
      render :new, status: :unprocessable_entity
    end
  end
end

Экшн new инициализирует новую статью, но не сохраняет ее. Эта статья будет использована во вью при построении формы. По умолчанию экшн new будет рендерить app/views/articles/new.html.erb, которую мы создадим далее.

Экшн create инициализирует новую статью со значениями для заголовка и содержимого и пытается сохранить ее. Если статью успешно сохранена, экшн перенаправляет браузер на страницу статьи "http://localhost:3000/articles/#{@article.id}". Иначе экшн отображает форму заново, отрендерив app/views/articles/new.html.erb с кодом статуса 422 Unprocessable Entity. Тут title и body фиктивные значения. После того, как мы создадим форму, мы вернемся и изменим их.

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

7.3.1. Использование построителя форм

Мы будем использовать особенность Rails, называемую form builder, для создания нашей формы. Используя построитель форм, можно, используя минимальное количество кода, вывести полностью настроенную форму, следующую соглашениям Rails.

Давайте создадим app/views/articles/new.html.erb со следующим содержимым:

<h1>New Article</h1>

<%= form_with model: @article do |form| %>
  <div>
    <%= form.label :title %><br>
    <%= form.text_field :title %>
  </div>

  <div>
    <%= form.label :body %><br>
    <%= form.text_area :body %>
  </div>

  <div>
    <%= form.submit %>
  </div>
<% end %>

Вспомогательный метод form_with инициализирует построитель форм. В блоке form_with мы вызываем на построителе форм методы, такие как label и text_field для вывода подходящих элементов формы.

Результирующий вывод от вызова form_with будет выглядеть так:

<form action="/articles" accept-charset="UTF-8" method="post">
  <input type="hidden" name="authenticity_token" value="...">

  <div>
    <label for="article_title">Title</label><br>
    <input type="text" name="article[title]" id="article_title">
  </div>

  <div>
    <label for="article_body">Body</label><br>
    <textarea name="article[body]" id="article_body"></textarea>
  </div>

  <div>
    <input type="submit" name="commit" value="Create Article" data-disable-with="Create Article">
  </div>
</form>

Чтобы подробнее узнать о построителях форм, смотрите Хелперы форм в Action View.

7.3.2. Использование Strong Parameters

Отправленные данные формы вкладываются в хэш params, вместе с захваченными параметрами маршрута. Таким образом, экшн create имеет доступ к отправленному заголовку как params[:article][:title] и к отправленному содержимому как params[:article][:body]. Эти значения можно передать в Article.new отдельно, но это может быть некрасиво и подвержено ошибкам. И это будет еще хуже, когда мы добавим больше полей.

Вместо этого, мы передадим единственный хэш, содержащий значения. Однако, мы все еще должны указать, какие значения допустимы в этом хэше. В противном случае, злоумышленник потенциально может отправить дополнительные поля формы и перезаписать конфиденциальные данные. Фактически, если мы передадим нефильтрованный хэш params[:article] непосредственно в Article.new, Rails вызовет ForbiddenAttributesError, чтобы предупредить нас о проблеме. Таким образом, мы будем использовать особенность Rails, названную Strong Parameters, для фильтрации params. Представьте это как сильную типизацию для params.

Давайте добавим приватный метод внизу app/controllers/articles_controller.rb, названный article_params, фильтрующий params. И давайте изменим create, чтобы использовать его:

class ArticlesController < ApplicationController
  def index
    @articles = Article.all
  end

  def show
    @article = Article.find(params[:id])
  end

  def new
    @article = Article.new
  end

  def create
    @article = Article.new(article_params)

    if @article.save
      redirect_to @article
    else
      render :new, status: :unprocessable_entity
    end
  end

  private
    def article_params
      params.require(:article).permit(:title, :body)
    end
end

Чтобы подробнее узнать о Strong Parameters, смотрите Обзор Action Controller § Strong Parameters.

7.3.3. Валидации и отображение сообщений об ошибке

Как мы видели, создание ресурса это многоступенчатый процесс. Обработка неправильного пользовательского ввода это еще один шаг этого процесса. Rails предоставляет особенность валидации, чтобы помочь нам разобраться с неправильным пользовательским вводом. Валидации это правила, проверяемые до сохранения объекта модели. Если любая из проверок провалится, сохранение будет прервано, и соответствующие сообщения об ошибке будут добавлены в атрибут errors у объекта модели.

Давайте добавим некоторые валидации в нашу модель в app/models/article.rb:

class Article < ApplicationRecord
  validates :title, presence: true
  validates :body, presence: true, length: { minimum: 10 }
end

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

Вторая валидация объявляет, что значение body также должно присутствовать. Кроме этого, она объявляет, что длина значения body должно быть как минимум 10 символов.

Возможно, вам интересно, где определены атрибуты title и body. Active Record автоматически определяет атрибуты модели для каждого столбца таблицы, таким образом, не нужно объявлять эти атрибуты в файле модели.

С имеющимися валидациями, давайте изменим app/views/articles/new.html.erb для отображения любых сообщений об ошибке для title и body:

<h1>New Article</h1>

<%= form_with model: @article do |form| %>
  <div>
    <%= form.label :title %><br>
    <%= form.text_field :title %>
    <% @article.errors.full_messages_for(:title).each do |message| %>
      <div><%= message %></div>
    <% end %>
  </div>

  <div>
    <%= form.label :body %><br>
    <%= form.text_area :body %><br>
    <% @article.errors.full_messages_for(:body).each do |message| %>
      <div><%= message %></div>
    <% end %>
  </div>

  <div>
    <%= form.submit %>
  </div>
<% end %>

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

Чтобы понять, как это все работает, давайте снова взглянем на экшны new и create контроллера:

  def new
    @article = Article.new
  end

  def create
    @article = Article.new(article_params)

    if @article.save
      redirect_to @article
    else
      render :new, status: :unprocessable_entity
    end
  end

При посещении http://localhost:3000/articles/new, запрос GET /articles/new направляется на экшн new. Экшн new не пытается сохранить @article. Следовательно, валидации не проверяются, и сообщений об ошибке не будет.

При отправке формы, запрос POST /articles направляется на экшн create. Экшн create пытается сохранить @article. Следовательно, валидации проверяются. Если любая из валидаций падает, @article не будет сохранена, и app/views/articles/new.html.erb будет отрендерена с сообщением об ошибке.

Чтобы больше узнать о валидациях, обратитесь к Валидации Active Record. Чтобы больше узнать о сообщениях об ошибке валидации, обратитесь к Валидации Active Record § Работаем с ошибками валидации.

7.3.4. Завершаем начатое

Теперь мы можем создать статью, посетив http://localhost:3000/articles/new. Для завершения давайте добавим ссылку на эту страницу внизу app/views/articles/index.html.erb:

<h1>Articles</h1>

<ul>
  <% @articles.each do |article| %>
    <li>
      <%= link_to article.title, article %>
    </li>
  <% end %>
</ul>

<%= link_to "New Article", new_article_path %>

7.4. Обновление статьи

Мы покрыли "CR" от CRUD. Теперь перейдем к "U" (Update). Обновление ресурса очень похоже на создание ресурса. Они оба многоступенчатые процессы. Во-первых, пользователь запрашивает форму для редактирования данных. Затем пользователь отправляет форму. Затем, если не было ошибок, ресурс обновляется. В противном случае, форма снова отображается с сообщениями об ошибке, и процесс повторяется.

По соглашениям, эти шаги обрабатываются экшнами edit и update контроллера. Давайте добавим типичную реализацию этих экшнов в app/controllers/articles_controller.rb, ниже экшнаcreate:

class ArticlesController < ApplicationController
  def index
    @articles = Article.all
  end

  def show
    @article = Article.find(params[:id])
  end

  def new
    @article = Article.new
  end

  def create
    @article = Article.new(article_params)

    if @article.save
      redirect_to @article
    else
      render :new, status: :unprocessable_entity
    end
  end

  def edit
    @article = Article.find(params[:id])
  end

  def update
    @article = Article.find(params[:id])

    if @article.update(article_params)
      redirect_to @article
    else
      render :edit, status: :unprocessable_entity
    end
  end

  private
    def article_params
      params.require(:article).permit(:title, :body)
    end
end

Обратите внимание, как похожи экшны edit и update на экшны new и create.

Экшн edit извлекает статью из базы данных и сохраняет ее в @article, таким образом ее можно использовать при построении формы. По умолчанию экшн edit отрендерит app/views/articles/edit.html.erb.

Экшн update (пере)извлекает статью из базы данных и пытается обновить ее с помощью отправленных данных формы, фильтрованных в article_params. Если ни одна валидация не упадет, и обновление будет успешным, этот экшн перенаправит браузер на страницу статьи. В противном случае, экшн повторно отобразит форму - с сообщениями об ошибке - отрендерив app/views/articles/edit.html.erb.

7.5. Использование партиалов для совместного использования кода вью

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

Так как код будет тем же самым, мы собираемся рефакторить его в совместную вью, называемую партиал. Давайте создадим app/views/articles/_form.html.erb со следующим содержимым:

<%= form_with model: article do |form| %>
  <div>
    <%= form.label :title %><br>
    <%= form.text_field :title %>
    <% article.errors.full_messages_for(:title).each do |message| %>
      <div><%= message %></div>
    <% end %>
  </div>

  <div>
    <%= form.label :body %><br>
    <%= form.text_area :body %><br>
    <% article.errors.full_messages_for(:body).each do |message| %>
      <div><%= message %></div>
    <% end %>
  </div>

  <div>
    <%= form.submit %>
  </div>
<% end %>

Вышеприведенный код тот же самый, что и форма в app/views/articles/new.html.erb, за исключением того, что все случаи @article заменены на article. Так как партиалы являются совместным кодом, хорошим тоном является не делать их зависимыми от определенных переменных экземпляра, установленных экшном контроллера. Вместо этого, мы передадим статью в партиал как локальную переменную.

Давайте обновим app/views/articles/new.html.erb, чтобы использовать этот партиал с помощью render:

<h1>New Article</h1>

<%= render "form", article: @article %>

Имя файла партиала должно начинаться на подчеркивание, т.е. _form.html.erb. Но при рендеринге ссылаемся на него без подчеркивания, т.е. render "form".

И теперь давайте создадим очень похожий app/views/articles/edit.html.erb:

<h1>Edit Article</h1>

<%= render "form", article: @article %>

Чтобы подробнее узнать о партиалах, обратитесь к Макеты и рендеринг в Rails § Использование партиалов.

7.5.1. Завершаем начатое

Теперь мы можем обновлять статью, посетив страницу ее редактирования, т.е. http://localhost:3000/articles/1/edit. Для завершения, давайте добавим ссылку на страницу редактирования внизу app/views/articles/show.html.erb:

<h1><%= @article.title %></h1>

<p><%= @article.body %></p>

<ul>
  <li><%= link_to "Edit", edit_article_path(@article) %></li>
</ul>

7.6. Удаление статьи

Наконец, мы добрались до "D" (Delete) из CRUD. Удаление ресурса это более простой процесс, чем создание или обновление. Он требует только маршрут и экшн контроллера. И наш ресурсный роутинг (resources :articles) уже предоставляет этот маршрут, связывающий запросы DELETE /articles/:id с экшном destroy в ArticlesController.

Итак, давайте добавим типичный экшн destroy в app/controllers/articles_controller.rb ниже экшна update:

class ArticlesController < ApplicationController
  def index
    @articles = Article.all
  end

  def show
    @article = Article.find(params[:id])
  end

  def new
    @article = Article.new
  end

  def create
    @article = Article.new(article_params)

    if @article.save
      redirect_to @article
    else
      render :new, status: :unprocessable_entity
    end
  end

  def edit
    @article = Article.find(params[:id])
  end

  def update
    @article = Article.find(params[:id])

    if @article.update(article_params)
      redirect_to @article
    else
      render :edit, status: :unprocessable_entity
    end
  end

  def destroy
    @article = Article.find(params[:id])
    @article.destroy

    redirect_to root_path, status: :see_other
  end

  private
    def article_params
      params.require(:article).permit(:title, :body)
    end
end

Экшн destroy извлекает статью из базы данных, и вызывает destroy на ней. Затем он перенаправляет браузер на корневой путь со статусом кода 303 See Other.

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

Теперь давайте добавим ссылку внизу app/views/articles/show.html.erb, чтобы мы могли удалить статью с ее страницы:

<h1><%= @article.title %></h1>

<p><%= @article.body %></p>

<ul>
  <li><%= link_to "Edit", edit_article_path(@article) %></li>
  <li><%= link_to "Destroy", article_path(@article), data: {
                    turbo_method: :delete,
                    turbo_confirm: "Are you sure?"
                  } %></li>
</ul>

В вышеприведенном коде мы используем опцию data для установки HTML-атрибутов data-turbo-method и data-turbo-confirm ссылки "Destroy". Оба этих атрибута подключаются к Turbo, который по умолчанию включен в новые приложения Rails. data-turbo-method="delete" заставит ссылку сделать запрос DELETE вместо запроса GET. data-turbo-confirm="Are you sure?" приведет к появлению диалогового окна подтверждения при нажатии на ссылку. Если пользователь отменит диалог, запрос будет прерван.

Вот и все! Теперь можно просматривать, создавать, обновлять и удалять статьи!

8. Добавляем вторую модель

Настало время добавить вторую модель в приложение. Вторая модель будет обрабатывать комментарии к статьям.

8.1. Генерируем модель

Мы намереваемся использовать тот же генератор, что мы использовали ранее при создании модели Article. В этот раз мы создадим модель Comment, содержащую ссылку на статью. Запустите следующую команду в терминале:

$ bin/rails generate model Comment commenter:string body:text article:references

Эта команда генерирует четыре файла:

Файл Назначение
db/migrate/20140120201010_create_comments.rb Миграция для создания таблицы comments в вашей базе данных (ваше имя файла будет включать другую временную метку)
app/models/comment.rb Модель Comment
test/models/comment_test.rb Каркас для тестирования модели комментария
test/fixtures/comments.yml Образцы комментариев для использования в тестировании

Сначала взглянем на app/models/comment.rb:

class Comment < ApplicationRecord
  belongs_to :article
end

Это очень похоже на модель Article, которую мы видели ранее. Разница в строчке belongs_to :article, которая устанавливает связь Active Record. Вы ознакомитесь со связями в следующем разделе руководства.

Ключевое слово (:references), использованное в команде консоли, это специальный тип данных для моделей. Он создает новый столбец в вашей базе данных с именем представленной модели с добавленным _id, который может содержать числовые значения. Чтобы лучше понять, проанализируйте файл db/schema.rb после выполнения миграции.

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

class CreateComments < ActiveRecord::Migration[7.0]
  def change
    create_table :comments do |t|
      t.string :commenter
      t.text :body
      t.references :article, null: false, foreign_key: true

      t.timestamps
    end

  end
end

Строчка t.references создает числовой столбец с именем article_id, индекс для него, и ограничение внешнего ключа, указывающего на столбец id таблицы articles. Далее запускаем миграцию:

$ bin/rails db:migrate

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

==  CreateComments: migrating =================================================
-- create_table(:comments)
   -> 0.0115s
==  CreateComments: migrated (0.0119s) ========================================

8.2. Связываем модели

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

  • Каждый комментарий принадлежит одной статье.
  • Одна статья может иметь много комментариев.

Фактически, это очень близко к синтаксису, который использует Rails для объявления этой связи. Вы уже видели строчку кода в модели Comment (app/models/comment.rb), которая делает каждый комментарий принадлежащим статье:

class Comment < ApplicationRecord
  belongs_to :article
end

Вам нужно отредактировать app/models/article.rb, добавив другую сторону связи:

class Article < ApplicationRecord
  has_many :comments

  validates :title, presence: true
  validates :body, presence: true, length: { minimum: 10 }
end

Эти два объявления автоматически делают доступным большое количество возможностей. Например, если у вас есть переменная экземпляра @article, содержащая статью, вы можете получить все комментарии, принадлежащие этой статье, в массиве, вызвав @article.comments.

Более подробно о связях Active Record смотрите руководство Связи (ассоциации) Active Record.

8.3. Добавляем маршрут для комментариев

Как в случае с контроллером articles, нам нужно добавить маршрут, чтобы Rails знал, по какому адресу мы хотим пройти, чтобы увидеть комментарии. Снова откройте файл config/routes.rb и отредактируйте его следующим образом:

Rails.application.routes.draw do
  root "articles#index"

  resources :articles do
    resources :comments
  end
end

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

Более подробно о роутинге написано в руководстве Роутинг в Rails.

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

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

$ bin/rails generate controller Comments

Создадутся три файла и пустая директория:

Файл/Директория Назначение
app/controllers/comments_controller.rb Контроллер Comments
app/views/comments/ Вью контроллера хранятся здесь
test/controllers/comments_controller_test.rb Тест для контроллера
app/helpers/comments_helper.rb Хелпер для вью

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

Сначала мы расширим шаблон Article show (app/views/articles/show.html.erb), чтобы он позволял добавить новый комментарий:

<h1><%= @article.title %></h1>

<p><%= @article.body %></p>

<ul>
  <li><%= link_to "Edit", edit_article_path(@article) %></li>
  <li><%= link_to "Destroy", article_path(@article), data: {
                    turbo_method: :delete,
                    turbo_confirm: "Are you sure?"
                  } %></li>
</ul>

<h2>Add a comment:</h2>
<%= form_with model: [ @article, @article.comments.build ] do |form| %>
  <p>
    <%= form.label :commenter %><br>
    <%= form.text_field :commenter %>
  </p>
  <p>
    <%= form.label :body %><br>
    <%= form.text_area :body %>
  </p>
  <p>
    <%= form.submit %>
  </p>
<% end %>

Это добавит форму на страницу отображения статьи, создающую новый комментарий при вызове экшна create в CommentsController. Тут вызов form_with использует массив, что создаст вложенный маршрут, такой как /articles/1/comments.

Давайте напишем create в app/controllers/comments_controller.rb:

class CommentsController < ApplicationController
  def create
    @article = Article.find(params[:article_id])
    @comment = @article.comments.create(comment_params)
    redirect_to article_path(@article)
  end

  private
    def comment_params
      params.require(:comment).permit(:commenter, :body)
    end
end

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

Кроме того, код пользуется преимуществом некоторых методов, доступных для связей. Мы используем метод create на @article.comments, чтобы создать и сохранить комментарий. Это автоматически связывает комментарий так, что он принадлежит к определенной статье.

Как только мы создали новый комментарий, мы возвращаем пользователя обратно на оригинальную статью, используя хелпер article_path(@article). Как мы уже видели, он вызывает экшн show в ArticlesController, который, в свою очередь, рендерит шаблон show.html.erb. В этом месте мы хотим отображать комментарии, поэтому давайте добавим следующее в app/views/articles/show.html.erb.

<h1><%= @article.title %></h1>

<p><%= @article.body %></p>

<ul>
  <li><%= link_to "Edit", edit_article_path(@article) %></li>
  <li><%= link_to "Destroy", article_path(@article), data: {
                    turbo_method: :delete,
                    turbo_confirm: "Are you sure?"
                  } %></li>
</ul>

<h2>Comments</h2>
<% @article.comments.each do |comment| %>
  <p>
    <strong>Commenter:</strong>
    <%= comment.commenter %>
  </p>

  <p>
    <strong>Comment:</strong>
    <%= comment.body %>
  </p>
<% end %>

<h2>Add a comment:</h2>
<%= form_with model: [ @article, @article.comments.build ] do |form| %>
  <p>
    <%= form.label :commenter %><br>
    <%= form.text_field :commenter %>
  </p>
  <p>
    <%= form.label :body %><br>
    <%= form.text_area :body %>
  </p>
  <p>
    <%= form.submit %>
  </p>
<% end %>

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

Статья с комментариями

9. Рефакторинг

Теперь, когда у нас есть работающие статьи и комментарии, взглянем на шаблон app/views/articles/show.html.erb. Он стал длинным и неудобным. Давайте воспользуемся партиалами, чтобы разгрузить его.

9.1. Рендеринг коллекций партиалов

Сначала сделаем партиал для комментариев, показывающий все комментарии для статьи. Создайте файл app/views/comments/_comment.html.erb и поместите в него следующее:

<p>
  <strong>Commenter:</strong>
  <%= comment.commenter %>
</p>

<p>
  <strong>Comment:</strong>
  <%= comment.body %>
</p>

Затем можно изменить app/views/articles/show.html.erb вот так:

<h1><%= @article.title %></h1>

<p><%= @article.body %></p>

<ul>
  <li><%= link_to "Edit", edit_article_path(@article) %></li>
  <li><%= link_to "Destroy", article_path(@article), data: {
                    turbo_method: :delete,
                    turbo_confirm: "Are you sure?"
                  } %></li>
</ul>

<h2>Comments</h2>
<%= render @article.comments %>

<h2>Add a comment:</h2>
<%= form_with model: [ @article, @article.comments.build ] do |form| %>
  <p>
    <%= form.label :commenter %><br>
    <%= form.text_field :commenter %>
  </p>
  <p>
    <%= form.label :body %><br>
    <%= form.text_area :body %>
  </p>
  <p>
    <%= form.submit %>
  </p>
<% end %>

Теперь это отрендерит партиал app/views/comments/_comment.html.erb по разу для каждого комментария в коллекции @article.comments. Так как метод render перебирает коллекцию @article.comments, он назначает каждый комментарий локальной переменной с именем, как у партиала, в нашем случае comment, которая нам доступна в партиале для отображения.

9.2. Рендеринг формы в партиале

Давайте также переместим раздел нового комментария в свой партиал. Опять же, создайте файл app/views/comments/_form.html.erb, содержащий:

<%= form_with model: [ @article, @article.comments.build ] do |form| %>
  <p>
    <%= form.label :commenter %><br>
    <%= form.text_field :commenter %>
  </p>
  <p>
    <%= form.label :body %><br>
    <%= form.text_area :body %>
  </p>
  <p>
    <%= form.submit %>
  </p>
<% end %>

Затем измените app/views/articles/show.html.erb следующим образом:

<h1><%= @article.title %></h1>

<p><%= @article.body %></p>

<ul>
  <li><%= link_to "Edit", edit_article_path(@article) %></li>
  <li><%= link_to "Destroy", article_path(@article), data: {
                    turbo_method: :delete,
                    turbo_confirm: "Are you sure?"
                  } %></li>
</ul>

<h2>Comments</h2>
<%= render @article.comments %>

<h2>Add a comment:</h2>
<%= render 'comments/form' %>

Второй render всего лишь определяет шаблон партиала, который мы хотим рендерить, comments/form. Rails достаточно сообразительный, чтобы подставить подчеркивание в эту строку и понять, что Вы хотели рендерить файл _form.html.erb в директории app/views/comments.

Объект @article доступен в любых партиалах, рендерящихся во вью, так как мы определили его как переменную экземпляра.

9.3. Использование Concerns

Concerns — это способ упростить большие контроллеры или модели для лучшего понимания и управления. У этого также есть преимущество повторного использования, когда несколько моделей (или контроллеров) разделяют те же самые concerns. Concerns реализуются с помощью модулей, содержащих методы, представляющие четко очерченный срез функциональности, за которую ответственны модель или контроллер. В других языках модули часто называются миксинами (mixin).

В контроллере или модели concerns можно использовать так же, как вы бы использовали другой модуль. Когда вы только создали свое приложение с помощью rails new blog, среди прочего в app/ были созданы две папки:

app/controllers/concerns
app/models/concerns

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

У статьи в блоге могут быть различные статусы - к примеру, она может быть видимой для всех (т.е. public) или только видимой для автора (т.е. private). Она также может быть скрытой, но все еще доступной (т.е. archived). Комментарии схожим образом могут быть скрытыми или видимыми. Это может быть представлено с помощью столбца status в каждой модели.

Сначала давайте запустим следующие миграции для добавления status в Articles и Comments:

$ bin/rails generate migration AddStatusToArticles status:string
$ bin/rails generate migration AddStatusToComments status:string

А затем давайте обновим базу данных с помощью сгенерированных миграций:

$ bin/rails db:migrate

Чтобы выбрать статус для существующих статей и комментариев, можно добавить значение по умолчанию в сгенерированные файлы миграции, добавив опцию default: "public" и запустив миграции заново. Также можно вызвать в консоли rails Article.update_all(status: "public") и Comment.update_all(status: "public").

Чтобы узнать больше о миграциях, смотрите Миграции Active Record.

Также нужно разрешить ключ :status, как часть strong parameter, в app/controllers/articles_controller.rb:

  private
    def article_params
      params.require(:article).permit(:title, :body, :status)
    end

и в app/controllers/comments_controller.rb:

  private
    def comment_params
      params.require(:comment).permit(:commenter, :body, :status)
    end

В модель article, после запуска миграции для добавления столбца status с помощью bin/rails db:migrate, можно добавить:

class Article < ApplicationRecord
  has_many :comments

  validates :title, presence: true
  validates :body, presence: true, length: { minimum: 10 }

  VALID_STATUSES = ['public', 'private', 'archived']

  validates :status, inclusion: { in: VALID_STATUSES }

  def archived?
    status == 'archived'
  end
end

и в модель Comment:

class Comment < ApplicationRecord
  belongs_to :article

  VALID_STATUSES = ['public', 'private', 'archived']

  validates :status, inclusion: { in: VALID_STATUSES }

  def archived?
    status == 'archived'
  end
end

Затем, в шаблоне экшна index (app/views/articles/index.html.erb) мы могли бы использовать метод archived?, чтобы избежать отображения любой архивированной статьи:

<h1>Articles</h1>

<ul>
  <% @articles.each do |article| %>
    <% unless article.archived? %>
      <li>
        <%= link_to article.title, article %>
      </li>
    <% end %>
  <% end %>
</ul>

<%= link_to "New Article", new_article_path %>

Схожим образом в нашем партиале (app/views/comments/_comment.html.erb) мы могли бы использовать метод archived?, чтоб избежать отображения любого архивированного комментария:

<% unless comment.archived? %>
  <p>
    <strong>Commenter:</strong>
    <%= comment.commenter %>
  </p>

  <p>
    <strong>Comment:</strong>
    <%= comment.body %>
  </p>
<% end %>

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

Concern ответственен только за конкретную часть ответственности модели; все методы нашего concern будут относиться к видимости модели. Давайте назовем новый concern (модуль) Visible. Можно создать новый файл в app/models/concerns с именем visible.rb, и хранить все методы статуса, которые продублированы в моделях.

app/models/concerns/visible.rb

module Visible
  def archived?
    status == 'archived'
  end
end

Также можно добавить наши валидации статуса в concern, но это немного сложнее, так как валидации — это методы, вызванные на уровне класса. ActiveSupport::Concern (руководство по API) предоставляет нам более простой способ включить их:

module Visible
  extend ActiveSupport::Concern

  VALID_STATUSES = ['public', 'private', 'archived']

  included do
    validates :status, inclusion: { in: VALID_STATUSES }
  end

  def archived?
    status == 'archived'
  end
end

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

В app/models/article.rb:

class Article < ApplicationRecord
  include Visible

  has_many :comments

  validates :title, presence: true
  validates :body, presence: true, length: { minimum: 10 }
end

и в app/models/comment.rb:

class Comment < ApplicationRecord
  include Visible

  belongs_to :article
end

Методы класса также можно добавить в concern. Если нам нужно отображать количество публичных статей или комментариев на главной странице, можно добавить метод класса в Visible следующим образом:

module Visible
  extend ActiveSupport::Concern

  VALID_STATUSES = ['public', 'private', 'archived']

  included do
    validates :status, inclusion: { in: VALID_STATUSES }
  end

  class_methods do
    def public_count
      where(status: 'public').count
    end
  end

  def archived?
    status == 'archived'
  end
end

Затем во вью его можно вызвать как любой другой метод класса:

<h1>Articles</h1>

Our blog has <%= Article.public_count %> articles and counting!

<ul>
  <% @articles.each do |article| %>
    <% unless article.archived? %>
      <li>
        <%= link_to article.title, article %>
      </li>
    <% end %>
  <% end %>
</ul>

<%= link_to "New Article", new_article_path %>

Наконец, добавим список выбора в формы и разрешим пользователю выбирать статус при создании новой статьи или публикации нового комментария. Также можно указать статус по умолчанию как public. В app/views/articles/_form.html.erb можно добавить:

<div>
  <%= form.label :status %><br>
  <%= form.select :status, ['public', 'private', 'archived'], selected: 'public' %>
</div>

и в app/views/comments/_form.html.erb:

<p>
  <%= form.label :status %><br>
  <%= form.select :status, ['public', 'private', 'archived'], selected: 'public' %>
</p>

10. Удаление комментариев

Другой важной особенностью блога является возможность удаления спама. Чтобы сделать это, нужно вставить некоторую ссылку во вью и экшн destroy в CommentsController.

Поэтому сначала добавим ссылку для удаления в партиал app/views/comments/_comment.html.erb:

<% unless comment.archived? %>
  <p>
    <strong>Commenter:</strong>
    <%= comment.commenter %>
  </p>

  <p>
    <strong>Comment:</strong>
    <%= comment.body %>
  </p>

  <p>
    <%= link_to "Destroy Comment", [comment.article, comment], data: {
                  turbo_method: :delete,
                  turbo_confirm: "Are you sure?"
                } %>
  </p>
<% end %>

Нажатие этой новой ссылки "Destroy Comment" запустит DELETE /articles/:article_id/comments/:id в нашем CommentsController, который затем будет использоваться для нахождения комментария, который мы хотим удалить, поэтому давайте добавим экшн destroy в наш контроллер (app/controllers/comments_controller.rb):

class CommentsController < ApplicationController

  def create
    @article = Article.find(params[:article_id])
    @comment = @article.comments.create(comment_params)
    redirect_to article_path(@article)
  end

  def destroy
    @article = Article.find(params[:article_id])
    @comment = @article.comments.find(params[:id])
    @comment.destroy
    redirect_to article_path(@article), status: :see_other
  end

  private
    def comment_params
      params.require(:comment).permit(:commenter, :body, :status)
    end
end

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

10.1. Удаление связанных объектов

Если удаляете статью, связанные с ней комментарии также должны быть удалены, в противном случае они будут просто занимать место в базе данных. Rails позволяет использовать опцию dependent на связи для достижения этого. Модифицируйте модель Article, app/models/article.rb, следующим образом:

class Article < ApplicationRecord
  include Visible

  has_many :comments, dependent: :destroy

  validates :title, presence: true
  validates :body, presence: true, length: { minimum: 10 }
end

11. Безопасность

11.1. Базовая аутентификация

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

Rails предоставляет аутентификационную систему HTTP, которая хорошо работает в этой ситуации.

В ArticlesController нам нужен способ блокировать доступ к различным экшнам, если пользователь не аутентифицирован. Тут мы можем использовать метод Rails http_basic_authenticate_with, разрешающий доступ к требуемым экшнам, если метод позволит это.

Чтобы использовать систему аутентификации, мы определим ее вверху нашего ArticlesController в app/controllers/articles_controller.rb. В нашем случае, мы хотим, чтобы пользователь был аутентифицирован для каждого экшна, кроме index и show, поэтому напишем так:

class ArticlesController < ApplicationController

  http_basic_authenticate_with name: "dhh", password: "secret", except: [:index, :show]

  def index
    @articles = Article.all
  end

  # пропущено для краткости

Мы также хотим позволить только аутентифицированным пользователям удалять комментарии, поэтому в CommentsController (app/controllers/comments_controller.rb) мы напишем:

class CommentsController < ApplicationController

  http_basic_authenticate_with name: "dhh", password: "secret", only: :destroy

  def create
    @article = Article.find(params[:article_id])
    # ...
  end

  # пропущено для краткости

Теперь, если попытаетесь создать новую статью, то встретитесь с вызовом базовой аутентификации HTTP:

Вызов базовой аутентификации HTTP

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

Также для приложений на Rails доступны иные методы аутентификации. Двумя популярными дополнениями для Rails, среди прочих, являются Devise и Authlogic.

11.2. Прочие мысли о безопасности

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

12. Что дальше?

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

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

13. Ошибки конфигурации

Простейший способ работы с Rails заключается в хранении всех внешних данных в UTF-8. Если не так, библиотеки Ruby и Rails часто будут способны конвертировать ваши родные данные в UTF-8, но это не всегда надежно работает, поэтому лучше быть уверенным, что все внешние данные являются UTF-8.

Если вы допускаете ошибку в этой области, наиболее обычным симптомом является черный ромбик со знаком вопроса внутри, появляющийся в браузере. Другим обычным симптомом являются символы, такие как "ü" появляющиеся вместо "ü". Rails предпринимает ряд внутренних шагов для смягчения общих случаев тех проблем, которые могут быть автоматически обнаружены и исправлены. Однако, если имеются внешние данные, не хранящиеся в UTF-8, это может привести к такого рода проблемам, которые не могут быть автоматически обнаружены Rails и исправлены.

Два наиболее обычных источника данных, которые не в UTF-8:

  • Ваш текстовый редактор: Большинство текстовых редакторов (такие как TextMate), по умолчанию сохраняют файлы как UTF-8. Если ваш текстовый редактор так не делает, это может привести к тому, что специальные символы, введенные в ваши шаблоны (такие как é) появятся как ромбик с вопросительным знаком в браузере. Это также касается ваших файлов перевода i18N. Большинство редакторов, не устанавливающие по умолчанию UTF-8 (такие как некоторые версии Dreamweaver) предлагают способ изменить умолчания на UTF-8. Сделайте так.
  • Ваша база данных: Rails по умолчанию преобразует данные из вашей базы данных в UTF-8 на границе. Однако, если ваша база данных не использует внутри UTF-8, она может не быть способной хранить все символы, которые введет ваш пользователь. Например, если ваша база данных внутри использует Latin-1, и ваш пользователь вводит русские, ивритские или японские символы, данные будут потеряны как только попадут в базу данных. Если возможно, используйте UTF-8 как внутреннее хранилище в своей базе данных.