ReactやwebpackもサポートしたRails 5.1の新機能・変更点

## はじめに

2016年の[バージョン5.0のリリース](http://weblog.rubyonrails.org/2016/6/30/Rails-5-0-final/)に引き続き、2017年の春にRuby on Rails（以下Rails）のバージョン5.1がリリースされました。

Rails 5.0は、APIモードやAction Cableなど、APIやWebSocket周りの新機能を中心としたリリースでしたが、Rails 5.1は、React / Vue.js / webpack / Yarn（npmパッケージ）のサポートなど、フロントエンド周りの新機能を中心としたリリースとなりました。

本記事では、[GitHubのRailsプロジェクト](https://github.com/rails/rails)の[Issues](https://github.com/rails/rails/issues)や[Pull Requests](https://github.com/rails/rails/pulls)の履歴、[Railsの公式ブログ](http://weblog.rubyonrails.org/)の記事をもとに、Rails 5.1の主要な新機能・変更点の紹介を行います。

※ 他のバージョンのRailsの主要な新機能・機能追加・変更点については以下を参照してください。

- [今から知っておきたいRails 5の新機能・変更点 - Qiita](https://qiita.com/ryohashimoto/items/09724ad4dddcda01bce3)
- [Ruby on Rails 5.2の新機能（Active Storage, Content Security Policyなど） - Qiita](https://qiita.com/ryohashimoto/items/75f48fa5a3846c58f735)
- [Ruby on Rails 6の主要な新機能・機能追加・変更点](https://qiita.com/ryohashimoto/items/622c3bcfb3336cb9317e)

## 新機能

### Reactとwebpackのサポート

[Webpacker](https://github.com/rails/webpacker)というパッケージを使用することで、JavaScriptファイルの依存関係の処理や圧縮・結合を[webpack](https://webpack.github.io/)経由で行えるようになりました。
Webpacker自体は既存のアセットパイプラインと共存し、比較的規模の大きいシングルページアプリケーションのようなJavaScriptの管理を行います。
（Webpackerが管理するJavaScriptのパスは、`app/javascript/packs/`になっています。）
webpackのサポートを有効にするには、`rails new`コマンドのオプションで`--webpack`を使用するか、Gemfileの`gem 'webpacker'`の部分を有効にして、`bin/rails webpacker:install`を実行します。

標準で[React](https://facebook.github.io/react/)もサポートされており、新しいアプリケーションの場合は`rails new myapp --webpack=react`を、既存のアプリケーションの場合は、`rails webpacker:install:react`をそれぞれ実行することですぐに利用することができます。

* [Basic --webpack delegation to new webpacker gem](https://github.com/rails/rails/pull/27288)

* [rails/webpacker](https://github.com/rails/webpacker)

### Yarnとnpmパッケージのサポート

`rails new`コマンドを実行した際にデフォルトで[Yarn](https://yarnpkg.com/)とnpmパッケージのサポートを有効にするようにしました。これによって、`package.json`がアプリケーションに追加され、`node_modules`がアセットパイプラインのパスに追加されます。

`rails new`コマンド実行時にYarnのサポートを無効にするには、`--skip-yarn`を指定します。

* [Add Yarn support in new apps using --yarn option](https://github.com/rails/rails/pull/26836)

## 機能追加

### `form_with`

これまで似たような使い方がされてきた`form_tag`と`form_for`の代わりに、`form_with`メソッドを使用することができるようになりました。
`form_tag`と`form_for`は受け取る引数やオプション、フォーム内部のフィールド用のタグを出力するメソッドなどが異なっていましたが、`form_with`によって共通の方法でフォームを作成することができるようになりました。
（今後は`form_with`に完全に統合され、`form_tag`と`form_for`は非推奨になる可能性があります。）

また、`form_with`では、以下のような変更が加えられています。

1. 標準では出力されるHTMLタグにDOMのid属性やclass属性をつけないようにしました。これは特にid属性の重複をなくすためで、必要な場合は簡単に追加することもできます。
2. id属性やclass属性を追加する際に、オプションの`html: {}`の値に入れる必要がなくなりました。これは、`id`や`class`といった名前が他のキーの名前とほぼ重複しないためです。
3. モデルの属性以外のフィールドを許可するようにしました。これによって、登録確認のチェックボックスなど、モデルに含まれないフィールドをフォームに入れるのが容易になりました。
4. `remote: true`オプションがデフォルトになりました。フォーム送信後は、全く新しいページに遷移するのではなく、`Turbolinks.visit()`か、サーバー側で生成されたレスポンス（のJavaScript）を使用します。

例）

* `form_with`で`url`を指定した場合

```erb
<%= form_with url: posts_path do |form| %>
  <%= form.text_field :title %>
<% end %>
```

↓

```html
<form action="/posts" method="post" data-remote="true">
  <input type="text" name="title">
</form>
```

* `form_with`で`model`を指定した場合

```erb
<%= form_with model: Post.new do |form| %>
  <%= form.text_field :title %>
<% end %>
```

↓

```html
<form action="/posts" method="post" data-remote="true">
  <input type="text" name="post[title]">
</form>
```

* [Provide form_with as a new alternative to form_for/form_tag](https://github.com/rails/rails/issues/25197)
* [Add form_with to unify form_tag/form_for. ](https://github.com/rails/rails/pull/26976)

### Action Mailerでパラメータを利用できるように

コントローラと同様にメーラでもパラメータ（`params`）を利用できるようになりました。

メーラのアクション（メソッド）で使用するパラメータは`with`メソッドの引数で指定します。
たとえば、以下のようになります。

```ruby
InvitationsMailer.with(inviter: '喜井太郎').account_invitation.deliver_now!
```

（パラメータを使用しない場合は、withメソッドの呼び出しがなくなるだけなので、後方互換姓が保たれています。）

メーラのアクション内では、以下のようにして`params`にアクセスできるようになります。

``` ruby
class InvitationsMailer < ApplicationMailer
  def account_invitation
    @inviter = params[:inviter]
    mail(subject: "#{@inviter}さんから招待が来ています。")
  end
end
```

また、`before_action`などのコールバックを使用して、各アクションに共通なパラメータの処理を定義することができます。

``` ruby
class InvitationsMailer < ApplicationMailer
  before_action  { @inviter = params[:inviter] }

  def account_invitation
    mail(subject: "#{@inviter}さんから招待が来ています。")
  end

  def project_invitation
    @project = params[:project]
    mail(subject: "#{@inviter}さんから#{@project}への招待が来ています。")
  end
end
```

* [rails/parameterized.rb](https://github.com/rails/rails/blob/2dadf73891431ff16709ae41ccb552a56ce34c32/actionmailer/lib/action_mailer/parameterized.rb)

### tagヘルパの記法改善

`ActionView::Helpers::TagHelper`で提供されている`#tag`と`#content_tag`の引数を減らし、HTML5をサポートするようにしました。
（下の例に示すように、これまで、上記のメソッドは多くのパラメータを渡す必要があり、デフォルトとしてXTHMLを想定していたため冗長な記述が必要でした。）
これにより、新しい記法では、より短くシンプルにヘルパメソッドを用いてタグを出力することができるようになります。

古い記法

```erb
tag(:br, nil, true)
content_tag(:div, content_tag(:p, "Hello world!"), class: "strong")

<%= content_tag :div, class: "strong" do -%>
  Hello world!
<% end -%>
```

新しい記法

```erb
tag.br
tag.div tag.p("Hello world!"), class: "strong"

<%= tag.div class: "strong" do %>
  Hello world!
<% end %>
```

* [Provide HTML tag dynamic proxy](https://github.com/rails/rails/issues/25195)

### ActiveJob::Baseにretry_on / discard_onを追加

Active Jobで例外が発生した場合のジョブの再試行と破棄の動作を指定できるようになりました。

#### 再試行 (`retry_on`)

例外をキャッチして、ジョブの再試行を指定した試行回数分だけ実行します。試行回数を超えた場合は、例外はキャッチされません。
再試行が失敗した場合は、ブロックを渡して独自の処理を行うこともできます。

オプション

* `wait`: 再試行の間隔を秒単位で指定することができます。（デフォルト: 3秒）
* `exponentially_longer`: 再試行の間隔に指数アルゴリズムを使用します。
（回数**4 + 2,となり、3秒後、18秒後、83秒後、…に再試行を行います。）
* `attempts`: 指定した回数だけ再試行を行います。（デフォルト: 5回）
* `queue`: 再試行を指定した別のキューで行います。
* `prioriry`: 再試行を別の優先度で行います。

例）

```ruby
class RemoteServiceJob < ActiveJob::Base
  retry_on CustomAppException
  retry_on ActiveRecord::StatementInvalid, wait: 5.seconds, attempts: 3
  retry_on Net::OpenTimeout, wait: :exponentially_longer, attempts: 10
  ..
end
```

#### 破棄

例外が発生した場合は再試行を行わずにジョブを破棄します。これは対象となるモデルのデータが存在しない場合などで使用することができます。

例）

```ruby
class SearchIndexingJob < ActiveJob::Base
  discard_on ActiveJob::DeserializationError
  ..
end
```

* [Add retry_on/discard_on for better exception handling](https://github.com/rails/rails/pull/25991)

### 単一リソースのパス生成方法の指定が可能に

ルーティングの記述で`resolve`を用いることによって、モデルのインスタンスを指定した場合のURLヘルパの生成方法をカスタマイズすることができるようになりました。

例えば、以下のようなコードで、`@current_user`に`User`モデルのインスタンスを引数として用いた場合に、URLとして`/profile`を生成できるようにする場合は、

```ruby
link_to 'プロフィール', @current_user
```

ルーティングで以下のような記述を行います。

```ruby
resource :profile
resolve("User") { [:profile] }
```

※上記のパス生成方法の指定に関しては、現在は1つのモデルインスタンスを使用した単一リソースの場合のみ有効で、複数リソースの場合や、`namespace`を使用した場合、リソースのネストを行った場合は使用できません。

* [Implement custom url helpers and polymorphic mapping](https://github.com/rails/rails/pull/23138)



### カスタムURLヘルパ

ルーティングの記述で`direct`を用いることによって、独自に定義した`apple_url`のようなURLヘルパを使用できるようになりました。

```ruby
direct :apple do
  "http://www.apple.com/"
end
```

カスタムURLヘルパに引数を取るようにできるようにするには、ブロック引数を使用します。

たとえば、`commentable_path(article)`みたいなURLヘルパを使用したい場合（引数`article`はモデルのインスタンス）、以下のようにルーティングを定義すると`article`がブロック引数の`model`となりURLの生成が可能になります。（ブロックが返す値は`url_for`の引数となります。）

```ruby
direct :commentable do |model|
  [ model, anchor: model.dom_id ]
end
```

* [Implement custom url helpers and polymorphic mapping](https://github.com/rails/rails/pull/23138)

### Capybaraによるシステムテスト

標準でChromeなどのブラウザを用いたテストが実行できるようになりました。内部的には、[Capybara](https://github.com/teamcapybara/capybara)を使用しています。
テストコードの記述には、以下の例のように、`ApplicationSystemTestCase`を継承したクラスを用います。

```ruby
require 'application_system_test_case'

class Users::CreateTest < ApplicationSystemTestCase
  test "adding a new user" do
    visit users_path
    click_on 'New User'
 
    fill_in 'Name', with: 'Arya'
     click_on 'Create User'
     assert_text 'Arya'
  end
end
```

Railsアプリケーションを作成した場合、標準で以下のような`application_system_test_case.rb`という名前のファイルが生成され、そこに`ApplicationSystemTestCase`クラスが定義されています。

```ruby
require "test_helper"

class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
  driven_by :selenium, using: :chrome, screen_size: [1400, 1400]
end
```

このファイルの内容を変更することで、Capybaraのドライバや、ブラウザ、画面解像度の設定が可能です。

* [WIP: Capybara Integration with Rails (AKA System Tests)](https://github.com/rails/rails/pull/26703)

### `config/secrets.yml`の暗号化

これまで`config/secrets.yml`ファイルにアプリケーションで使用するsecret_key_baseや、外部APIのトークンなどの記述を平文で行っていましたが、Rails 5.1ではこの内容を暗号化したファイルに保存したり、編集することができるようになりました。

#### 鍵と暗号化されたファイルの生成

以下のコマンドの実行を行います。

```
bin/rails secrets:setup
```

`config/secrets.yml.key`（鍵）と`config/secrets.yml.enc`（暗号化された`config/secrets.yml`）の2つのファイルが生成されます。

※ 絶対に、`config/secrets.yml.key`をリポジトリにコミットしないようにしてください。また、その中身を失わないようにしてください。第三者に悪用されたり、`config/secrets.yml.enc`の内容を読み書きすることができなくなったりしてしまいます。
Railsは`ENV["RAILS_MASTER_KEY"]`からも鍵を参照するので、`config/secrets.yml.key`の内容を環境変数`RAILS_MASTER_KEY`に入れておくのが簡単です。

#### 暗号化されたファイルの編集

以下のコマンドを実行します。

```
bin/rails secrets:edit
```

これによって、`$EDITOR`環境変数で指定されたエディタが立ち上がり、一時ファイルに書き出された平文化された`config/secrets.yml.enc`を編集することができます。エディタで保存を行うと、一時ファイルは破棄され、`config/secrets.yml.enc`に書き出しが行われます。　

* [Add encrypted secrets](https://github.com/rails/rails/pull/28038)

## 変更点

### jQuery依存の廃止

これまで使用されていたRailsのJavaScriptのjquery-ujsがjQueryではなく素のJavaScriptで書き直されてrails-ujsとなりました。

* [Drop jQuery as a dependency](https://github.com/rails/rails/pull/27113)

### PostgreSQLとMySQLのプライマリキーがBIGINTに

PostgreSQLとMySQLの両方でデフォルトのプライマリキーがIntegerからBIGINTになりました。（SQLiteはBIGINTのプライマリキーをサポートしていないので、Integerのままです。）
既存のテーブルはIntegerのままで、新しいテーブルでBIGINTのプライマリキーが有効になります。

* [Change Default Primary Keys to BIGINT](https://github.com/rails/rails/pull/26266/files)

### ActiveRecord::NotNullViolation

NOT NULL制約の為にモデルの保存に失敗した場合は、`ActiveRecord::StatementInvalid`の代わりに`ActiveRecord::NotNullViolation`を例外として発生するようにしました。

* [Translate NOT NULL violation to the specific exception](https://github.com/rails/rails/pull/25451)

### ActiveRecord::DeadlockError

トランザクション処理でシリアライズに失敗した場合や、デッドロックが検出された場合に、RDBMSのエラーコードを元に、`ActiveRecord::DeadlockError`を例外として発生するようにしました。

* [Provide unified ActiveRecord::DeadlockError](https://github.com/rails/rails/issues/26018)
* [Introduce AR::TransactionSerializationError for transaction serialization failures or deadlocks](https://github.com/rails/rails/pull/25093)
* [Introduce new ActiveRecord transaction error classes](https://github.com/rails/rails/pull/25107)

### Time形式のカラムがタイムゾーンを考慮するように

マイグレーションで`:datetime`を指定したカラムだけでなく、`:time`を指定したカラムも、標準でタイムゾーンを考慮して（具体的には、`Time.zone`として）処理が行われることになりました。
以前の挙動に戻すには以下のように設定を行う必要があります。

`config/application.rb`

```ruby
config.active_record.time_zone_aware_types = [:datetime]
```

### Active Record (Active Model)のコールバックの停止条件の変更

これまで、Active RecordやActive Modelで、`before_save` / `before_validation`のコールバックのメソッドの返り値が`false`の場合は実行を停止してましたが、この挙動が変更になり、Rails 5.0で導入された`throw :abort`の呼び出しによってのみ停止するようになりました。
（特にRails 4からアプリケーションをアップグレードした場合は`false`で停止を行っていないかチェックが必要です。）

## 廃止

### `ActionController::TestCase`をgemとして分離

Rails 5.0では`ActionDispatch::IntegrationTest`を`ActionController::TestCase`の代わりに標準で使用するようになっており、`ActionController::TestCase`は非推奨になっていました。
Rails 5.1では`ActionController::TestCase`はGemとしてRailsとは別のリポジトリに切り離されて、標準では利用できなくなります。

### コントローラのコールバックの名称を`action`を用いたものに統一

`before_filter` / `after_filter`などの`filter`を用いた名称が廃止され、`before_action` / `after_action`など`action`を用いた名称に統一されます。

### `redirect_to :back`の廃止

コントローラ内で以前のアクセスしたURLに戻る際に用いられてきた、`redirect_to :back`のメソッド呼び出しが廃止され、代わりに`redirect_back(fallback_location: fallback_location)`を使うようにする必要があります。ここで、`fallback_location`としてはリクエストにHTTPリファラの情報がない場合に戻る場所（URL）を指定します。

### `render :nothing`の廃止

コントローラ内でステータスコードのみを返す場合に利用されていた`render :nothing`のメソッド呼び出しが廃止されました。

Rails 5.1以降では、単純にステータスコードを返す場合、`head :created`のように、`head`メソッドを使用する必要があります。

### `params`（コントローラ内のパラメータ）とハッシュの比較ができないように

Rails 5で`ActionController::Parameters`が`Hash`から継承しなくなったのに伴い、Rails 5.1では`params`とハッシュの比較ができなくなりました。今後比較を行う場合には、`ActionController::Parameters#new`でハッシュを変換する必要があります。また、`ActionController::Parameters`から`Hash`にあるメソッドの一部が廃止されています。

## まとめ

Rails 5.1はReact / webpack / Yarnのサポートなどフロントエンド周りの新機能と共に、`form_with`タグや`tag`ヘルパの文法変更など、主にJavaScriptアセットやView部分の変更が大きいリリースとなりそうです。将来的な変更に備え、今からアップグレードの準備しておきましょう。

## 参考サイト

* [Riding Rails](http://weblog.rubyonrails.org/)
* [rails/rails](https://github.com/rails/rails)