3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

callbackのthrow :abortの挙動を変える

Posted at

どうでもいいネタ、やってはいけないネタ。

コールバック内のthrow :abortの挙動を調べる機会があって、その挙動を変えてみた、という内容。
before_validationを例にします。

ソースを追う

以下のようにコールバックが設定されている。引数を確認。

activemodel-5.2.2/lib/active_model/validations/callbacks.rb
      included do
        include ActiveSupport::Callbacks
        define_callbacks :validation,
                         skip_after_callbacks_if_terminated: true,
                         scope: [:kind, :name]

define_callbacksの定義は以下のようになっている。多分、上記Hashの部分は options として CallbackChain.new に渡されている。

activesupport-5.2.2/lib/active_support/callbacks.rb
        def define_callbacks(*names)
          options = names.extract_options!

          names.each do |name|
            name = name.to_sym

            set_callbacks name, CallbackChain.new(name, options)

CallbackChainクラスは、以下のようになっている。上記の options は、 config という名前になっている。 :terminator というキーは、デフォルトでは default_terminator が設定されるが、上書き可能になっている。

activesupport-5.2.2/lib/active_support/callbacks.rb
      class CallbackChain #:nodoc:#
        include Enumerable

        attr_reader :name, :config

        def initialize(name, config)
          @name = name
          @config = {
            scope: [:kind],
            terminator: default_terminator
          }.merge!(config)
          @chain = []
          @callbacks = nil
          @mutex = Mutex.new
        end

default_terminator は、以下のようになっている。 result_lambda.call の結果 :abort が発生するとその次の行を実行しないので terminatetrue のままになる。 true の場合はチェーンが終了するようだ。

activesupport-5.2.2/lib/active_support/callbacks.rb
          def default_terminator
            Proc.new do |target, result_lambda|
              terminate = true
              catch(:abort) do
                result_lambda.call
                terminate = false
              end
              terminate
            end
          end

ということで、この :terminator の値を default_terminator から別のものに上書きしてやれば throw :abort の挙動を変えることができる。(もちろん、それに意味があるとは思わない)

現状確認

以下のような Grade クラスがあった場合、

app/models/Grade.rb
class Grade < ApplicationRecord
  before_validation do |grade|
    puts "before_validation 1"
  end

  before_validation do |grade|
    puts "before_validation 2"
  end
end

以下のように2つ実行される。

> Grade.new.valid?
before_validation 1
before_validation 2

以下のように throw :abort を入れた場合は、

app/models/Grade.rb
class Grade < ApplicationRecord
  before_validation do |grade|
    puts "before_validation 1"
    throw :abort # これを追加
  end

  before_validation do |grade|
    puts "before_validation 2"
  end
end

以下のように2つ目が実行されないことがわかる。

> Grade.new.valid?
before_validation 1

挙動を変更

では、:terminatorの値を上書きしてみる。以下のように puts "overriden" だけ追加したものを設定してみると、

app/models/Grade.rb
class Grade < ApplicationRecord
  include ActiveSupport::Callbacks
  define_callbacks :validation, {
                   skip_after_callbacks_if_terminated: true,
                   scope: [:kind, :name],
                   terminator: Proc.new do |target, result_lambda|
                      puts "overriden" # ここがdefault_terminatorと異なる
                      terminate = true
                      catch(:abort) do
                        result_lambda.call
                        terminate = false
                      end
                      terminate
                    end
                   }

  before_validation do |grade|
    puts "before_validation 1"
    throw :abort
  end

  before_validation do |grade|
    puts "before_validation 2"
  end
end

以下のようにオーバーライドされていることを確認できる。

> Grade.new.valid?
overridden
before_validation 1

では、 :abort で中断しないように変更してみる。

app/models/Grade.rb
                   terminator: Proc.new do |target, result_lambda|
                      terminate = true
                      catch(:end) do # :abort から :end に変更してみた
                        result_lambda.call
                        terminate = false
                      end
                      terminate
                    end

このようにすると、:abortでは中断しなくなるが、、、catchされなくなってしまうため、以下のように例外が発生してしまう・・・。確かに・・・。

> Grade.new.valid?
before_validation 1
UncaughtThrowError: uncaught throw :abort

そのため、 throw :abortthrow :end に変更してみると、

app/models/Grade.rb
  before_validation do |grade|
    puts "before_validation 1"
    throw :end # 投げるものも :end に変更
  end

  before_validation do |grade|
    puts "before_validation 2"
  end
end

以下のように正常に中断してくれる。

> Grade.new.valid?
before_validation 1

以下のように常に false を返すようにすれば、

app/models/Grade.rb
                   terminator: Proc.new do |target, result_lambda|
                      terminate = true
                      catch(:abort) do
                        result_lambda.call
                        terminate = false
                      end
                      terminate
                      false # 常に false を返す
                    end

throw :abort では中断しなくなる。

> Grade.new.valid?
before_validation 1
before_validation 2

まとめ

  • :abortは変更可能。
    • しかし、変更することの影響は大きい。
  • 条件を追加したい、という用途はあるかもしれない。
    • 特定の例外のみ追加で補足する、とか。ないかな・・・。
3
2
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
3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?