LoginSignup
15
8

More than 3 years have passed since last update.

ActiveSupport::Callbackを使うのをやめろ

Last updated at Posted at 2019-10-30

追記:

個人的な最終結論が出たのでそちらをまとめました。

概要

タイトルは1年ちょっと前の自分に向けた自戒なので絶対に使うのが悪というわけではない(と思う)。
だがぼくにはうまい付き合い方が見つけられそうになかったのでやめろという強い口調を意図して使っている。

ActiveSupport::Callback is 何?

これ→activesupport/lib/active_support/callbacks.rb

Railsガイドにも記載されているのでRailsチュートリアル完走するかそれ相当のことをしていたら使う使わないは別にして知っているだろうと思う。

Active Record コールバック

# サンプルでよくみるやつ
class User < ApplicationRecord
  after_create :send_confirm_to_user

  private

  def send_confirm_to_user
    # sign upしたら最終確認のために送られるメールだと思ってほしい
    UserMailer.signup_confirmation(to: mail).deliver
  end
end

当時の考え

当時コールバックで実装すべきか、Service Objectで実装すべきか一瞬考えた……がrailsというフレームワークが提供している機能なので巨人の肩には乗っておいたほうが後々良いだろうと判断した。

その結果、いま地獄をみている。

ActiveSupport::Callbackの問題

このエントリを書こうとしてから何故か見つかるという非常に悲しい思いを今まさにしているが以下のような現象に苦しまされるのでコールバックは使わない、使うとしてもコントロール可能な実装を用いて最小限で使うべきというのがいまの感想だ。

地獄とはなんであるか?

他にもあるが大きくは「テストがしにくい」「影響範囲がわからない」の2点にほぼ集約される。

  • FactoryBot.create :userしたらコールバックが走ってメールが送信される
  • rails consoleUser.create したらメールが送信される
  • migration内で User.create したらメールが(ry

などなど。
特に注意しないといけないのがconsolemigration内でモデルを使うときでこれらのときにコールバックが実行されることを意識していることはまれだと思う。
ぼくは全く意識しておらず実行して「😱😱😱」と何度かなった、マジでやばい。

上記の「Rails: メールをActive Recordのコールバックで送信しないこと(翻訳) おたより発掘」でも言及されているが「コールバックは副作用である」という説明が非常に端的かつわかりやすいと思う。

ぼくの場合FactoryBotで非常に困ったというかテストが書きにくくなってしまいました。
これはコールバックそれ自体だけではコントロールが難しい点に問題がある。

A、B、Cというユーザを新規作成し、それぞれに別のOrganizationに所属させるテストを書きたいだけなのにコールバックでメールを送信してしまうとこのときメールが実行されてしまいます。

一応コールバックには skip_callback で処理をスキップさせることができます……が今回の例のような非常に関心の高い User というモデルはテストでもよく使われますがそのたびに毎回 skip_callback を書く?
正気の沙汰じゃないですね!!!

またコールバックはデータの意図が明示的でないので問題が起こったときに調査しにくくなってしまいました。
極端な例ですが

  1. Organization を削除したときに Membersafter_destroy で削除
  2. Membersafter_destroyUserorganization_idnil に変更

という処理を書いていた場合 OrganizationMemberUser に影響することを常に意識しなければいけない。

だが、現実的にこれは非常にしんどい。

テストを書いているときもコンソールを叩いているときもマイグレーションを実行するときにもどこでなにをするにも Organization が削除されたときにこれらを意識してデータが意図しない状態になっていないかを検知できなくてはいけないことになります。

将来的に影響する範囲が縮小するのであればいいですが、現実はその反対でより依存する数が増えるだろうと思います。
ぼくは増えてしまい大変困りました、困っています。

ではどうするか?

1番、というか実質ほぼこれだと思うのですが、Service Object相当な場所に実装を移します。

# CreateUserAndSendMailService.new(user).create をコントローラなどで呼び出す
class CreateUserAndSendMailService
  def initialize(user)
    @user = user
  end

  def create
    raise "なんかエラー起こった" unless @user.save
    UserMailer.signup_confirmation(to: @user.mail).deliver
  end
end

どうしてもコールバックでないといけない事情がある場合はやむなく以下のようにして通常はコールバックは実行されないようにし、どうしても必要な箇所だけで can_send_mail=true にすることでコールバックを実行する許可を与える実装にするかなと思います。
ただ基本的には上記のService Objectで実装するほうがよいと考えています。

理由としては依存を特定のService Objectに押し込めることができるため、リファクタリングをすることになっても無用な不安(自分の検知していないところに影響しないか)などを心配する必要がなくなります。

最悪なくならずとも最小限に抑えることができます。

class User < ApplicationRecord
  # 初期値は nil になる
  # https://ref.xaio.jp/ruby/classes/module/attr_accessor
  attr_accessor :can_send_mail
  after_create :send_confirm_to_user if: :can_send_mail?

  private

  def can_send_mail?
    can_send_mail.present?
  end

  def send_confirm_to_user
    # sign upしたら最終確認のために送られるメールだと思ってほしい
    UserMailer.signup_confirmation(to: mail).deliver
  end
end

user.can_send_mail = true
user.save # メールが送信される

other.new(name: 'alice', mail: 'alice@example.com')
other.save # メールは送信されない

コールバックとは関係ないのですがどこでなにが呼ばれるかわからない不安はコントローラでActiveRecordwheresaveなどのメソッドが出ているから心配になるのであって最初からCreateUserServiceDestroyOrganizationServiceのようなものがありコントローラはそれが呼び出されているだけであれば心配や不安に思うこともないのでコールバックだけの責任ではないですね。

とはいえ、コールバックがコードに現れたらそれはほぼ実質的に黒なのでコードレビューの段階で阻止すべき事柄なのだなと学習しました。

まとめ

正直なところ default_scope はあまり使うべきでない(unscopeがあるとはいえ)というのは知っていたのだけど callback もそうだとは思っておらず楽をしようとして大きな負債を積み上げてしまった。
rails 便利機能が非常に豊富である一方、この機能をうかつに使うとあとで大きなコストを払うことになる……みたいな初心者がよく躓くケースをまとめてRailsガイドに載せておいてくれといいたい。

普通に考えたらフレームワークが提供している機能のほうが自前で実装するよりもいいと思うじゃん。

追記

類似エントリにサジェストされていたおかげで↓の記事を読ませてもらった、2012年09月04日の話なので少し古いがテストがしにくいという問題は一応解決できそう。
ただ根本的解決というよりは暫定対応っぽい解決方法に感じるのでやはり Service Object 相当な実装のほうがいいのかなという印象は変わらず。

FactoryGirlでコールバックをスキップする

15
8
5

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
15
8