0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Rails初学者】備忘録:RailsのJobでメール送信とフラグ更新を連携させたくて、やってみた

Last updated at Posted at 2025-09-21

概要

アプリ開発中に、メール送信機能実装で遭遇した課題と解決方法について備忘録として残します。

環境

Rails 8.0 / Sidekiq / letter_opener

発生した課題

初期実装では、メール送信処理とデータベースの送信フラグ更新が分離されており、以下の問題に当たってしまいました。

  • コンソールで NotificationMailer.visit_reminder(n).deliver_now を実行してもメールは送信されるが、is_sent フラグが更新されない
  • メール送信の成功/失敗とデータベースの状態が同期していない

やってみたアプローチ

1: トランザクションでの送信と更新の統合をする

まず、コンソールで送信とフラグ更新を一括実行する方法を確認

Notification.transaction do
  NotificationMailer.visit_reminder(n).deliver_now
  n.update!(is_sent: true)
end

実行結果:

TRANSACTION (0.3ms)  BEGIN
Delivered mail ...
Notification Update (0.4ms)  UPDATE "notifications" SET "is_sent" = TRUE
TRANSACTION (1.4ms)  COMMIT
=> true

上記から、

  • メール送信が成功した場合のみ is_senttrue に更新される
  • 送信に失敗した場合はロールバックされ、is_sentfalse のまま
  • データベースの整合性が保たれる

2: Jobでの責務集約

次に、この処理をJob内に組み込んで責務を集約

class VisitReminderJob < ApplicationJob
  queue_as :default

  def perform
    visits = Visit.where(visit_date: Time.zone.tomorrow).includes(:notifications)

    visits.each do |visit|
      visit.notifications
           .where(is_sent: false, due_date: Time.zone.tomorrow)
           .find_each do |notification|

        Notification.transaction do
          NotificationMailer.visit_reminder(notification).deliver_now
          notification.update!(is_sent: true)
        end

      end
    end
  end
end

3: 動作確認

テストデータの作成

u = User.first
d = Department.first

v = u.visits.create!(
  visit_date: Date.tomorrow,
  hospital_name: "テスト会社",
  appointed_at: Time.zone.now,
  purpose: "テスト目的",
  department_id: d.id
)

n = u.notifications.create!(
  title: "【リマインド】#{v.hospital_name}",
  description: "明日の予定です\n日付: #{v.visit_date}",
  due_date: Date.tomorrow,
  visit: v,
  is_sent: false
)

Job実行:

VisitReminderJob.perform_now

実行結果:

NotificationMailer#visit_reminder: processed outbound mail
Delivered mail ...
Notification Update (0.3ms)  UPDATE "notifications" SET "is_sent" = TRUE
TRANSACTION (0.2ms)  COMMIT
Performed VisitReminderJob from Sidekiq(default) in 90.79ms

重要

deliver_nowと、deliver_laterの選択について

Job内では deliver_now を使用した

# Job内での推奨実装
NotificationMailer.visit_reminder(notification).deliver_now

# deliver_laterだと「Job内からさらに別のJobを積む」ことになり整合性が崩れる

Controller側の責務分離

Controller側は「通知レコード作成とJob呼び出し」のみに責務を限定したい

def create
  @item = current_user.items.build(item_params)
  if @item.save
    notification = current_user.notifications.create!(
      title: "...",
      description: "...",
      due_date: @item.scheduled_date - 1.day,
      item: @item,
      is_sent: false
    )
    # Job に委譲
    ReminderJob.perform_later
    redirect_to items_path, notice: "予定を保存しました"
  else
    render :new, status: :unprocessable_entity
  end
end

まとめ

  1. 責務を分ける: メール送信と状態更新は密接に関連するため、同じ処理単位(Job)内で実行
  2. トランザクションを活用してみる: 送信成功時のみデータベースを更新することで整合性を保持
  3. Job内で同期処理をする: Job内では deliver_now を使い、送信と更新を同期的に実行
  4. 試しに動かす: 実際にJobを動かして期待通りの動作を確認することの重要性

結果

  • メール送信とデータの更新がちゃんとセットで動くようになった。
  • 「メールは送れたのにDBが更新されない」「DBだけ更新されたのにメールは届かない」といったズレは直せたみたい、

初学者のため、内容が間違えていたらすいません。
また、いい書き方やアプローチの仕方がありましたら、コメントいただけるとありがたいです。

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?