第29章|今さら学ぶ「非同期処理・メール送信(Active Job / Action Mailer)」
📚 シリーズ目次はこちら → 「今さら学ぶ」シリーズ — はじめに
🗺️ KnowledgeNoteの設計を確認 → 設計マップ
この章でわかること
- Active Jobとは — 「あとでやっておいて」と頼める仕組み
- Solid Queue(Rails 8.0 標準)— Redis不要のジョブキュー
- Action Mailerの流れ — アプリが自動で手紙を出す郵便局
- 開発環境でのメール確認(letter_opener)
- 本番環境でのメール配信(Resend)
🏠 たとえ話で掴む「非同期処理」
レストランで料理を注文したとき、ウェイターが厨房で料理ができあがるまでテーブルの前で立ち尽くしていたら困る。普通は注文を厨房に伝えたら、すぐ次のお客さんの注文を取りに行きます。
非同期処理 はまさにこれです。「メールを送る」「画像をリサイズする」「通知を作成する」といった時間のかかる処理を、 裏方(バックグラウンドジョブ) に任せて、ユーザーへのレスポンスを先に返します。
| レストラン | Railsアプリ |
|---|---|
| 注文を厨房に伝える | ジョブをキューに入れる |
| ウェイターは次の注文へ | レスポンスを即座に返す |
| 厨房で料理を作る | バックグラウンドでジョブを実行 |
| できたら運ぶ | 完了したらメール送信等が実行される |
非同期処理とは何か — 技術的な定義
同期処理 とは、処理が完了するまで次の処理に進まない方式です。Railsのコントローラは通常、上から順番に処理を実行して、すべて終わってからレスポンスを返します。
問題は「時間のかかる処理」がコントローラの中にあるときです。たとえばフォロワー100人に通知メールを送る処理が5秒かかるとすると、ユーザーは記事を投稿してから5秒間、画面が固まった状態で待つことになります。
非同期処理 は、この時間のかかる処理を キュー(待ち行列) に入れて、別のプロセス(ワーカー)に実行させる方式です。コントローラは「あとでやっておいて」とキューにジョブを積んだら、すぐにレスポンスを返します。
【同期処理】
リクエスト → コントローラ → 記事保存 → メール送信(5秒) → レスポンス
↑ ユーザーはここで待つ
【非同期処理】
リクエスト → コントローラ → 記事保存 → ジョブをキューに追加 → レスポンス(即座)
↓
ワーカーが裏でメール送信を実行
Railsではこの仕組みを Active Job (ジョブの標準インターフェース)と ジョブバックエンド (実際にキューを管理するライブラリ)の組み合わせで実現します。
⚙️ Active Job — ジョブの共通インターフェース
Active Job は、バックグラウンドジョブを書くためのRails標準のフレームワークです。ジョブの書き方を統一する「共通インターフェース」であり、裏側で使うバックエンド(Solid Queue、Sidekiq等)を差し替えても ジョブのコードは変更不要 です。
# app/jobs/notification_job.rb
class NotificationJob < ApplicationJob
queue_as :default
def perform(article)
article.user.followers.find_each do |follower|
Notification.create!(
user: follower,
notifiable: article,
action_type: "published"
)
end
end
end
# ジョブをキューに投入(即座にreturnされる)
NotificationJob.perform_later(@article)
# → ユーザーにはすぐレスポンスが返り、通知作成は裏で実行される
perform_later と perform_now
# 非同期実行(キューに入れて、ワーカーがあとで実行)← 推奨
NotificationJob.perform_later(@article)
# 同期実行(その場で即実行。キューを使わない)
NotificationJob.perform_now(@article)
# → テストやデバッグ時に使うことがある
リトライとエラーハンドリング
ジョブの実行中にエラーが起きることもあります(外部APIの一時障害など)。Active Jobにはリトライの仕組みがあります。
class NotificationJob < ApplicationJob
queue_as :default
retry_on Net::OpenTimeout, wait: 5.seconds, attempts: 3
def perform(article)
# ...
end
end
retry_on を指定すると、指定したエラーが発生したとき自動でリトライします。
🔋 Solid Queue(Rails 8.0 標準)
Rails 8.0 では Solid Queue がデフォルトのジョブバックエンドです。従来のSidekiq + Redisの代わりに、 DBだけでジョブキューを管理 します。
# config/environments/production.rb
config.active_job.queue_adapter = :solid_queue
# → Redis不要!DBのテーブルでジョブを管理
| 従来(Sidekiq + Redis) | Solid Queue(Rails 8.0) | |
|---|---|---|
| 追加ミドルウェア | Redis サーバ必要 | 不要(DBに保存) |
| 運用コスト | Redisの管理が必要 | DB だけで完結 |
| 処理速度 | 高速 | 十分に高速 |
| 設定 | Gemfile + 設定ファイル | Rails 8.0 でデフォルト有効 |
💡 Rails 7 との違い
Rails 7 では Sidekiq + Redis が主流でした。Rails 8.0 では Solid Queue がデフォルトなので、Redisのセットアップなしでバックグラウンドジョブが使えます。既存のSidekiqプロジェクトはそのまま使えますが、新規プロジェクトではSolid Queueがおすすめです。
📮 Action Mailer — アプリが自動で手紙を出す
Action Mailer は、Railsからメールを送信するための仕組みです。コントローラに似た構造で、ビュー(テンプレート)を使ってメール本文を組み立てます。
技術的には、Action Mailerは「メーラー(Mailer)」というクラスで送信処理を定義し、対応するビューテンプレート(app/views/メーラー名/)でメール本文のHTMLを作成します。コントローラが render でHTMLを返すのと同じ構造です。
メーラーの作成
$ rails generate mailer NotificationMailer
# app/mailers/notification_mailer.rb
class NotificationMailer < ApplicationMailer
def new_article(user, article)
@user = user
@article = article
mail(to: @user.email_address, subject: "新しい記事が投稿されました")
end
def password_reset(user, token)
@user = user
@token = token
mail(to: @user.email_address, subject: "パスワード再設定のお知らせ")
end
end
<%# app/views/notification_mailer/new_article.html.erb %>
<h1><%= @user.name %> さん</h1>
<p>フォロー中のユーザーが新しい記事を投稿しました。</p>
<p>
<strong><%= @article.title %></strong><br>
by <%= @article.user.name %>
</p>
<p>
<%= link_to "記事を読む", article_url(@article) %>
</p>
メールの送信
# 同期送信(レスポンスが遅くなるので非推奨)
NotificationMailer.new_article(user, article).deliver_now
# 非同期送信(Active Job経由で裏側で送信 ← 推奨)
NotificationMailer.new_article(user, article).deliver_later
deliver_later を使えば、メール送信はバックグラウンドで処理され、ユーザーへのレスポンスは即座に返ります。
🔧 開発環境でのメール確認(letter_opener)
開発環境で実際にメールを送ると困るので、 letter_opener でブラウザ上にメールをプレビューします。
# Gemfile
group :development do
gem "letter_opener_web"
end
# config/environments/development.rb
config.action_mailer.delivery_method = :letter_opener_web
config.action_mailer.default_url_options = { host: "localhost", port: 3000 }
# config/routes.rb
mount LetterOpenerWeb::Engine, at: "/letter_opener" if Rails.env.development?
http://localhost:3000/letter_opener にアクセスすると、送信されたメールの一覧がブラウザで確認できます。実際にメールが送られることはないので安心です。
📬 本番環境でのメール配信(Resend)
本番環境では Resend を使います。APIがシンプルで、無料枠(月100通)があります。
# Gemfile
gem "resend"
# config/environments/production.rb
config.action_mailer.delivery_method = :smtp
config.action_mailer.smtp_settings = {
address: "smtp.resend.com",
port: 587,
user_name: "resend",
password: Rails.application.credentials.dig(:resend, :api_key),
authentication: :plain,
enable_starttls_auto: true
}
config.action_mailer.default_url_options = { host: "knowledgenote.example.com" }
💡
passwordに直接APIキーを書かず、credentials(→ 第20章)で管理している。環境変数(ENV["RESEND_API_KEY"])を使う方法もある。
🛠️ KnowledgeNoteでの具体例
KnowledgeNoteでは「記事が公開されたらフォロワーに通知+メール送信」を非同期で処理します。フォロワーへのリアルタイム通知(WebSocket)については(→ 第31章で詳しく扱います)。
# app/services/article_publish_service.rb([第25章](https://qiita.com/harapeco-mgn/items/dcbdf96e5c35d2249f46)の続き)
class ArticlePublishService
def call
ActiveRecord::Base.transaction do
save_article!
attach_tags!
end
# トランザクションの外で非同期処理を投入
NotificationJob.perform_later(@article) if @article.published?
true
rescue ActiveRecord::RecordInvalid
false
end
end
# app/jobs/notification_job.rb
class NotificationJob < ApplicationJob
queue_as :default
retry_on Net::OpenTimeout, wait: 10.seconds, attempts: 3
def perform(article)
article.user.followers.find_each do |follower|
Notification.create!(
user: follower,
notifiable: article,
action_type: "published"
)
NotificationMailer.new_article(follower, article).deliver_later
end
end
end
ポイントは、NotificationJob の中で deliver_later を使っている点です。ジョブの中からさらにメール送信ジョブが作られるので、通知の作成とメール送信が並行して進みます。
また、非同期処理は トランザクションの外 で投入するのが重要です。トランザクション内で perform_later すると、ロールバック時にジョブだけが残ってしまう(DBにデータがないのにメールが送られる)危険があります。
💼 面接で聞かれたら?
Q:非同期処理はなぜ必要ですか?
「メール送信や通知作成など、時間のかかる処理をリクエストの中で同期的に実行すると、ユーザーへのレスポンスが遅くなります。Active Jobでこれらをバックグラウンドジョブとしてキューに入れれば、レスポンスを即座に返しつつ、処理は裏側で順次実行されます。Rails 8.0ではSolid QueueがデフォルトでRedis不要です。」
深掘りされたら:
- 「deliver_now と deliver_later の違いは?」→ deliver_nowは同期送信でレスポンスが遅くなる。deliver_laterはActive Job経由の非同期送信で、レスポンスに影響しない。本番ではdeliver_laterを使う。
- 「ジョブが失敗したらどうなる?」→ Active Jobの
retry_onで自動リトライできる。リトライ回数を超えたらdiscard_onで破棄するか、Dead Letter Queue(失敗ジョブの保管場所)に入れて後で調査する。- 「トランザクション内でperform_laterを呼ぶとどうなる?」→ ロールバックしてもジョブは取り消されない。データが存在しないのにジョブが実行される危険があるため、トランザクションの外でジョブを投入すべき。
🔗 もっと深く知りたい人へ(1次情報リンク)
- Rails ガイド:Active Job の基礎 — ジョブの作成・実行の全体像
- Rails ガイド:Action Mailer の基礎 — メーラーの作成からテストまで
- Solid Queue(GitHub) — Rails 8.0 標準のジョブキュー
- Resend 公式 — メール配信サービス
まとめ
- ✅ 非同期処理は「注文を厨房に伝えて、ウェイターは次の注文へ」。レスポンスを即座に返し、重い処理は裏で実行する
- ✅ Active Jobはジョブの共通インターフェース。バックエンド(Solid Queue / Sidekiq等)を差し替えてもコード変更不要
- ✅ Solid Queue(Rails 8.0 標準)はRedis不要。DBだけでジョブキューを管理する
- ✅ Action Mailerでメール送信。deliver_laterで非同期送信が推奨
- ✅ 開発環境は letter_opener_web でブラウザプレビュー、本番は Resend
- ✅ 非同期ジョブはトランザクションの外で投入し、データの整合性と非同期処理を両立する
📚 シリーズ目次:「今さら学ぶ」シリーズ — はじめに