Ruby on Rails 6.1の主要な新機能・変更点

## はじめに

昨年（2019年）8月にRuby on Rails（以下Rails）バージョン6.0がリリースしてから1年4ヶ月経ち、ようやく[バージョン6.1が2020年12月にリリースされました](https://weblog.rubyonrails.org/2020/12/9/Rails-6-1-0-release/)。　

Rails 6.1は、複数データベースの接続の改善、水平分割シャーディングのサポート、Strict Loadingなど、Active Recordやデータベース周りの改善や機能強化を中心としたリリースになるようです。

本記事では、公式ブログの[Riding Rails](https://weblog.rubyonrails.org/)や、GitHubの[Railsプロジェクト](https://github.com/rails/rails)の[Issues](https://github.com/rails/rails/issues)や[Pull Requests](https://github.com/rails/rails/pulls)の内容をベースにして、Rails 6.1の主要な新機能・変更点の紹介と解説を行います。

※ 以前のバージョンのRailsの主要な新機能・変更点についてはこちら。

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

## 注意点

### Rubyのバージョン

Rails 6.1を動かすには、Rails 6.0と同様に、Ruby 2.5以上が必要になります。

### セキュリティアップデート

Rails 6.1のリリース後は、Rails 6.1がバグ修正の対象、Rails 5.2以上がセキュリティ問題の修正の対象となります。それ以下のバージョンを使用していて、セキュリティ問題の修正を受け取りたい場合は、アップグレードを行う必要があります。

**参考**

* Maintenance Policy for Ruby on Rails — Ruby on Rails Guides
https://guides.rubyonrails.org/maintenance_policy.html

## 新機能

### 複数データベースの接続切り替えの改善

Rails 6.0で複数データベース対応が行われましたが、Rails 6.1では改善が行われ、データベース接続を切り替える`connected_to`メソッドを`ActiveRecord::Base`からだけでなく、抽象クラスからも呼び出せるようになり、モデル毎のより細かな接続の切り替えが可能になりました。（この切り替えは、後述の水平分割シャーディングにも対応しています。）

以下の例では、抽象クラス`AnimalsRecord`の`connected_to`メソッドのブロック内で実行することで、readingロールとは異なるwritingロールのデータベースから読み出しを行っています。

```ruby
# UserクラスはApplicationRecordの子クラス
# DogクラスはAnimalsRecordの子クラス
ActiveRecord::Base.connected_to(role: :reading) do
  User.first # readingから読み出し
  Dog.first # readingから読み出し
  AnimalsRecord.connected_to(role: :writing) do
    User.first # readingから読み出し（AnimalsRecordの子クラスではないので影響を受けない）
    Dog.first # writingから読み出し
  end
end
```

上記のような、柔軟なデータベース接続の切り替えを行うには、以下のようにRailsの設定でレガシーな接続切り替えをオフにする必要があります。

```ruby:config/application.rb
config.active_record.legacy_connection_handling = false
```

**参考**

* Implement granular role and shard swapping by eileencodes · Pull Request #40370 · rails/rails
https://github.com/rails/rails/pull/40370

### 水平分割シャーディング対応

Rails 6.0の複数データベース対応で、異なるスキーマのレコードを別々のデータベースに格納することが可能になりましたが、Rails 6.1では水平分割シャーディング対応を行うことで、同じスキーマのレコードを複数のパーティションに区切って別々のデータベースに格納することを可能にしました。

例えば、以下のように`connects_to`で接続先のシャードが定義されているとします。

```ruby
class ApplicationRecord < ActiveRecord::Base
  self.abstract_class = true

  connects_to shards: {
    default: { writing: :primary },
    shard_one: { writing: :primary_shard_one }
  }
  ...
end

class Venue < ApplicationRecord
  ...
end
```

上記の定義では、標準で書き込みはdefaultで指定したDB（primary）に対して行われますが、`connected_to`で`shard`を指定することで別のDB（primary_shard_one）に書き込むことが可能になります。[リファレンスにあるように](https://api.rubyonrails.org/classes/ActiveRecord/ConnectionHandling.html#method-i-connected_to)`shard`を指定する場合は`role`も指定する必要があることに注意してください。

```ruby
first_venue = Venue.first
...
first_venue.save # primaryに書き込み

ApplicationRecord.connected_to(role: :primary, shard: :shard_one) do
  second_venue = Venue.second
  ...
  second_venue.save # primary_shard_oneに書き込み
end
```

Railsで自動的に別々のシャードを指定してレコードを読み込んだり、書き込んだりしたりする訳ではないので、接続の切り替えのコードはアプリケーション側で（`connected_to`を使用して）記述する必要があります。

**参考**

* Add support to `connected_to` and `connects_to` for horizontal sharding by seejohnrun · Pull Request #38531 · rails/rails
https://github.com/rails/rails/pull/38531

### モデルの関連のStrict Loading

モデルの`has_many`などの関連に`strict_loading: true`オプションをつけることで、関連先のモデルのインスタンスを参照する際に、事前にレコードの読み込みを行っているかどうかをチェックすることが可能になりました。
`preload`などのメソッドで事前にレコードの読み込みを行っていない場合は、`ActiveRecord::StrictLoadingViolationError`例外を発生させるので、N+1問題の発生を事前に検知させることができます。（これまでGemの[Bullet](https://github.com/flyerhzm/bullet)で行っていたようなことがRails標準でもできるようになりました。）
以下のような記述をモデル内に行うことで、関連先の`strict_loading`のデフォルト値を設定することもできます。

```ruby
self.strict_loading_by_default = true
```

**参考**

* Add `strict_loading` mode to optionally prevent lazy loading by eileencodes · Pull Request #37400 · rails/rails 
  https://github.com/rails/rails/pull/37400
* Support strict_loading on association declarations by kddeisz · Pull Request #38541 · rails/rails
  https://github.com/rails/rails/pull/38541
* Allow to enable/disable strict_loading mode by default for a model. by bogdanvlviv · Pull Request #39491 · rails/rails 
  https://github.com/rails/rails/pull/39491

### Delegated Types

これまでは、あるモデルから他のモデルの属性を引き継ぐ際にSTI（Single Table Inheritance、単一テーブル継承）を使用することができましたが、Rails 6.1で別の実装方法であるDelegated Typesが使用できるようになりました。

STIでは、親クラスと子クラスの属性をまとめて1つのテーブルに保存しますが、Delegated Typesでは共通で使用するものは委譲元のテーブルに保存し、個別で使用する属性を移譲先のテーブルに分けて保存するようにします。 （Delegate Typesではモデル間に継承関係はないので、本記事ではSTIにおける親クラスに相当するクラスを委譲元、子クラスに相当するクラスを移譲先と呼ぶようにしています。）

以下の例では、MessageとCommentの2つのモデルが、Entryモデルで定義されている属性（`user_id` ) を共有しています。Entryに`delegated_type :entryableを`定義しているので、DBのテーブルのスキーマに、`entryable_id`と`entryable_type`　カラムを定義して、そこにMessageやCommentのidと、モデル（クラス）名を入れておきます。　

```ruby
class Entry < ApplicationRecord
  belongs_to :user
  delegated_type :entryable, types: %w[Message Comment]
end

class Message < ApplicationRecord
  has_one :entry, as: :entryable, touch: true
  has_rich_text :content
end

class Comment < ApplicationRecord
  has_one :entry, as: :entryable, touch: true
  include Entryable
end
```

このようにすることで、以下のようなコードで、最新のメッセージとコメントを合わせて取得することができます。

```ruby
Account.entries.order(created_at: :desc).limit(50)
```

ビューでは以下のようにして、Entryモデルのインスタンスである`entry`から、委譲先（関連先）の`message`や`comment`を取得して、使用することができます。

```erb
# entries/_entry.html.erb
<%= render "entries/entryables/#{entry.entryable_name}", entry: entry %>

# entries/entryables/_message.html.erb
<div class="message">
  Posted on <%= entry.created_at %> by <%= entry.user.name %>: <%= entry.message.content %>
</div>

# entries/entryables/_comment.html.erb
<div class="comment">
  <%= entry.user.name %> said: <%= entry.comment.content %>
</div>
```

**参考**

* Add delegated type to Active Record by dhh · Pull Request #39341 · rails/rails
  https://github.com/rails/rails/pull/39341

### 非同期による関連レコードの削除

モデルの関連のオプションとして`dependent: :destroy_later`を指定することで、あるモデルのレコードが削除された際に、Active Jobで非同期で関連するレコードを削除することができるようになりました。

以下の例では、記事（article）にコメント（comment）が紐付いている場合に、バックグランドでコメントのレコードの削除を行います。

```ruby
class Comment < ActiveRecord::Base
  belongs_to :article, dependent: :destroy_async
end
```

**参考**

* Offer dependent: :destroy_async for associations by adrianna-chang-shopify · Pull Request #40157 · rails/rails
  https://github.com/rails/rails/pull/40157

### `ActiveModel::Error`オブジェクト

モデルのバリデーションエラーなどを格納する`ActiveModel::Errors`が、単一のエラー内容を保持する`ActiveModel::Error`クラスのオブジェクトの配列を保持するように変更されました。以前はエラーメッセージや詳細をハッシュとして保持していましたが、よりオブジェクト指向的な内部実装に変更されています。

新しくエラー内容を作成するには、以下のように`ActiveModel::Error.new`を使用します。`details`や`full_message`で、エラー内容をハッシュやメッセージの文字列で取り出すことが可能です。

```ruby
error = ActiveModel::Error.new(user, :name, :too_short, count: 5)
error.details
# => { error: :too_short, count: 5 }
error.full_message
# => "Name is too short (minimum is 5 characters)"
```

以下のような`where`メソッドでのエラーの絞り込みができるようになっています。

```ruby
user.errors.where(:name, :blank).last.message
#=> "can't be blank"
```

上記の変更に伴って、`ActiveModel::Errors`のメソッドの一部で変更になったり、使用できなくなっているものがあります。詳細に関しては以下の参考のURLを確認してください。

**参考**

* Model error as object by lulalala · Pull Request #32313 · rails/rails
  https://github.com/rails/rails/pull/32313
* Encapsulate each validation error as an Error object | Saeloun Blog
  https://blog.saeloun.com/2020/06/17/rails-active-model-errors

## 機能追加

### 複数データベース用のタスクの追加

複数データベースで設定した、個別のデータベース（のrole）を対象としたタスクを新たに使用することができるようになりました。

例えば、roleがprimaryの場合のマイグレーションのコマンドは、`rails db:migrate:primary`、roleがaminalsの場合のDB作成のコマンドは、`rails db:create:animals`となります。

**参考**

* Adds additional database-specific rake tasks for multi-database users by kylekthompson · Pull Request #38449 · rails/rails
https://github.com/rails/rails/pull/38449

### アプリケーション生成時の minimalモード

`rails new`コマンドに`--minimal`モードを渡すことで、以下のライブラリを**含まない**、最小のアプリケーションを構成することが可能になりました。

* minimalモードに含まれないライブラリ：action_cable, action_mailbox, action_mailer, action_text, active_job, active_storage, bootsnap, jbuilder, spring, system_tests, turbolinks, webpack

**参考**

* rails new cool_app --minimal by hahmed · Pull Request #39282 · rails/rails
https://github.com/rails/rails/pull/39282

### config/routes.rbで外部のファイルを読み込むことが可能に

`config/routes.rb`ファイルから、別のルーティング定義ファイルを読み込むことが可能になりました。
例えば、以下のようなルーティングファイルを`config/routes/admin.rb`に保存しておいて、

```ruby:config/routes/admin.rb
get :top, to: 'top#index'
```

`draw(:admin)`と書くことで、`config/routes.rb`ファイルから、上記のファイルで定義したルーティングを読み込むことができます。

```ruby:config/routes.rb
Rails.application.routes.draw do
  draw(:admin)
end
```

**参考**

* Bring back feature that allows loading external route files: by Edouard-chin · Pull Request #37892 · rails/rails
  https://github.com/rails/rails/pull/37892

### HTTP Feature PolicyをDSLで指定可能に

[HTTPのFeature-Policyヘッダ](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Feature-Policy)は、表示中のページ内で位置情報やカメラなどのブラウザの機能を使用することを許可するかもしくは拒否するかを設定できる仕組みですが、これをRailsの設定で記述できるようになりました。

```ruby:config/initializers/feature_policy.rb
Rails.application.config.feature_policy do |f|
  f.geolocation :none
  f.camera      :none
  f.payment     "https://secure.example.com"
  f.fullscreen  :self
end
```

コントローラで個別に指定するようには以下のように記述します。

```ruby
class SampleController < ApplicationController
  def index
    feature_policy do |f|
      f.geolocation "https://example.com"
    end
  end
end
```

**参考**

* Adds support for configuring HTTP Feature Policy by jacobbednarz · Pull Request #33439 · rails/rails https://github.com/rails/rails/pull/33439

### ERBテンプレートのファイル名をHTMLのソースに出力可能に

以下の設定を各環境の設定ファイルに行うことで、テンプレートのパスをHTML内にコメントとして出力することが可能になりました。

```ruby:config/environments/development.rb
config.action_view.annotate_template_file_names = true
```

下の例では、以下のようにERBファイルのパスが出力されています。（ERBファイル以外のテンプレートにはまだ対応していないようです。）

```html
 <html>
   <head>
     <title>Hello, world!</title>
     ...
   </head>
   <body>
     <!-- BEGIN app/views/hello/index.html.erb -->
     <h1>Hello, world!</h1>
     <!-- END app/views/hello/index.html.erb -->
   </body>
</html>
```

**参考**

* .annotate_template_file_names annotates HTML output with template names by joelhawksley · Pull Request #38848 · rails/rails
  https://github.com/rails/rails/pull/38848

### CSSのクラス名を`class_names`ヘルパーで動的にセットできるように

これまで条件に応じてCSSクラス名を付与する際には、以下のように条件式と、その結果出力されるCSSクラス名を直接記述していました。

```erb
<div class="<%= item.for_sale? ? 'active' : '' %>">
```

これが、`class_names`ヘルパーを使用することでより簡潔に書けるようになり、引数のキーがCSSクラス名、値が条件式になっていて、条件式が真のときに、CSSクラス名を出力します。

```erb
<div class="<%= class_names(active: item.for_sale?) %>">
```

**参考**

* Introduce class_names helper by joelhawksley · Pull Request #37918 · rails/rails https://github.com/rails/rails/pull/37918

### 有効期限付きの改ざん防止機能を備えたsigned id

Active Recordに有効期限付きの改ざん防止機能を備えたIDを生成し、それを元にレコードを取得するsigned idと呼ばれる機能が追加されました。

以下の例のように、モデルのインスタンスの`#signed_id`メソッドを、有効期限`expires_in`、目的`purpose`を指定して呼び出すことでsigned idを生成し、モデルの`.find_signed`メソッドを生成した`signed_id`、目的`purpose`を指定して呼び出すことで、レコードを取得します。`signed_id`と`purpose`が一致しない場合や、期限切れになった場合は、`nil`を返します。（`.find_signed!`メソッドを使用することで、`nil`を返す代わりに例外（`ActiveSupport::MessageVerifier::InvalidSignature`）を発生させることもできます。）

例

```ruby
user = User.first
signed_id = user.signed_id(expires_in: 15.minutes, purpose: :password_reset)
User.find_signed(signed_id, purpose: :password_reset)
```

生成したsigned idは、トークンとしてメールの文中に埋め込むことができ、メール認証やパスワードリセットなどに使用できるようです。

**参考**

* Add signed ids to Active Record by dhh · Pull Request #39313 · rails/rails https://github.com/rails/rails/pull/39313

### `*_previously_was`メソッドの追加

モデルのインスタンスの保存後に、保存前の属性の値を取得できる`*_previously_was`メソッドが追加されました。（`*`は属性名）

例

```ruby
user = User.find_by(name: 'Adam')
user.save
user.name = 'Bob'
user.name_previously_was # => "Adam"
user.reload
user.name_previously_was # => nil
```

`reload`を呼び出すと、属性の値の追跡はリセットされるので、`*_previously_was`は`nil`を返します。

**参考**

* Add \*\_previously\_was attribute methods when dirty tracking by dhh · Pull Request #36836 · rails/rails https://github.com/rails/rails/pull/36836
-  Rails 6.1 adds \*\_previously\_was attribute methods | BigBinary Blog https://blog.bigbinary.com/2019/12/03/rails-6-1-adds-_previously_was-attribute-methods.html

### Active Storageでファイルの永続的なURLを使用できるように

Active Storageの設定ファイルの`config/storage.yml`で、各サービスの設定で`public: true`にした場合、`Blob#url`でファイルの永続的なURLを使用することができるようになりました。（これまでは、`Blob#service_url`で一時的なURLを返していました。）

**参考**

* Permanent URLs for Active Storage blobs by peterzhu2118 · Pull Request #36729 · rails/rails
https://github.com/rails/rails/pull/36729

## 変更点

### CSRFトークンの形式がURLセーフに

CSRFを生成する際にURLセーフなBase64でエンコードするようになりました。この変更により、以前のRailsのバージョンで生成されたCSRFトークンと互換性がなくなってしまったため、クッキーなどのCSRFトークンが保存されている場合は注意が必要です。

例えば、開発環境のアプリケーションでRails 6.1とそれ以前のバージョンを切り替えたり、本番環境で複数のサーバーにRails 6.1とそれ以前のバージョンが一時的にデプロイされている状態だと、例外（`ArgumentError: invalid base64`）が起きる可能性があります。

これを防ぐために、Rails 6.1にアップグレードする際は、設定（ファイル`config/application.rb`や`config/initializers/new_framework_defaults_6_1.rb`）で、以下のように設定しておくと、以前の挙動（URLセーフでないCSRFトークンを生成）になります。

```ruby:config/initializers/new_framework_defaults_6_1.rb
Rails.application.config.action_controller.urlsafe_csrf_tokens = false
```

**参考**

* Add application config for URL-safe Base64 CSRF tokens · rails/rails@cf3736d
https://github.com/rails/rails/commit/cf3736dce8d478ed9cd10bc96a387ee8a94dd666

### `form_with`で`local: true`がデフォルトに

これまでは、フォームヘルパーの`form_with`のデフォルトが、`local: false`になっていて、コントローラー側ではAjaxのリクエストを処理するコードを書く必要がありましたが、これが変更になり、デフォルトが`local: true`に変更されました。

以下のようにアプリケーションで設定を行うことで、以前の挙動に戻すことも可能です。

```ruby:config/application.rb
config.action_view.form_with_generates_remote_forms = true
```

**参考**

* Change form_with to generate non-remote forms by default by p8 · Pull Request #40708 · rails/rails https://github.com/rails/rails/pull/40708

### `where.not`でNORではなく、NANDの条件のSQLを生成するように

これまでは、Active Recordの`where.not`に複数の条件式を書くと、以下のように`NOR`の条件式（Aではなくかつ、Bではない）のSQLを生成するようになっていましたが、

```ruby
User.where.not(name: "Jon", role: "admin")
# SELECT * FROM users WHERE name != 'Jon' AND role != 'admin'
```

Rails 6.1では以下のように、`NAND`の条件式（AかつBではない）のSQLを生成するようになりました。

```ruby
User.where.not(name: "Jon", role: "admin")
# SELECT * FROM users WHERE NOT (name == 'Jon' AND role == 'admin')
```

すでに、Rails 6.0で`NOR`の条件式の生成時に非推奨の警告を出していましたが、Rails 6.1では`NAND`の条件式の生成に切り替わっているので、注意が必要です。

**参考**

* Deprecate `where.not` working as NOR and will be changed to NAND in Rails 6.1 by kamipo · Pull Request #36029 · rails/rails https://github.com/rails/rails/pull/36029
* Rails 6 deprecates where.not as NOR & Rails 6.1 as NAND | BigBinary Blog https://blog.bigbinary.com/2019/07/31/rails-6-deprecates-where-not-working-as-nor-and-will-change-to-nand-in-rails-6-1.html

## 参考サイト

* Ruby on Rails 6.1 Release Notes — Ruby on Rails Guides
  https://edgeguides.rubyonrails.org/6_1_release_notes.html
* Rails 6.1: Horizontal Sharding, Multi-DB Improvements, Strict Loading, Destroy Associations in Background, Error Objects, and more! | Riding Rails
https://weblog.rubyonrails.org/2020/12/9/Rails-6-1-0-release/
* Rails 6.1 RC1: Horizontal Sharding, Multi-DB Improvements, Strict Loading, Destroy Associations in Background, Error Objects, and more! | Riding Rails
  https://weblog.rubyonrails.org/2020/11/2/Rails-6-1-rc1-release/
* Rails 6.1 RC2: Horizontal Sharding, Multi-DB Improvements, Strict Loading, Destroy Associations in Background, Error Objects, and more! | Riding Rails
  https://weblog.rubyonrails.org/2020/12/1/Rails-6-1-rc2-release/