88
55

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

RailsAdvent Calendar 2020

Day 23

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

Last updated at Posted at 2020-12-10

はじめに

昨年(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プロジェクトIssuesPull Requestsの内容をベースにして、Rails 6.1の主要な新機能・変更点の紹介と解説を行います。

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

注意点

Rubyのバージョン

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

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

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

参考

新機能

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

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

以下の例では、抽象クラスAnimalsRecordconnected_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/application.rb
config.active_record.legacy_connection_handling = false

参考

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

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_toshardを指定することで別の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を使用して)記述する必要があります。

参考

モデルの関連のStrict Loading

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

self.strict_loading_by_default = true

参考

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_identryable_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から、委譲先(関連先)のmessagecommentを取得して、使用することができます。

# 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>

参考

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

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

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

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

参考

ActiveModel::Errorオブジェクト

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

新しくエラー内容を作成するには、以下のようにActiveModel::Error.newを使用します。detailsfull_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を確認してください。

参考

機能追加

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

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

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

参考

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

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

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

参考

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

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

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

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

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

参考

HTTP Feature PolicyをDSLで指定可能に

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

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

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

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

参考

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

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

config/environments/development.rb
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>

参考

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

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

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

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

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

参考

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

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

以下の例のように、モデルのインスタンスの#signed_idメソッドを、有効期限expires_in、目的purposeを指定して呼び出すことでsigned idを生成し、モデルの.find_signedメソッドを生成したsigned_id、目的purposeを指定して呼び出すことで、レコードを取得します。signed_idpurposeが一致しない場合や、期限切れになった場合は、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は、トークンとしてメールの文中に埋め込むことができ、メール認証やパスワードリセットなどに使用できるようです。

参考

*_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_wasnilを返します。

参考

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

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

参考

変更点

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

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

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

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

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

参考

form_withlocal: trueがデフォルトに

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

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

config/application.rb
config.action_view.form_with_generates_remote_forms = true

参考

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の条件式の生成に切り替わっているので、注意が必要です。

参考

参考サイト

88
55
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
88
55

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?