Rails 7.1に入る主要な機能まとめ(1)update_attribute!、CTEサポートほか(翻訳)
🔗 はじめに
2022年のRuby on Railsのリポジトリの動きは驚くほど活発でした。フレームワークに豊富な新機能が導入され、多くのバグも修正され、パフォーマンス改善も数え切れないほどです。
Ruby on Railsリポジトリでは多くのことが行われているので、ついていくのは大変です。しかし本記事では、Railsの次期マイナーリリースである7.1に取り入れられる素晴らしい新機能や改良点のいくつかについて焦点を当ててみたいと思います。
本ブログ記事シリーズでは、2022年を通じてRailsリポジトリで行われた新機能や改良点の中から私の好きなものを紹介していきます。今回は3部構成の第1弾につき、本年のRailsのエキサイティングな開発内容の包括的な説明については今後の2記事をどうぞお楽しみに。Twitterで私をフォローいただければ更新情報をお知らせします。Twitterをご利用でない方は、元記事末尾でニュースレターを購読いただけます。
Ruby on Railsの最新動向を知りたい方には、HEYが毎週発行するThis Week In Railsの購読をおすすめします。
注: 本記事のサンプルコードはさまざまなプルリクやドキュメントから引用したものに手を加えたものであり、そのまま動作することを保証するテストは行われていませんのでご了承ください。なお、紹介する機能はすべてマージ済みです。
それでは始めましょう。
🔗 01. Rails 7.0.1がリリース
2022年はRails 7.0.1のリリースで幕を開けました。Ruby 3.1のサポートに加えてさまざまなバグ修正やドキュメントの改善も行われています。
2022年後半にはRailsのWebpackerが役目を終えてリタイアしました↓。Webpackerは5年以上の長きに渡って、コンパイルおよびバンドルされたJavaScriptとの橋渡しを担ってきました。RailsではWebpacker、Turbolinks、UJSを置き換える形で、import maps、Turbo、Stimulusがデフォルトオプションとして導入されました。
RETIREMENT: Webpacker has served the Rails community for over five years as a bridge to compiled and bundled JavaScript. This bridge is no longer needed for most people in most situations following the release of Rails 7.https://t.co/vIp3DeG82L
— Ruby on Rails (@rails) January 19, 2022
Webpacker引退のニュースで世間が沸き立ったときのことが今も鮮明に思い出されます。Webpackerは多くの開発者にとって悩みのタネでした。./bin/importmapを導入してNPMパッケージをpin/unpinできるようになったことで、Railsで作業する喜びがよみがえったように思います。
🔗 02. $LOAD_PATHへのオートロードパス追加が無効になった
Rails 7.1以降は、オートローダーが管理するパスが$LOAD_PATHに追加されなくなります(#44133)。つまり、手動でrequireしても読み込めなくなり、代わりにクラスやモジュールを直接参照できるようになります。この変更を実現するため、Rails 7.1以降はconfig.add_autoload_paths_to_load_pathコンフィグオプションがfalseに設定されます。
:zeitwerkモードで動かす場合は、config/application.rbファイルでconfig.add_autoload_paths_to_load_pathをfalseに設定することが推奨されます。理由は、Zeitwerkが内部で絶対パスを利用しているためです。require_dependencyが使われていなければ、モデル、コントローラ、ジョブなどのファイルをLOAD_PATHに配置する必要はありません。
config.add_autoload_paths_to_load_pathをfalseに設定すると、require相対パスでを解決するときにRubyがそれらのディレクトリをチェックしなくなります。Bootsnap gemがそれらのディレクトリのインデックスを構築する必要がなくなるため、Bootsnapの負荷とメモリ使用量が軽減され、アプリケーションのパフォーマンスも向上します。
🔗 03. #update_attribute!メソッドが追加された
ActiveRecord::Persistence#update_attribute!メソッドが新たに追加されました(#44141)。このメソッドはupdate_attributeと似ていますが、saveではなくsave!を呼ぶ点が異なります。
class Topic < ActiveRecord::Base
  before_save :check_title
  def check_title
    throw(:abort) if title == "abort"
  end
end
topic = Topic.create(title: "Test Title")
# => #<Topic title: "Test Title">
topic.update_attribute!(:title, "Another Title")
# => #<Topic title: "Another Title">
topic.update_attribute!(:title, "abort")
# raises ActiveRecord::RecordNotSaved
ActiveRecord::Persistence#update_attribute!は、属性がreadonlyの場合にActiveRecord::ActiveRecordErrorが発生します。
🔗 04. Dart Sass for Railsがリリース
Railsが新たにリリースしたdartsass-rails gemは、DartベースのSassのスタンドアロン実行可能ファイルバージョンをRails 7で使えるようラップしたものです。
dartsass-railsはRailsでSassスタイルシートを使いやすくするためのもので、従来使われていたRuby Sassが非推奨化されたことに伴って置き換えられました。このgemによって、Rails開発者はRailsで作業しながらSassの最新機能を利用できるようになります。
新しいRailsアプリでは、以下を実行することでDart Sassをインストールできます。
./bin/bundle add dartsass-rails
./bin/rails dartsass:install
このインストーラはSassのデフォルトの入力ファイルapp/assets/stylesheets/application.scssを作成します。コンパイルするすべてのスタイルファイルをここでインポートします。rails dartsass:buildを実行すると、この入力ファイルが出力CSSファイルapp/assets/builds/application.cssの生成に使われ、これによってアプリケーションでインクルードできるようになります。
🔗 05. #stub_constメソッドが追加
#stub_constメソッドが新たに追加されました(#44294)。このメソッドは、そのブロック内で定数の値を手軽に変更し、警告を抑制します。ただし、この実装はパラレルテストを有効にしている場合はスレッドセーフになりません(参考)。
# World::List::Import::LARGE_IMPORT_THRESHOLD = 5000
stub_const(World::List::Import, :LARGE_IMPORT_THRESHOLD, 1) do
  assert_equal 1, World::List::Import::LARGE_IMPORT_THRESHOLD
end
assert_equal 5000, World::List::Import::LARGE_IMPORT_THRESHOLD = 5000
上の例では、World::List::Import::LARGE_IMPORT_THRESHOLD to 5000を設定する代わりにこのメソッドを使うことで警告の発生を防ぎ、テストが終了すると元の値が復元されます。
ただし、マルチスレッド環境で定数をスタブ化すると予期しない結果が発生する可能性がある点にご注意ください。複数のスレッドが同じ定数に依存していて、それぞれのスレッドが定数のスタブ化を試みると、スタブが衝突して予測不能な振る舞いを示す可能性があります。この問題を回避するには、コンカレントなスレッド内の定数をスタブ化する場合(独立したテストスイートをパラレル実行する場合など)の影響を慎重に検討することが重要です。
🔗 06. 関連付けが見つからない場合のエラーメッセージが改善された
従来はwhere.associatedで関連付けが見つからない場合に出力されるメッセージがわかりにくかっのですが、わかりやすいメッセージに変更されました(#44227)。これは例で示す方がわかりやすいでしょう。
Postモデルにcarsという名前の関連付けがない場合、従来は以下のようなメッセージが表示されました。
Post.where.associated(:cars).to_a
# => NoMethodError: undefined method `table_name' for nil:NilClass
改善後は以下のようになります。
Post.where.associated(:cars).to_a
# => ArgumentError: An association named `:cars` does not
# exist on the model `Post`.
この方がずっとわかりやすいですね。
🔗 07. ActiveRecord::ConnectionPoolがFiberセーフになった
RailsのActiveRecord::ConnectionPoolがFiberセーフになりました(#44219)。Railsにはスレッド中心のコードがたくさんあり、データベースとのI/Oが基本的にスレッドで行われるので、このプルリクによってコネクションプールとのやりとりを切り替えられるようにしました。たとえば、falcon gemのようなFiberベースのジョブプロセッサやサーバーを使っている場合は、config.active_support.isolation_levelを:fiberに設定することで、同一スレッド内にある複数のfiberを用いてコネクションを管理できるようになります。
🔗 08. Rails 7.0.2がリリース
2022/02/08にRails 7.0.2がリリースされました。このリリースには、問題のある機能が取り消すパッチが含まれており(#38957)、利用中のRailsバージョンに基づいてデータベーススキーマのバージョニングを行う機能も導入されました(#44286)。この新機能によって、既存のRailsアプリケーションがRails 6.1で生成されたデータベーススキーマを引き続き利用できるようになり、振る舞いを維持しつつproduction環境のデータベーススキーマを一貫させることが可能になりました。
🔗 09. #to_s(:format)が#to_fs(:format)に置き換えられた
#to_s(:format)メソッドは少し前に非推奨化され、代わりに#to_formatted_s(:format)を使うことになりました(#43772)。
以前は#to_fs(:format)が#to_formatted_s(:format)のエイリアスでしたが、#44354で入れ替えられ、#to_formatted_s(:format)が#to_fs(:format)のエイリアスになりました。そのようになった理由は、DHHによると#to_formatted_s(:format)は利用頻度が高いのにメソッド名が長すぎるというものです。私も同感です。
🔗 10. has_secure_passwordによるパスワードチャレンジが追加
パスワードのチャレンジが、パスワードの確認と同じぐらい手軽に実装できるようになりました(#43688)。ビューとコントローラで同じエラーハンドリングロジックを再利用しています。
この改修は、has_secure_passwordを拡張してpassword_challengeアクセサと適切なバリデーションを定義します。password_challengeが設定されると、現在永続化されているpassword_digest(つまりpassword_digest_was)と一致するかどうかがチェックされます。
たとえば、従来コントローラでは以下のように書いていましたが、
password_params = params.require(:password).permit(
  :password_challenge,
  :password,
  :password_confirmation,
)
password_challenge = password_params.delete(:password_challenge)
@password_challenge_failed = !current_user.authenticate(password_challenge)
if !@password_challenge_failed && current_user.update(password_params)
  # 何かする
end
以下のように書けます。
password_params = params.require(:password).permit(
  :password_challenge,
  :password,
  :password_confirmation,
).with_defaults(password_challenge: "")
if current_user.update(password_params)
  # 何かする
end
このプルリク作者ほどうまく説明できなかったので、プルリクの記述をそのまま引用しました。Jonathanに感謝いたします。
🔗 11. ActiveStorage: 添付ファイルをレコードに保存するとblobを返すようになった
添付ファイルを#attachメソッドでレコードに保存すると、レコードにアタッチされたblobまたはblobの配列を返すようになりました(#44439)。つまり、添付ファイルに対して直接blob用メソッドを呼び出せるようになりました。#attachでレコードの保存に失敗すると、falseが返されます。
@user = User.create!(name: "Josh")
avatar = @user.avatar.attach(params[:avatar]) # => ブーリアンを返す
# 以下のようにblob用メソッドを直接呼び出せるようになった
avatar.download
avatar.url
avatar.variant(:thumb)
🔗 12. audio_tagとvideo_tagにAttachmentオブジェクトを渡せるようになった
Railsのaudio_tagとvideo_tagが拡張されて、ActiveStorage::Attachmentを受け取れるようになりました(#44085)。
audio_tag(polymorphic_path(user.audio_file))
video_tag(polymorphic_path(user.video_file))
上のように書く代わりに、以下のように書けるようになりました。
audio_tag(user.audio_file)
video_tag(user.video_file)
🔗 13. #destroy_association_async_batch_sizeが追加された
 dependent: :destroy_async関連付けオプションを指定したときに1件のバックグラウウドジョブで削除する最大レコード数を指定できるActiveRecord.destroy_association_async_batch_sizeが新たにRailsに追加されました(#44617)。従属するレコード数が指定のバッチサイズよりも大きい場合は、複数のバックグラウンドジョブで削除され、そうでない場合は1件のバックグラウンドジョブで削除されます。このメソッドは、アプリケーションを占有せずに多数のレコードを効率よく削除するのに有用です。
🔗 14. Active Recordに一般的な非同期クエリ用APIが追加された
Active Recordに新たな一般的な非同期クエリ用APIが追加されました(#44446)。Rails 7.1では、集約系メソッド、単一レコードを返すメソッド、およびRelationとfind_by_sql以外の全メソッドを非同期実行できるようになります。これによって、以下のように書けるようになります。
Post.where(published: true).count # => 2
promise = Post.where(published: true).async_count
# => #<ActiveRecord::Promise status=pending>
promise.value # => 2
# 以下のすべてのメソッドもサポートされる
=begin
async_sum
async_minimum
async_maximum
async_average
async_pluck
async_pick
async_find_by_sql
async_count_by_sql
=end
🔗 15. CSRFトークンをセッションの外に保存可能になった
CSRFトークンがセッションの外でも保存できるようになりました(#44283)。セッションがcookiesに保存されない場合、CSRFトークンを保存するためだけに数百万ものセッションを作成し、絶えずそこに退避させることになってしまいます。Railsはこの問題に対処するために新しい設定オプションを追加し、CSRFトークンを任意の場所に保存可能なlambdaを提供できるようになりました。
以下のように、CSRFトークン保存用のカスタムストラテジークラスを実装することも可能です。
class CustomStore
  def fetch(request)
    # カスタムの保存場所にあるトークンを返す
  end
  def store(request, csrf_token)
    # トークンをカスタムの保存場所に保存する
  end
  def reset(request)
    # 保存されているセッショントークンを削除する
  end
end
class ApplicationController < ActionController:x:Base
  protect_from_forgery store: CustomStore.new
end
🔗 16. システムテストのスクリーンショットヘルパーにキーワード引数が2つ追加された
screenshot:とhtml:キーワード引数を用いて、システムテスト実行時にテストのスクリーンショットを保存するかHTMLダンプを保存するかを個別に指定できるようになりました(#44720)。
以下は具体的な例です。
# スクリーンショットを撮影してiTermで表示し、
# HTMLもファイルにダンプして両方のパスを出力する
take_screenshot(html: true, screenshot: "inline")
# HTMLをファイルにダンプしてパスを出力する
take_screenshot(html: true)
# スクリーンショットを撮影してターミナルで表示し、パスを出力する
take_screenshot(screenshot: "artifact")
# スクリーンショットを撮影してパスを出力する
take_screenshot
17. 🔗 デフォルト値付きのカラムで暗号化属性を利用可能になった
従来は、デフォルト値付きのカラムに定義された暗号化済み属性を読み込むと、これらのカラムが暗号化されていないため、config.active_record.encryption.support_unencrypted_dataを有効にしない限りエラーが発生しました。これらの値は、コンフィグの設定にかかわらず暗号化されるようになりました(#45033)。
18. 🔗 Active Modelにパターンマッチングが導入された(その後取り消し)
更新情報: このプルリクは#45553で取り消されました。zverok_khaによる注意喚起のおかげです。新しいrails-pattern_matching gemでこの機能をとりあえず代替できます(#45070)。
おそらく2022年で最も愛されているであろう#45035は、Ruby 2.7以降で導入されたハッシュパターン向けのパターンマッチングインターフェイスを導入し、ActiveModel::AttributeMethodsモジュールをincludeする任意のオブジェクト(ActiveRecord::Baseなど)でパターンマッチングを実行できるようにします。
以下のような楽しいことができます。
case Current.user
in { superuser: true }
  "Thanks for logging in. You are a superuser."
in { admin: true, name: }
  "Thanks for logging in, admin #{name}!"
in { name: }
  "Welcome, #{name}!"
end
以下は別の例です。
class Person
  include ActiveModel::AttributeMethods
  attr_accessor :name
  define_attribute_method :name
end
def greeting_for(person)
  case person
  in { name: "Mary" }
    "Welcome back, Mary!"
  in { name: }
    "Welcome, stranger!"
  end
end
person = Person.new
person.name = "Mary"
greeting_for(person) # => "Welcome back, Mary!"
person = Person.new
person.name = "Bob"
greeting_for(person) # => "Welcome, stranger!"
19. 🔗 db_runtimeがActive Job instrumentationに追加された
perform.active_jobイベントの通知ペイロードにdb_runtimeが導入されました(#40058)。改修後のdb_runtimeは、ジョブ実行中にデータベースクエリで要した合計時間(単位: ms)をトラッキングし、ジョブの時間がどのように消費されたかをより詳しく理解できるようになります。
20. 🔗 PostgreSQLのインデックスに有効性チェックが追加された
たとえばadd_index :account, :active, algorithm: :concurrentlyのような方法でインデックスを作成すると、無効なインデックスが作成される可能性があります。改修後はインデックスが有効かどうかをチェックできるようになりました(#45160)。
connection.index_exists?(:users, :email, valid: true)
connection.indexes(:users).select(&:valid?)
21. 🔗 EXCLUDE制約のサポートが追加された
PostgreSQLのみの機能ですが、Active RecordのマイグレーションやスキーマダンプでPostgreSQLのEXCLUSION制約をサポートするように拡張されました(#40224)。
add_exclusion_constraint :invoices,
  "daterange(start_date, end_date) WITH &&",
  using: :gist,
  name: "invoices_date_overlap"
remove_exclusion_constraint :invoices, name: "invoices_date_overlap"
22. 🔗 Messageverifierのイニシャライザにurlsafe:オプションが追加された
MessageVerifierとMessageEncryptorのコンストラクタが:urlsafeオプションを受け取るようになりました(#45419)。このオプションを有効にすると、メッセージでURLセーフなエンコーディングが行われるようになります。イニシャライザにurlsafe: trueを渡すと、URLセーフな文字列が返されます。
verifier = ActiveSupport::MessageVerifier.new(urlsafe: true)
message = verifier.generate(data) # => "urlsafe_string"
23. 🔗  in_batchesでDESCを指定できるようになった
in_batchesの呼び出しで並び順を指定する機能が追加されました(#45282)。
Post.in_batches(order: :desc).each {}
# 以下の結果が得られる:
# Post Pluck (0.1ms)
# SELECT "posts"."id"
# FROM "posts"
# ORDER BY "posts"."id" DESC LIMIT ?  [["LIMIT", 1000]]
以前は並び順を指定できなかったのが、指定できるようになりました。これはバグ修正と呼ぶべきか、それとも機能と呼ぶべきでしょうか?
24. 🔗 空のデータベースが存在していてもdb:prepareでスキーマを読み込めるようになった
従来のdb:prepareは、データベースが存在していてもテーブルが存在しない場合にすべてのマイグレーションを実行していました。改修後のdb:prepareは、初期化されていないデータベースが存在する場合はスキーマを読み込んで、マイグレーション後にスキーマをダンプするようになりました(#45464)。
25. 🔗 ジョブジェネレータでジョブの親クラスを指定できるようになった
Railsのジョブジェネレータに、親クラスを指定する--parentオプションが追加されました(#45528)。これにより、ジョブジェネレータでスーパークラスをオプション指定できます。
bin/rails g job process_payment --parent=payment_job
上のコマンドを実行すると、以下が生成されます。
class ProcessPaymentJob < PaymentJob
  # ここに何か書く
end
26. 🔗 datetime_fieldにinclude_secondsオプションが追加された
input要素の時刻に"秒"ビットが含まれていない場合、ブラウザは時刻を「秒あり」でレンダリングします。改修後はinclude_seconds: falseオプションを指定することで、フォーマットされた時刻の秒表示を省略できるようになります(#45188)。
つまり、以下のようなコードで
datetime_field("user", "born_on", include_seconds: false)
以下が生成されるようになります。
<input
  id="user_born_on"
  name="user[born_on]"
  type="datetime-local"
  value="2014-05-20T14:35"
/>
27. 🔗 RailsにCommon Table Expressionsのサポートが追加された
モデルに対して.withクエリメソッドを使うことで、CTE(Common Table Expressions)による洗練されたクエリをビルドできるようになりました(#37944)。.withでは、Arel::Nodes::Asノードを手動でビルドせずにActive Recordリレーションを利用できます。
28. 🔗 previewで定義済みバリアントが使えるようになった
Active Storageの添付ファイルでpreviewメソッドやrepresentationメソッドを呼び出すときに定義済みバリアントを利用する機能が導入されました(#45098)。これにより、添付ファイルの扱いをより柔軟にカスタマイズできるようになります。
たとえば以下のように、previewやrepresentationを生成するときに、添付ファイルでどのバリアントを使いたいかを指定できるようになりました。
class User < ActiveRecord::Base
  has_one_attached :file do |attachable|
    attachable.variant :thumb, resize_to_limit: [100, 100]
  end
end
続いて、定義されたバリアントを以下のように使います。
<%= image_tag user.file.representation(:thumb) %>
29. 使われていないルーティングを検出するroutes --unusedが追加された
Railsアプリは、ルーティングが多いというだけで時とともに遅くなることがあります。routesコマンドに追加された新しいオプションを使って、表示されるが有効でないルーティングを検出できるようになりました(#45701)。
bin/rails routes --unused
Found 2 unused routes:
Prefix Verb URI Pattern    Controller#Action
   one GET  /one(.:format) controller#one
   two GET  /two(.:format) controller#two
30. 🔗 ビューテンプレートが受け取れるローカル変数をコメントで定義できるようになった
デフォルト値を持つ必須引数をビューテンプレートで定義できるようになりました(#45602)。この更新の前は、テンプレートに任意のローカル変数をキーワード引数として渡すことが可能でしたが、テンプレートが受け付ける特定のローカル変数をマジックコメントの形で定義できるようになりました。
この拡張によって、テンプレートの振る舞いや機能をより細かく制御したりカスタマイズしたりできるようになります。
従来のパーシャルは以下のような感じでした。
<% title = local_assigns[:title] || "Default title" %>
<% comment_count = local_assigns[:comment_count] || 0 %>
<h2><%= title %></h2>
<span class="comment-count"><%= comment_count %></span>
改修後は、同じパーシャルを以下のようにずっとシンプルに書けます。
<%# locals: (title: "Default title", comment_count: 0) %>
<h2><%= title %></h2>
<span class="comment-count"><%= comment_count %></span>
何と素晴らしいことでしょう。
パート1のまとめ
以上で、本シリーズにおけるThis Week In Railsのまとめ、すなわちRails 7.1に入る主要な機能まとめのパート1はおしまいです。私が書いている本記事および次の2本の記事を合わせても、Rails 7.1に入る多くの機能のほんのさわりに過ぎません。本記事でカバーできなかった細かな機能は無数にあります。また、本シリーズではバグ修正やパフォーマンス改善について触れていない点が重要であることも申し上げておきます。
これほどの機能追加を可能にした485人のコントリビューターの皆さんに、他のRails開発者を代表して感謝申し上げたいと思います。
わたくしを含むThis Week In Railsチームの方々、すなわちGreg Molnar、Petrik de Heus、Wojciech Wnętrzakのおかげで、あらゆる変更点を追いかけてお届けする作業が楽になったことに特別に感謝申し上げます。This Week In Railsを購読いただければ、今後更新情報を毎週メールで受け取れるようになります。
お知らせ
元記事末尾で私からのニュースレターを購読いただくと、今後公開予定の素晴らしいRubyイディオムやヒント集(電子書籍)の情報が配信されます。Ruby、JavaScript、Web技術を探求したい方はTwitterでフォローしてください。
関連記事
Rails 7.1に入る主要な機能まとめ(2)error_highlight対応、routes --grepほか(翻訳)
      
概要
原著者の許諾を得て翻訳・公開いたします。
日本語タイトルは内容に即したものにしました。原文の目次は省略しました。また、週刊Railsウォッチの該当項目へのリンクも追加してあります。