Rails7.2の新機能と変更点をまとめる【後編】

Rails7.2がbetaフェーズとなっています。

リリースに先立ち、新機能や変更点などをまとめつつ、実際に検証してみました。

[前編](https://qiita.com/Kazuyaa/items/859568c8f7cbe9a78ddf)も併せてご確認ください。

## トランザクションの最中にActive Jobをキューに渡さない
トランザクションの最中でActive Jobをエンキューした場合に、トランザクションのコミット完了までエンキューが遅延されるようになりました。

トランザクションがロールバックされた場合はジョブを削除します。

トランザクション内でジョブをエンキューしてしまうと、コミットが完了する前にジョブが別プロセスで実行されてしまう可能性があるからです。

例えば以下のコードがある場合、コミット完了前にジョブが実行されてしまうと、実行時点でまだ`user`は存在しないため、エラーが発生します。

```ruby
ActiveRecord::Base.transaction do
  user = User.create!(...)
  HogeJob.perform_later(user.id)
end
```

一方で必ずしもトランザクション完了前にジョブが実行されるとも限らないため、ある時はエラー・ある時は正常動作という予測不可能な状況が生まれてしまうのです。

こういったデータ不整合の問題を未然に防ぐためRails7.2から変更が加わりました。

`enqueue_after_transaction_commit`オプションを指定することで、ジョブ毎にトランザクションの最中にエンキューするか、コミット後にエンキューするか選択できるようになります。

以下のIssueで議論がされていました。

https://github.com/rails/rails/issues/26045

なおこの議論は2016年から続いていた模様です。

### 実際にやってみた
まずは`Post`テーブルと`Post`コントローラーを作成します。
```bash
$ rails g model Post content:text
$ rails db:migrate
$ rails g controller posts new create
```

作成したコントローラーに以下の記述を追加します。またルーティングも作成します。
```ruby:app/controllers/posts_controller.rb
class PostsController < ApplicationController
  def new
  end

  def create
    ActiveRecord::Base.transaction do
      post = Post.create(content: "New Content")
      # 後ほど作成するJob
      UpdatePostJob.perform_later(post.id)
      # Jobの実行タイミングを観察するためにわざと例外を発生させる
      raise ActiveRecord::Rollback
    end
    render json: { message: "Transaction rolled back" }
  end
end
```
```ruby:config/routes.rb
Rails.application.routes.draw do
  get "posts/new", to: "posts#new"
  post "posts", to: "posts#create"
end
```

ERBファイルで簡易的なフォームも作成しておきます。
```ERB:app/views/posts/new.html.erb
<!DOCTYPE html>
<html>
<head>
  <title>Create Post</title>
</head>
<body>
<h1>Create a New Post</h1>
<%= form_with url: posts_path, method: :post do %>
  <div>
    <%= submit_tag "Create Post" %>
  </div>
<% end %>
</body>
</html>
```

次にコントローラーに記載した`UpdatePostJob`を作成します。
```bash
$ rails g job UpdatePostJob
```
`app/jobs/update_post_job.rb`の実装は次のとおりです。
```ruby:app/jobs/update_post_job.rb
class UpdatePostJob < ApplicationJob
  queue_as :default
  self.enqueue_after_transaction_commit = :never # この1行がRails7.2の新機能

  def perform(post_id)
    Post.find(post_id).update(content: "Updated Content")
  end
end

```

まずは`enqueue_after_transaction_commit`メソッドに`:never`オプションを指定します。

`:never`オプションは、トランザクションのコミットを待たず即座にジョブをエンキューするオプションです。

サーバーを起動し、`http://localhost:3000/posts/new`を開きます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2325281/3f3b5387-d8ab-4cd5-fe01-a1f16d101112.png)

`Create Post`ボタンを押してみてください。

ログは以下のようになります。
（個人情報が含まれる一部のパスは改変しています）
```bash
Started POST "/posts" for 127.0.0.1 at 2024-07-21 18:03:34 +0900
Processing by PostsController#create as TURBO_STREAM
  Parameters: {"authenticity_token"=>"[FILTERED]", "commit"=>"Create Post"}
  TRANSACTION (0.0ms)  begin transaction
  ↳ app/controllers/posts_controller.rb:7:in `block in create'
  Post Create (0.2ms)  INSERT INTO "posts" ("content", "created_at", "updated_at") VALUES (?, ?, ?) RETURNING "id"  [["content", "New Content"], ["created_at", "2024-07-21 09:03:34.258860"], ["updated_at", "2024-07-21 09:03:34.258860"]]
  ↳ app/controllers/posts_controller.rb:7:in `block in create'
[ActiveJob] Enqueued UpdatePostJob (Job ID: 88e42cde-ac56-4785-b579-d67fcf269c62) to Async(default) with arguments: 1
[ActiveJob] ↳ app/controllers/posts_controller.rb:8:in `block in create'
  TRANSACTION (0.0ms)  rollback transaction
  ↳ app/controllers/posts_controller.rb:6:in `create'
Completed 200 OK in 23ms (Views: 0.2ms | ActiveRecord: 1.8ms (1 query, 0 cached) | GC: 0.4ms)


[ActiveJob] [UpdatePostJob] [88e42cde-ac56-4785-b579-d67fcf269c62] Performing UpdatePostJob (Job ID: 88e42cde-ac56-4785-b579-d67fcf269c62) from Async(default) enqueued at 2024-07-21T09:03:34.265483000Z with arguments: 1
[ActiveJob] [UpdatePostJob] [88e42cde-ac56-4785-b579-d67fcf269c62]   Post Load (0.0ms)  SELECT "posts".* FROM "posts" WHERE "posts"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
[ActiveJob] [UpdatePostJob] [88e42cde-ac56-4785-b579-d67fcf269c62]   ↳ app/jobs/update_post_job.rb:6:in `perform'
[ActiveJob] [UpdatePostJob] [88e42cde-ac56-4785-b579-d67fcf269c62] Error performing UpdatePostJob (Job ID: 88e42cde-ac56-4785-b579-d67fcf269c62) from Async(default) in 6.24ms: ActiveRecord::RecordNotFound (Couldn't find Post with 'id'=1):
/opt/homebrew/lib/ruby/gems/3.2.0/gems/activerecord-7.2.0.beta3/lib/active_record/core.rb:256:in `find'
/hogehoge/rails7.2-app/app/jobs/update_post_job.rb:6:in `perform'
/opt/homebrew/lib/ruby/gems/3.2.0/gems/activejob-7.2.0.beta3/lib/active_job/execution.rb:68:in `block in _perform_job'
/opt/homebrew/lib/ruby/gems/3.2.0/gems/activesupport-7.2.0.beta3/lib/active_support/callbacks.rb:121:in `block in run_callbacks'
/opt/homebrew/lib/ruby/gems/3.2.0/gems/i18n-1.14.5/lib/i18n.rb:351:in `with_locale'
/opt/homebrew/lib/ruby/gems/3.2.0/gems/activejob-7.2.0.beta3/lib/active_job/translation.rb:9:in `block (2 levels) in <module:Translation>'
/opt/homebrew/lib/ruby/gems/3.2.0/gems/activesupport-7.2.0.beta3/lib/active_support/callbacks.rb:130:in `instance_exec'
/opt/homebrew/lib/ruby/gems/3.2.0/gems/activesupport-7.2.0.beta3/lib/active_support/callbacks.rb:130:in `block in run_callbacks'
/opt/homebrew/lib/ruby/gems/3.2.0/gems/activesupport-7.2.0.beta3/lib/active_support/core_ext/time/zones.rb:65:in `use_zone'
/opt/homebrew/lib/ruby/gems/3.2.0/gems/activejob-7.2.0.beta3/lib/active_job/timezones.rb:9:in `block (2 levels) in <module:Timezones>'
/opt/homebrew/lib/ruby/gems/3.2.0/gems/activesupport-7.2.0.beta3/lib/active_support/callbacks.rb:130:in `instance_exec'
/opt/homebrew/lib/ruby/gems/3.2.0/gems/activesupport-7.2.0.beta3/lib/active_support/callbacks.rb:130:in `block in run_callbacks'
/opt/homebrew/lib/ruby/gems/3.2.0/gems/activesupport-7.2.0.beta3/lib/active_support/callbacks.rb:141:in `run_callbacks'
/opt/homebrew/lib/ruby/gems/3.2.0/gems/activejob-7.2.0.beta3/lib/active_job/execution.rb:67:in `_perform_job'
/opt/homebrew/lib/ruby/gems/3.2.0/gems/activejob-7.2.0.beta3/lib/active_job/instrumentation.rb:32:in `_perform_job'
/opt/homebrew/lib/ruby/gems/3.2.0/gems/activejob-7.2.0.beta3/lib/active_job/execution.rb:51:in `perform_now'
/opt/homebrew/lib/ruby/gems/3.2.0/gems/activejob-7.2.0.beta3/lib/active_job/instrumentation.rb:26:in `block in perform_now'
/opt/homebrew/lib/ruby/gems/3.2.0/gems/activerecord-7.2.0.beta3/lib/active_record/railties/job_runtime.rb:13:in `block in instrument'
/opt/homebrew/lib/ruby/gems/3.2.0/gems/activejob-7.2.0.beta3/lib/active_job/instrumentation.rb:40:in `block in instrument'
/opt/homebrew/lib/ruby/gems/3.2.0/gems/activesupport-7.2.0.beta3/lib/active_support/notifications.rb:210:in `block in instrument'
/opt/homebrew/lib/ruby/gems/3.2.0/gems/activesupport-7.2.0.beta3/lib/active_support/notifications/instrumenter.rb:58:in `instrument'
/opt/homebrew/lib/ruby/gems/3.2.0/gems/activesupport-7.2.0.beta3/lib/active_support/notifications.rb:210:in `instrument'
/opt/homebrew/lib/ruby/gems/3.2.0/gems/activejob-7.2.0.beta3/lib/active_job/instrumentation.rb:39:in `instrument'
/opt/homebrew/lib/ruby/gems/3.2.0/gems/activerecord-7.2.0.beta3/lib/active_record/railties/job_runtime.rb:11:in `instrument'
/opt/homebrew/lib/ruby/gems/3.2.0/gems/activejob-7.2.0.beta3/lib/active_job/instrumentation.rb:26:in `perform_now'
/opt/homebrew/lib/ruby/gems/3.2.0/gems/activejob-7.2.0.beta3/lib/active_job/logging.rb:32:in `block in perform_now'
/opt/homebrew/lib/ruby/gems/3.2.0/gems/activesupport-7.2.0.beta3/lib/active_support/tagged_logging.rb:138:in `block in tagged'
/opt/homebrew/lib/ruby/gems/3.2.0/gems/activesupport-7.2.0.beta3/lib/active_support/tagged_logging.rb:38:in `tagged'
/opt/homebrew/lib/ruby/gems/3.2.0/gems/activesupport-7.2.0.beta3/lib/active_support/tagged_logging.rb:138:in `tagged'
/opt/homebrew/lib/ruby/gems/3.2.0/gems/activesupport-7.2.0.beta3/lib/active_support/broadcast_logger.rb:241:in `method_missing'
/opt/homebrew/lib/ruby/gems/3.2.0/gems/activejob-7.2.0.beta3/lib/active_job/logging.rb:39:in `tag_logger'
/opt/homebrew/lib/ruby/gems/3.2.0/gems/activejob-7.2.0.beta3/lib/active_job/logging.rb:32:in `perform_now'
/opt/homebrew/lib/ruby/gems/3.2.0/gems/activejob-7.2.0.beta3/lib/active_job/execution.rb:29:in `block in execute'
/opt/homebrew/lib/ruby/gems/3.2.0/gems/activesupport-7.2.0.beta3/lib/active_support/callbacks.rb:121:in `block in run_callbacks'
/opt/homebrew/lib/ruby/gems/3.2.0/gems/activejob-7.2.0.beta3/lib/active_job/railtie.rb:81:in `block (4 levels) in <class:Railtie>'
/opt/homebrew/lib/ruby/gems/3.2.0/gems/activesupport-7.2.0.beta3/lib/active_support/reloader.rb:77:in `block in wrap'
/opt/homebrew/lib/ruby/gems/3.2.0/gems/activesupport-7.2.0.beta3/lib/active_support/execution_wrapper.rb:91:in `wrap'
/opt/homebrew/lib/ruby/gems/3.2.0/gems/activesupport-7.2.0.beta3/lib/active_support/reloader.rb:74:in `wrap'
/opt/homebrew/lib/ruby/gems/3.2.0/gems/activejob-7.2.0.beta3/lib/active_job/railtie.rb:80:in `block (3 levels) in <class:Railtie>'
/opt/homebrew/lib/ruby/gems/3.2.0/gems/activesupport-7.2.0.beta3/lib/active_support/callbacks.rb:130:in `instance_exec'
/opt/homebrew/lib/ruby/gems/3.2.0/gems/activesupport-7.2.0.beta3/lib/active_support/callbacks.rb:130:in `block in run_callbacks'
/opt/homebrew/lib/ruby/gems/3.2.0/gems/activesupport-7.2.0.beta3/lib/active_support/callbacks.rb:141:in `run_callbacks'
/opt/homebrew/lib/ruby/gems/3.2.0/gems/activejob-7.2.0.beta3/lib/active_job/execution.rb:27:in `execute'
/opt/homebrew/lib/ruby/gems/3.2.0/gems/activejob-7.2.0.beta3/lib/active_job/queue_adapters/async_adapter.rb:70:in `perform'
/opt/homebrew/lib/ruby/gems/3.2.0/gems/concurrent-ruby-1.3.3/lib/concurrent-ruby/concurrent/executor/ruby_thread_pool_executor.rb:359:in `run_task'
/opt/homebrew/lib/ruby/gems/3.2.0/gems/concurrent-ruby-1.3.3/lib/concurrent-ruby/concurrent/executor/ruby_thread_pool_executor.rb:350:in `block (3 levels) in create_worker'
/opt/homebrew/lib/ruby/gems/3.2.0/gems/concurrent-ruby-1.3.3/lib/concurrent-ruby/concurrent/executor/ruby_thread_pool_executor.rb:341:in `loop'
/opt/homebrew/lib/ruby/gems/3.2.0/gems/concurrent-ruby-1.3.3/lib/concurrent-ruby/concurrent/executor/ruby_thread_pool_executor.rb:341:in `block (2 levels) in create_worker'
/opt/homebrew/lib/ruby/gems/3.2.0/gems/concurrent-ruby-1.3.3/lib/concurrent-ruby/concurrent/executor/ruby_thread_pool_executor.rb:340:in `catch'
/opt/homebrew/lib/ruby/gems/3.2.0/gems/concurrent-ruby-1.3.3/lib/concurrent-ruby/concurrent/executor/ruby_thread_pool_executor.rb:340:in `block in create_worker'
```

トランザクションはロールバックされているものの、ジョブはすでに実行されてしまっているため、`ActiveRecord::RecordNotFound (Couldn't find Post with 'id'=1)`とエラーが出てしまっています。

次に`enqueue_after_transaction_commit`オプションを`:always`に変更して再度`Create Post`ボタンを押してみます。

結果は次のようになります。

```bash
Started POST "/posts" for 127.0.0.1 at 2024-07-21 18:06:18 +0900
Processing by PostsController#create as TURBO_STREAM
  Parameters: {"authenticity_token"=>"[FILTERED]", "commit"=>"Create Post"}
  TRANSACTION (0.0ms)  begin transaction
  ↳ app/controllers/posts_controller.rb:7:in `block in create'
  Post Create (0.1ms)  INSERT INTO "posts" ("content", "created_at", "updated_at") VALUES (?, ?, ?) RETURNING "id"  [["content", "New Content"], ["created_at", "2024-07-21 09:06:18.833509"], ["updated_at", "2024-07-21 09:06:18.833509"]]
  ↳ app/controllers/posts_controller.rb:7:in `block in create'
[ActiveJob] Enqueued UpdatePostJob (Job ID: ac905a7e-3719-410a-8a2b-880d49622ca6) to Async(default) with arguments: 1
[ActiveJob] ↳ app/controllers/posts_controller.rb:8:in `block in create'
  TRANSACTION (0.0ms)  rollback transaction
  ↳ app/controllers/posts_controller.rb:6:in `create'
Completed 200 OK in 11ms (Views: 0.1ms | ActiveRecord: 2.3ms (1 query, 0 cached) | GC: 0.3ms)
```

今度はトランザクションがロールバックされただけでログが終了しています。

これはトランザクションのコミットが完了するまで、ジョブのキューへの登録が遅延されていることを表しています。


## トランザクション内でのコールバックが使える
Rails 7.2ではトランザクションの中で`after_commit`と`after_rollback`コールバックを指定できるようになります。

`ActiveRecord::Base.transaction`がトランザクションオブジェクトを返すようになったのです。

```ruby
# after_commit の使用例
ActiveRecord::Base.transaction do |transaction|
  user.save!
  
  transaction.after_commit do
    NotificationService.send_welcome_email(user)
  end
end

# after_rollback の使用例
ActiveRecord::Base.transaction do |transaction|
  user.save!

  transaction.after_rollback do
    ErrorLoggerService.log_user_creation_failure(user)
  end
end
```

また`ActiveRecord::Base.current_transaction`メソッドも導入されました。

先ほどの実装は次のように書くこともできます。

```ruby
# after_commit の使用例
ActiveRecord::Base.transaction do
  user.save!
  
  ActiveRecord::Base.current_transaction.after_commit do
    NotificationService.send_welcome_email(user)
  end
end

# after_rollback の使用例
ActiveRecord::Base.transaction do
  user.save!

  ActiveRecord::Base.current_transaction.after_rollback do
    ErrorLoggerService.log_user_creation_failure(user)
  end
end
```

### 実際にやってみた
まずは`after_commit`からやってみます。

先ほどのサンプルを一部流用し、以下のコードを記述しました。

```ruby:app/controllers/posts_controller.rb
class PostsController < ApplicationController
  def new
  end

  def create
    ActiveRecord::Base.transaction do |transaction|
      post = Post.create(content: "New Content")
      # after_commit を使用
      transaction.after_commit do
        puts "Transaction committed"
      end
    end
  end
end
```

サーバーを起動し、`http://localhost:3000/posts/new`を開きます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2325281/3f3b5387-d8ab-4cd5-fe01-a1f16d101112.png)

`Create Post`ボタンを押してみてください。

ログは以下のようになります。

```bash
Started POST "/posts" for 127.0.0.1 at 2024-07-25 10:36:01 +0900
Processing by PostsController#create as TURBO_STREAM
  Parameters: {"authenticity_token"=>"[FILTERED]", "commit"=>"Create Post"}
  TRANSACTION (0.0ms)  begin transaction
  ↳ app/controllers/posts_controller.rb:7:in `block in create'
  Post Create (0.5ms)  INSERT INTO "posts" ("content", "created_at", "updated_at") VALUES (?, ?, ?) RETURNING "id"  [["content", "New Content"], ["created_at", "2024-07-25 01:36:02.005851"], ["updated_at", "2024-07-25 01:36:02.005851"]]
  ↳ app/controllers/posts_controller.rb:7:in `block in create'
  TRANSACTION (0.3ms)  commit transaction
  ↳ app/controllers/posts_controller.rb:6:in `create'
Transaction committed
Completed 200 OK in 33ms (Views: 0.1ms | ActiveRecord: 2.0ms (1 query, 0 cached) | GC: 0.5ms)
```

トランザクションのコミット完了後に`Transaction committed`と出力されることが確認できました。

また次のように`current_transaction`を使った場合も挙動は同じでした。
```ruby:app/controllers/posts_controller.rb
class PostsController < ApplicationController
  def new
  end

  def create
    ActiveRecord::Base.transaction do
      post = Post.create(content: "New Content")
      # current_transaction.after_commit を使用
      ActiveRecord::Base.current_transaction.after_commit do
        puts "Transaction committed"
      end
    end
  end
end
```

続いて`after_rollback`を試します。

```ruby:app/controllers/posts_controller.rb
class PostsController < ApplicationController
  def new
  end

  def create
    ActiveRecord::Base.transaction do |transaction|
      post = Post.create(content: "New Content")

      transaction.after_rollback do
        puts "Transaction rollback"
      end
      
      # わざと例外を発生
      raise ActiveRecord::Rollback
    end
  end
end
```

ログは以下のようになります。
```bash
Started POST "/posts" for 127.0.0.1 at 2024-07-25 10:43:28 +0900
Processing by PostsController#create as TURBO_STREAM
  Parameters: {"authenticity_token"=>"[FILTERED]", "commit"=>"Create Post"}
  TRANSACTION (0.0ms)  begin transaction
  ↳ app/controllers/posts_controller.rb:7:in `block in create'
  Post Create (0.2ms)  INSERT INTO "posts" ("content", "created_at", "updated_at") VALUES (?, ?, ?) RETURNING "id"  [["content", "New Content"], ["created_at", "2024-07-25 01:43:28.251642"], ["updated_at", "2024-07-25 01:43:28.251642"]]
  ↳ app/controllers/posts_controller.rb:7:in `block in create'
  TRANSACTION (0.1ms)  rollback transaction
  ↳ app/controllers/posts_controller.rb:6:in `create'
Transaction rollback
Completed 200 OK in 14ms (Views: 0.1ms | ActiveRecord: 3.1ms (1 query, 0 cached) | GC: 0.4ms)
```

トランザクションのロールバック完了後に`Transaction rollback`と出力されていますね。

こちらも同じく`current_transaction`を使っても同じ挙動になりました。

```ruby:app/controllers/posts_controller.rb
class PostsController < ApplicationController
  def new
  end

  def create
    ActiveRecord::Base.transaction do
      post = Post.create(content: "New Content")

      # current_transaction.after_rollback を使用
      ActiveRecord::Base.current_transaction.after_rollback do
        puts "Transaction rollback"
      end
      
      # わざと例外を発生
      raise ActiveRecord::Rollback
    end
  end
end
```

## Ruby3.3以上の場合、デフォルトでYJITが有効
Ruby3.3以降を使用している場合、YJITをデフォルトで有効にする設定が加わりました。

これによりRuby3.3以上を使ったRailsアプリケーションは追加設定なしにパフォーマンス向上が見込めます。

以下のPRで変更が取り込まれているようです。

https://github.com/rails/rails/pull/49947

YJIT（Yet Another Ruby JIT）はRubyの新しいJIT（Just In Time）コンパイラです。

JITコンパイラとは、プログラムの実行中にコードを機械語に変換し、実行速度を向上させる技術です。

コード生成を遅延させて必要な記述だけを部分的に変換することで、高速化を図っているようです。

Ruby3.1で導入後、Ruby3.3で安定化が行われています。

この恩恵をRailsアプリケーションでも受けられるように、Ruby3.3以上を使っている場合のみデフォルトでYJITが有効になります。

### 実際に見てみる
まずはRuby3.3以上、Rails7.2であることを確認します。
```bash
ruby3.3-rails7.2-app % ruby -v
ruby 3.3.4 (2024-07-09 revision be1089c8ec) [arm64-darwin22]
ruby3.3-rails7.2-app % rails -v
Rails 7.2.0.beta3
```

railsコンソールでRubyVM::YJIT.enabled?を実行し、YJITが有効になっていることを確認します。
```ruby
ruby3.3-rails7.2-app % rails c
Loading development environment (Rails 7.2.0.beta3)
ruby33-rails72-app(dev)> RubyVM::YJIT.enabled?
=> true
```

たしかに有効になっていました。

## メモリ割り当て最適化のためDockerfileにjemallocを設定
Rails7.2からデフォルトで作成されるDockerfileに、`jemalloc`というライブラリをインストールするステップが追加されました。

`jemalloc`とはメモリ割り当てを最適化するためのライブラリです。

Railsアプリケーションのメモリ効率とそれに伴うアプリケーション全体のパフォーマンス向上を目的に導入されました。

### 実際に見てみる
Rails7.2で`rails new`した場合に生成されるDockerfileは次のとおりです。

```Dockerfile
# syntax = docker/dockerfile:1

# This Dockerfile is designed for production, not development. Use with Kamal or build'n'run by hand:
# docker build -t my-app .
# docker run -d -p 80:80 -p 443:443 --name my-app -e RAILS_MASTER_KEY=<value from config/master.key> my-app

# For a containerized dev environment, see Dev Containers: https://guides.rubyonrails.org/getting_started_with_devcontainer.html

# Make sure RUBY_VERSION matches the Ruby version in .ruby-version
ARG RUBY_VERSION=3.2.2
FROM docker.io/library/ruby:$RUBY_VERSION-slim AS base

# Rails app lives here
WORKDIR /rails

# Install base packages
RUN apt-get update -qq && \
    apt-get install --no-install-recommends -y curl libjemalloc2 libsqlite3-0 libvips && \
    rm -rf /var/lib/apt/lists /var/cache/apt/archives

# Set production environment
ENV RAILS_ENV="production" \
    BUNDLE_DEPLOYMENT="1" \
    BUNDLE_PATH="/usr/local/bundle" \
    BUNDLE_WITHOUT="development"

# Throw-away build stage to reduce size of final image
FROM base AS build

# Install packages needed to build gems
RUN apt-get update -qq && \
    apt-get install --no-install-recommends -y build-essential git pkg-config && \
    rm -rf /var/lib/apt/lists /var/cache/apt/archives

# Install application gems
COPY Gemfile Gemfile.lock ./
RUN bundle install && \
    rm -rf ~/.bundle/ "${BUNDLE_PATH}"/ruby/*/cache "${BUNDLE_PATH}"/ruby/*/bundler/gems/*/.git && \
    bundle exec bootsnap precompile --gemfile

# Copy application code
COPY . .

# Precompile bootsnap code for faster boot times
RUN bundle exec bootsnap precompile app/ lib/

# Precompiling assets for production without requiring secret RAILS_MASTER_KEY
RUN SECRET_KEY_BASE_DUMMY=1 ./bin/rails assets:precompile




# Final stage for app image
FROM base

# Copy built artifacts: gems, application
COPY --from=build "${BUNDLE_PATH}" "${BUNDLE_PATH}"
COPY --from=build /rails /rails

# Run and own only the runtime files as a non-root user for security
RUN groupadd --system --gid 1000 rails && \
    useradd rails --uid 1000 --gid 1000 --create-home --shell /bin/bash && \
    chown -R rails:rails db log storage tmp
USER 1000:1000

# Entrypoint prepares the database.
ENTRYPOINT ["/rails/bin/docker-entrypoint"]

# Start the server by default, this can be overwritten at runtime
EXPOSE 3000
CMD ["./bin/rails", "server"]
```

`apt-get install --no-install-recommends -y curl libjemalloc2 libsqlite3-0 libvips && \`の部分で`libjemalloc2`という形でインストールされていました。

## bin/setupにpuma-dev設定用のコメントを追加
`bin/setup`ファイルにpuma-devを設定するためのコメントが追加されました。

puma-devはDockerを使わずに環境構築する際、開発用サーバーの設定を迅速かつ簡単にしてくれるためのものです。

### 実際に見てみる
`rails new`した時点の`bin/setup`の中身はこんな感じです。

```bash:bin/setup
#!/usr/bin/env ruby
require "fileutils"

APP_ROOT = File.expand_path("..", __dir__)
APP_NAME = "rails7_2_app"

def system!(*args)
  system(*args, exception: true)
end

FileUtils.chdir APP_ROOT do
  # This script is a way to set up or update your development environment automatically.
  # This script is idempotent, so that you can run it at any time and get an expectable outcome.
  # Add necessary setup steps to this file.

  puts "== Installing dependencies =="
  system! "gem install bundler --conservative"
  system("bundle check") || system!("bundle install")

  # puts "\n== Copying sample files =="
  # unless File.exist?("config/database.yml")
  #   FileUtils.cp "config/database.yml.sample", "config/database.yml"
  # end

  puts "\n== Preparing database =="
  system! "bin/rails db:prepare"

  puts "\n== Removing old logs and tempfiles =="
  system! "bin/rails log:clear tmp:clear"

  puts "\n== Restarting application server =="
  system! "bin/rails restart"

  # puts "\n== Configuring puma-dev =="
  # system "ln -nfs #{APP_ROOT} ~/.puma-dev/#{APP_NAME}"
  # system "curl -Is https://#{APP_NAME}.test/up | head -n 1"
end
```

Rails7.2で一番最後の3行が追加されています。
```bash
  # puts "\n== Configuring puma-dev =="
  # system "ln -nfs #{APP_ROOT} ~/.puma-dev/#{APP_NAME}"
  # system "curl -Is https://#{APP_NAME}.test/up | head -n 1"
```

## おわりに
この記事では、Rails 7.2の新機能や変更点について、一部の内容を実際に試しながら解説しました。

前編からやってきたため、結構パワーを使うなぁ、という感想です。

今回の記事執筆を機に、RailsのコードやIssueを読む時間を意識的に確保していきたいと思います。

またこの記事に関して間違っている点がありましたら、教えていただけますと幸いです。

## 参考資料
https://edgeguides.rubyonrails.org/7_2_release_notes.html