はじめに
昨年(2019年)8月にRuby on Rails(以下Rails)バージョン6.0がリリースしてから1年4ヶ月経ち、ようやくバージョン6.1が2020年12月にリリースされました。
Rails 6.1は、複数データベースの接続の改善、水平分割シャーディングのサポート、Strict Loadingなど、Active Recordやデータベース周りの改善や機能強化を中心としたリリースになるようです。
本記事では、公式ブログのRiding Railsや、GitHubのRailsプロジェクトのIssuesやPull Requestsの内容をベースにして、Rails 6.1の主要な新機能・変更点の紹介と解説を行います。
※ 以前のバージョンのRailsの主要な新機能・変更点についてはこちら。
- Ruby on Rails 6の主要な新機能・機能追加・変更点
- Ruby on Rails 5.2の新機能(Active Storage, Content Security Policyなど)
- ReactやwebpackもサポートしたRails 5.1の新機能・変更点
- 今から知っておきたいRails 5の新機能・変更点
注意点
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ロールのデータベースから読み出しを行っています。
# 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の設定でレガシーな接続切り替えをオフにする必要があります。
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
で接続先のシャードが定義されているとします。
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)に書き込むことが可能になります。リファレンスにあるようにshard
を指定する場合はrole
も指定する必要があることに注意してください。
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
andconnects_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で行っていたようなことがRails標準でもできるようになりました。)
以下のような記述をモデル内に行うことで、関連先のstrict_loading
のデフォルト値を設定することもできます。
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と、モデル(クラス)名を入れておきます。
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
このようにすることで、以下のようなコードで、最新のメッセージとコメントを合わせて取得することができます。
Account.entries.order(created_at: :desc).limit(50)
ビューでは以下のようにして、Entryモデルのインスタンスであるentry
から、委譲先(関連先)のmessage
やcomment
を取得して、使用することができます。
# 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)が紐付いている場合に、バックグランドでコメントのレコードの削除を行います。
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
で、エラー内容をハッシュやメッセージの文字列で取り出すことが可能です。
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
メソッドでのエラーの絞り込みができるようになっています。
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
に保存しておいて、
get :top, to: 'top#index'
draw(:admin)
と書くことで、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ヘッダは、表示中のページ内で位置情報やカメラなどのブラウザの機能を使用することを許可するかもしくは拒否するかを設定できる仕組みですが、これをRailsの設定で記述できるようになりました。
Rails.application.config.feature_policy do |f|
f.geolocation :none
f.camera :none
f.payment "https://secure.example.com"
f.fullscreen :self
end
コントローラで個別に指定するようには以下のように記述します。
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内にコメントとして出力することが可能になりました。
config.action_view.annotate_template_file_names = true
下の例では、以下のようにERBファイルのパスが出力されています。(ERBファイル以外のテンプレートにはまだ対応していないようです。)
<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クラス名を直接記述していました。
<div class="<%= item.for_sale? ? 'active' : '' %>">
これが、class_names
ヘルパーを使用することでより簡潔に書けるようになり、引数のキーがCSSクラス名、値が条件式になっていて、条件式が真のときに、CSSクラス名を出力します。
<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
)を発生させることもできます。)
例
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
メソッドが追加されました。(*
は属性名)
例
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トークンを生成)になります。
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
に変更されました。
以下のようにアプリケーションで設定を行うことで、以前の挙動に戻すことも可能です。
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を生成するようになっていましたが、
User.where.not(name: "Jon", role: "admin")
# SELECT * FROM users WHERE name != 'Jon' AND role != 'admin'
Rails 6.1では以下のように、NAND
の条件式(AかつBではない)のSQLを生成するようになりました。
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/