はじめに、というか最初に結論
この記事では以下のようなRails(ActiveRecord)のコールバック仕様を説明します。
before_xxx が false を返した場合のみ保存処理が停止する
before_xxx が nil を返した場合や、after_xxx が false を返した場合はそのまま保存される
この仕様を知らなかった人は先に進みましょう。
知っていた人も興味があれば続きを読んでください。
動作確認したRailsバージョン
- Rails 4.2.4
Railsあるある問題のひとつ、「before_xxx の戻り値を間違える」
知っている人は知っている、そして知っている人でもたまにうっかり間違える、Railsのよくある落とし穴として before_valdiation や before_save の戻り値のワナがあります。
この問題については @suginoy さんがドンピシャな記事を書いてくれています。
rails3 - ActiveRecordのbefore_saveでうっかり - Qiita
以下は上の記事に載っているコード例です。
class Item < ActiveRecord::Base
before_save :prepare_save # This callback doesn't validate
def prepare_save
if hoge_check_ok?
self.checked = true
else
self.checked = false
end
end
end
このコードの問題点は次の通りです。
まず、before_save コールバックで false
が返されるとそこで処理が止まってしまい、レコードが保存されないというのがRailsの(なかなかいやらしい)仕様です。
なので、else
に入ってくるとコールバックの戻り値が false
になり、処理が止まってしまいます。(そして大半のケースで、これはバグとして扱われます)
詳細は元の記事の説明を参照してください。
false と nil は同じか、別物か?
一方、Rubyでは false
と nil
を偽の値として扱い、それ以外の値を真として扱う、という言語仕様があります。
なので、コールバックの戻り値も false
と nil
はどちらも同じように扱われると思っていました。
が!!
before_xxx の場合は false
だけが処理停止の条件になり、 nil
は続行します。
これはこれでなんとも紛らわしい仕様だと思いませんか?
少なくとも僕はビックリしました。
公式ドキュメントにはどう書いてあるのか?
公式ドキュメントにはどう書いてあるのか調べてみました。
Canceling callbacks
If a before_* callback returns false, all the later callbacks and the associated action are cancelled.
以下ざっくり翻訳です。
コールバックのキャンセルについて
before_* コールバックが false を返した場合、それ以降の全コールバックと、呼び出し元のアクションはキャンセルされます。
うーん、「nil
は違うよ!」と明示的に書いてもらわないと、Rubyの慣習的に「false
も nil
も一緒だよね?」と思えなくもありません。(どうでしょうか??)
テストを書いて確認してみた
というわけで、この仕様を確認するテストを書いてみました。
まず、こんなモデルを作ってみました。
コールバックメソッドを一通り定義しています。
(コールバックの一覧はこちらの公式ドキュメントに記載されています)
# user.rb
class User < ActiveRecord::Base
validates :name, presence: true
before_validation :method_before_validation
def method_before_validation
true
end
after_validation :method_after_validation
def method_after_validation
true
end
before_save :method_before_save
def method_before_save
true
end
before_create :method_before_create
def method_before_create
true
end
after_create :method_after_create
def method_after_create
true
end
after_save :method_after_save
def method_after_save
true
end
after_commit :method_after_commit
def method_after_commit
true
end
end
それから、RSpecのテストコードはこんなふうに書きました。
rspec-parameterizedとモックを使って順番にコールバックの戻り値を true
または false
に切り換え、保存できる/できないを検証しています。
(RSpecのモックの使い方についてはこちらの記事をご覧ください)
# user_spec.rb
require 'rails_helper'
RSpec.describe User, type: :model do
using RSpec::Parameterized::TableSyntax
let(:user) { User.new(name: 'Alice') }
example 'default implementation' do
expect(user.save).to be_truthy
end
where(:callback, :value, :result) do
:method_before_validation | false | false
:method_before_validation | nil | true
:method_after_validation | false | true
:method_after_validation | nil | true
:method_before_save | false | false
:method_before_save | nil | true
:method_before_create | false | false
:method_before_create | nil | true
:method_after_create | false | true
:method_after_create | nil | true
:method_after_save | false | true
:method_after_save | nil | true
:method_after_commit | false | true
:method_after_commit | nil | true
end
with_them do
example 'callback value' do
allow(user).to receive(callback).and_return(value)
expect(user.save).to eq result
end
end
end
上のテストコードから result
が false
になるケース(つまり、保存できないケース)だけを抜き出すと以下のようになります。
:method_before_validation | false | false
:method_before_save | false | false
:method_before_create | false | false
これ以外はすべて result
が true
になります。
結論
上のテストからわかる結論は以下の通りです。
before_xxx が false を返した場合のみ保存処理が停止する
before_xxx が nil を返した場合や、after_xxx が false を返した場合はそのまま保存される
知らなかった人はこの機会に覚えておきましょう!
追記:Rails 5での仕様変更について
Rails 5では仕様が変わり、 before_xxx で false
を返してもそのまま処理が続行されるようです。
コールバックで処理を停止したいときは throw :abort
します。
before_save :ensure_admin
def ensure_admin
throw(:abort) unless self.admin
end
こちらのスライドに詳しい情報が載っています。
Better Callbacks in Rails 5 // Speaker Deck
情報提供してくれた @suginoy さん、ありがとうございます!!