どうでもいいネタ、やってはいけないネタ。
コールバック内のthrow :abort
の挙動を調べる機会があって、その挙動を変えてみた、という内容。
before_validation
を例にします。
ソースを追う
以下のようにコールバックが設定されている。引数を確認。
included do
include ActiveSupport::Callbacks
define_callbacks :validation,
skip_after_callbacks_if_terminated: true,
scope: [:kind, :name]
define_callbacksの定義は以下のようになっている。多分、上記Hashの部分は options
として CallbackChain.new
に渡されている。
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
が設定されるが、上書き可能になっている。
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
が発生するとその次の行を実行しないので terminate
は true
のままになる。 true
の場合はチェーンが終了するようだ。
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
クラスがあった場合、
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
を入れた場合は、
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"
だけ追加したものを設定してみると、
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
で中断しないように変更してみる。
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 :abort
を throw :end
に変更してみると、
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
を返すようにすれば、
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
は変更可能。- しかし、変更することの影響は大きい。
- 条件を追加したい、という用途はあるかもしれない。
- 特定の例外のみ追加で補足する、とか。ないかな・・・。