Ruby on Rails 7.1の主要な新機能・機能追加・変更点

## はじめに

Rubyを使用したOSSのWebアプリケーションフレームワーク、Ruby on Rails（以下Rails）の最新バージョンである7.1（以下Rails 7.1）が[2023年10月5日にリリースされました](https://rubyonrails.org/2023/10/5/Rails-7-1-0-has-been-released)。

Rails 7.1はデプロイ用のDockerfileの自動生成、複合プライマリキーのサポート、Active Recordの非同期クエリの機能追加、Trilogy MySQLアダプタのサポートなど、多くの機能追加が行われました。

本記事では、[Railsの公式ブログ](https://rubyonrails.org/blog/)や[Railsガイド](https://guides.rubyonrails.org/v7.1/)、GitHubのRailsプロジェクトの[Issues](https://github.com/rails/rails/issues)や[Pull Requests](https://github.com/rails/rails/pulls)の内容をもとに、Rails 7.1の主要な機能追加・変更点の紹介を行います。

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

* [Ruby on Rails 7の主要な新機能・機能追加・変更点](https://qiita.com/ryohashimoto/items/f5382478c78f296d8291)
* [Ruby on Rails 6.1の主要な新機能・変更点](https://qiita.com/ryohashimoto/items/7caf905389ccb5d7d8fb)

## 注意点

### Rubyのバージョン

Rails 7.1を動かすには、Ruby 2.7.0以上が必要になります。可能であれば、現時点で最新のRuby 3.2系列にアップグレードを行ってください。

### Railsのサポート対象のバージョン

Rails 7.1のリリース後は、バグ修正の対象がバージョン7.1、セキュリティアップデートの対象が7.1、7.0、6.1系列となります。それより前のバージョンに関してはサポートは行われません。

**参考**

* [Maintenance Policy for Ruby on Rails](https://guides.rubyonrails.org/maintenance_policy.html)

## 新機能・機能追加

### Dockerfile（デプロイ用）の自動生成

アプリケーションを`rails new`で新規作成した際に、本番環境デプロイ用のDockerfileをはじめとした、Docker関連のファイルを自動生成するようになりました。
（`--skip-docker`オプションを渡すことで、Docker関連のファイルの生成をスキップすることができます。）

以下のコマンドでDockerイメージのビルドとボリューム作成、実行を行うことができます。

```console
$ docker build -t app .
$ docker volume create app-storage
$ docker run --rm -it -v app-storage:/rails/storage -p 3000:3000 --env RAILS_MASTER_KEY=<config/master.keyの値> app
```

※ 元々この機能は本番環境へのデプロイを想定していて、開発環境で使用することを意図していないことに注意してください。（開発環境ではHTTPSの設定や開発用のRubyGemsパッケージのインストールなどカスタマイズが必要です。）

**参考**
* [Add default Dockerfiles by dhh · Pull Request #46762 · rails/rails](
https://github.com/rails/rails/pull/46762)

### 複合プライマリキー

複合プライマリキーがサポートされ、マイグレーションや、Active RecordのクエリメソッドなどRailsのアプリケーション全般で使用できるようになりました。

DBに複合プライマリキーを使用したテーブルを作成するには、以下のようにマイグレーションの`change_table`の`primary_key:`オプションにカラム名の配列を渡します。

```ruby
class CreateProducts < ActiveRecord::Migration[7.1]
  def change
    create_table :products, primary_key: [:store_id, :sku] do |t|
      t.integer :store_id
      t.string :sku
      t.text :description
    end
  end
end
```

`find`や`where`のActive Recordのクエリメソッドでも、複合プライマリキーに対応する引数を指定することができるようになっています。

```ruby
Product.find([3, "XYZ12345"]) # store_id: 3, sku: "XYZ12345"を指定
Product.where(Product.primary_key => [[1, "ABC98765"], [7, "ZZZ11111"]]) # store_id: 3, sku: "XYZ12345"とstore_id: 7, sku: "ZZZ11111"
```

`first`メソッドでは、2つのプライマリキーを組み合わせた順番で並び替えを行い、その最初のレコードを取得するようになっています。

```ruby
Product.first # SQLで実行: SELECT * FROM products ORDER BY products.store_id ASC, products.sku ASC LIMIT 1
```

モデルの関連で、従来の`id`ではなく、複合プライマリキーを使用するには、`has_many`や`belongs_to`などのオプションで`query_constraints`を使用します。

```ruby
class Author < ApplicationRecord
  self.primary_key = [:first_name, :last_name]
  has_many :books, query_constraints: [:first_name, :last_name]
end

class Book < ApplicationRecord
  belongs_to :author, query_constraints: [:author_first_name, :author_last_name]
end
```

他にも、フォームへルパやルーティングなどでもサポートされます。詳しい内容に関しては、以下のRails guidesの記事を参照してください。

**参考**
* [Composite Primary Keys — Ruby on Rails Guides](https://guides.rubyonrails.org/v7.1/active_record_composite_primary_keys.html)


### `ActiveRecord::Base.normalizes`によるモデルの属性の正規化

メールアドレスの小文字化、電話番号のハイフン削除などの正規化を、モデル内に以下のように`normalizes`で指定することで、属性に値がアサインされたり、更新された際に行うことができるようになりました。

```ruby
class User < ApplicationRecord
  normalizes :email, with: -> email { email.downcase }
  normalizes :tel, with: -> tel { tel.delete("-") }
end
```

上記のモデル定義を使用した場合、以下のように`email`と`tel`の正規化が行われます。

```ruby
 user = User.new(email: 'JOHN@example.com', tel: '01-2345-6789')
=> #<User:0x0000000108142a68 id: nil, email: "john@example.com", tel: "0123456789", created_at: nil, updated_at: nil>
```

`normalizes`を指定している場合、Active Recordの`where`、`exists?`に正規化前の値を渡すと、正規化されてSQLが実行されます。

```ruby
> User.where(email: 'JOHN@example.com')
  User Load (0.2ms)  SELECT "users".* FROM "users" WHERE "users"."email" = ? /* loading for pp */ LIMIT ?  [["email", "john@example.com"], ["LIMIT", 11]]
> 9> User.exists?(email: 'JOHN@example.com')
  User Exists? (0.3ms)  SELECT 1 AS one FROM "users" WHERE "users"."email" = ? LIMIT ?  [["email", "john@example.com"], ["LIMIT", 1]]
```

**参考**
* [ActiveRecord::Normalization::ClassMethods](https://api.rubyonrails.org/v7.1/classes/ActiveRecord/Normalization/ClassMethods.html#method-i-normalizes)

### Active Recordに非同期クエリのメソッドが追加

Active Recordに、以下の`async_`で始まる各種の非同期クエリのメソッドが追加されました。

* `async_count`
* `async_sum`
* `async_minimum`
* `async_maximum`
* `async_average`
* `async_pluck`
* `async_pick`
* `async_ids`
* `async_find_by_sql`
* `async_count_by_sql`

これらのメソッドは呼出後に、クエリの実行状態を持つ`ActiveRecord::Promise`のオブジェクトを返し、実行が完了している場合は、`#value`メソッドで値を取得することができます。

```ruby
promise = Post.where(published: true).async_count
promise.value # => 10
```

**参考**
* [Active Record API for general async queries by casperisfine · Pull Request #44446 · rails/rails](https://github.com/rails/rails/pull/44446)
* [ActiveRecord::Promise](https://api.rubyonrails.org/classes/ActiveRecord/Promise.html)


### Trilogy MySQLアダプタのサポート

GitHubによってOSS化された、MySQL互換のDBに対するアダプタであるTrilogyがサポートされるようになりました。
trilogyは従来のmysql2アダプタに比べて高速で軽量に動作し、GitHub以外にもShopifyなどで利用されています。(詳しくは参考にあげたページを参照してください。)

Railsアプリケーションでは`config/database.yml`で以下のように`adapter: trilogy`を指定することで、従来のmysql2アダプタから移行することができます。

```yaml
development:
  adapter: trilogy
  database: blog_development
  pool: 5
```

**参考**
* [Introduce adapter for Trilogy, a MySQL-compatible DB client by adrianna-chang-shopify · Pull Request #47880 · rails/rails](https://github.com/rails/rails/pull/47880)
* [Introducing Trilogy: a new database adapter for Ruby on Rails - The GitHub Blog](https://github.blog/2022-08-25-introducing-trilogy-a-new-database-adapter-for-ruby-on-rails/)
* [Benchmarking Trilogy : Is it really the future of Rails ?](https://www.linkedin.com/pulse/benchmarking-trilogy-really-future-rails-hugo-vast)

### ジョブの一括エンキュー（`perform_all_later`）

複数のジョブを一括でエンキューする、`perform_all_later`が`ActiveJob`のクラスメソッドとして追加されました。
このメソッドを使用することで、[Sidekiq](https://github.com/sidekiq/sidekiq)など、Active Jobのアダプタで`enqueue_all`メソッドを使用できるジョブ実行システムの場合では、個別にジョブをエンキューすることなく効率よくジョブを実行できるようになっています。（Sidekiqの場合は[push_bulk](https://github.com/sidekiq/sidekiq/wiki/Bulk-Queueing)で処理が行われます。）
この場合Active Jobのエンキュー用のコールバックが実行されないことに注意してください。
また、`enqueue_all`を使用できない場合は、ループで個別のジョブをエンキューします。

**参考**
* [Add `perform_all_later` to enqueue multiple jobs at once by Mangara · Pull Request #46603 · rails/rails](https://github.com/rails/rails/pull/46603)
* [ActiveJob.perform_all_later](https://api.rubyonrails.org/v7.1/classes/ActiveJob.html#method-c-perform_all_later)

### ActiveSupport::MessagePack

Rubyオブジェクトをシリアライズする際に従来はJSONやMarshalのシリアライザが使用されていましたが、より高速で、データが軽量になる[msgpack gem](https://rubygems.org/gems/msgpack)を使用した[ActiveSupport::MessagePack](https://api.rubyonrails.org/v7.1/classes/ActiveSupport/MessagePack.html)が利用できるようにようになりました。
Railsアプリケーションの各種設定でも、以下の例のように`:message_pack`が指定できるようになりました。

```ruby
config.active_support.message_serializer = :message_pack
config.action_dispatch.cookies_serializer = :message_pack
config.cache_store = :file_store, "tmp/cache", { serializer: :message_pack }
```

**参考**

* [Add `ActiveSupport::MessagePack` by jonathanhefner · Pull Request #47770 · rails/rails](https://github.com/rails/rails/pull/47770)

### `config.autoload_lib` / `config.autoload_lib_once`

Railsアプリケーションの`lib/`ディレクトリ以下にあるクラスやモジュールを自動読み込みする設定が追加されました。
以下のように指定することで、`lib/assets`、`lib/tasks`、`lib/generators`以外の`lib`ディレクトリ内のファイルにある、モジュールやクラスが起動時に自動読み込みされ、起動中にファイルの変更を行った場合は（モデルやコントローラと同様に）リロードされます。

```ruby
config.autoload_lib(ignore: %w[assets tasks generators])
```

`autoload_lib`の代わりに`config.autoload_lib_once`を使用した場合は、起動時に1度だけ読み込まれ、変更後はリロードされません。

**参考**
* [Autoloading and Reloading Constants — Ruby on Rails Guides](https://edgeguides.rubyonrails.org/autoloading_and_reloading_constants.html#config-autoload-lib-ignore)

### JSONとHTMLの`response.parsed_body`でパターンマッチングが利用できるように

`ActionDispatch::IntegrationTest`を使用した際に、`response.parsed_body`に対して以下のようにパターンマッチングを使用したテストを書くことができるようになりました。

以下はコンテンツがJSONの場合で、Rails 6から`response.parsed_body`が`ActiveSupport::HashWithIndifferentAccess`または`Array`を返すようになっており、それに対してパターンマッチングを行なっています。
（以下の例ではMinitestに追加された`assert_pattern`でテストを行なっています。）

```ruby
get "/posts/42.json" # JSONで{ "title": "Title" }を返す
assert_pattern { response.parsed_body => [{ title: /title/i }] } # マッチする
```

また、コンテンツがHTMLの場合は、Rails 7.1で`response.parsed_body`が`Nokogiri::HTML5::Document`を返すようになり、それに対してパターンマッチングを行えるようになっています。

```ruby
get "/posts" # <main><h1>Some main content</h1></main>を含むHTMLを返す
assert_pattern { html.at("main") => { content: "Some main content" } } # マッチする
```

**参考**
* [Ensure `response.parsed_body` support for pattern matching by seanpdoyle · Pull Request #49003 · rails/rails](https://github.com/rails/rails/pull/49003)


## 変更点

### 自動読み込みのパスがデフォルトで`$LOAD_PATH`に追加されなくなった

Rails 7.1から自動読み込みパスを`$LOAD_PATH`に追加しないようになりました。これは、Rails 7.0からZeitwerkモードでの起動が必須になり、Zeitwerkは内部的に絶対パスを使用していて、`require`や`require_relative`のように`$LOAD_PATH`に登録された情報は使用しないからのようです。
（`lib`ディレクトリはZeitwerk外からも使用されるため、`$LOAD_PATH`に追加されます。）

**参考**

* [2.1 Autoloaded paths are no longer in $LOAD_PATH](https://guides.rubyonrails.org/upgrading_ruby_on_rails.html#autoloaded-paths-are-no-longer-in-$load-path)

## 参考

* [Ruby on Rails — Rails 7.1: Dockerfiles, BYO Authentication, More Async Queries, and more!](https://rubyonrails.org/2023/10/5/Rails-7-1-0-has-been-released)
* [Ruby on Rails 7.1 Release Notes — Ruby on Rails Guides](https://edgeguides.rubyonrails.org/7_1_release_notes.html)