LoginSignup
110
92

More than 5 years have passed since last update.

Railsの意外な落とし穴? before_xxx で false を返すと処理を停止するが、 nil を返すと続行する

Last updated at Posted at 2015-09-04

はじめに、というか最初に結論

この記事では以下のような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では falsenil を偽の値として扱い、それ以外の値を真として扱う、という言語仕様があります。
なので、コールバックの戻り値も falsenil はどちらも同じように扱われると思っていました。

が!!

before_xxx の場合は false だけが処理停止の条件になり、 nil は続行します。
これはこれでなんとも紛らわしい仕様だと思いませんか?
少なくとも僕はビックリしました。

公式ドキュメントにはどう書いてあるのか?

公式ドキュメントにはどう書いてあるのか調べてみました。

ActiveRecord::Callbacks

Canceling callbacks

If a before_* callback returns false, all the later callbacks and the associated action are cancelled.

以下ざっくり翻訳です。

コールバックのキャンセルについて

before_* コールバックが false を返した場合、それ以降の全コールバックと、呼び出し元のアクションはキャンセルされます。

うーん、「nil は違うよ!」と明示的に書いてもらわないと、Rubyの慣習的に「falsenil も一緒だよね?」と思えなくもありません。(どうでしょうか??)

テストを書いて確認してみた

というわけで、この仕様を確認するテストを書いてみました。

まず、こんなモデルを作ってみました。
コールバックメソッドを一通り定義しています。
(コールバックの一覧はこちらの公式ドキュメントに記載されています)

# 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

上のテストコードから resultfalse になるケース(つまり、保存できないケース)だけを抜き出すと以下のようになります。

:method_before_validation | false | false
:method_before_save       | false | false
:method_before_create     | false | false

これ以外はすべて resulttrue になります。

結論

上のテストからわかる結論は以下の通りです。

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 さん、ありがとうございます!!

110
92
2

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
110
92