1. 問題
TemporaryArticle
はArticle
からextendしています。
Article
モデル内に、push_notification
コールバックを定義しています。
class Article
include Mongoid::Document
field: name
has_one :draft_article, dependent: :destroy, class_name: TemporaryArticle
after_save :push_notification
def push_notification
#do great work
end
end
class TemporaryArticle < Article
belongs_to :public_article, class_name: Article.name
skip_callback :save, :after, :push_notification
end
TemporaryArticle
はpush_notification
を呼びたくないので、skip_callback
を利用して、push_notification
を呼ばないようにします。
しかし、skip_callback
がうまく動かなくて、TemporaryArticle#save
するとき、 push_notification
はまだ呼ばれています。
2. TL;DR
解決方法: TemporaryArticle
定数がある行は各コールバックメソッドの下に置いてください。
class Article
include Mongoid::Document
field: name
#move this line to the bellow of callback methods
#has_one :draft_article, dependent: :destroy, class_name: TemporaryArticle
after_save :push_notification
# move to here
has_one :draft_article, dependent: :destroy, class_name: TemporaryArticle
def push_notification
#do great work
end
end
3. 詳しく説明
3.1. callback
- How it work?
callback
のソースコードを見てみましょう。
def set_callback(name, *filter_list, &block)
type, filters, options = normalize_callback_params(filter_list, block)
self_chain = get_callbacks name
mapped = filters.map do |filter|
Callback.build(self_chain, filter, type, options)
end
__update_callbacks(name) do |target, chain|
options[:prepend] ? chain.prepend(*mapped) : chain.append(*mapped)
target.set_callbacks name, chain
end
end
def skip_callback(name, *filter_list, &block)
type, filters, options = normalize_callback_params(filter_list, block)
options[:raise] = true unless options.key?(:raise)
__update_callbacks(name) do |target, chain|
filters.each do |filter|
callback = chain.find { |c| c.matches?(type, filter) }
if !callback && options[:raise]
raise ArgumentError, "#{type.to_s.capitalize} #{name} callback #{filter.inspect} has not been defined"
end
if callback && (options.key?(:if) || options.key?(:unless))
new_callback = callback.merge_conditional_options(chain, if_option: options[:if], unless_option: options[:unless])
chain.insert(chain.index(callback), new_callback)
end
chain.delete(callback)
end
target.set_callbacks name, chain
end
end
def define_callbacks(*names)
options = names.extract_options!
names.each do |name|
name = name.to_sym
set_callbacks name, CallbackChain.new(name, options)
module_eval <<-RUBY, __FILE__, __LINE__ + 1
# do great work
RUBY
end
end
各コールバックメソッドは一つのCallBack
配列を利用して、管理しています。
set_callback
するとき、各コールバックメソッドをCallBack
配列に追加します。
skip_callback
するとき、skipするコールバックメソッドをCallBack
配列から除きます。
- メリット
一つの配列を利用するので、管理しやすいです。
- デメリット
skip_callback
を呼ぶタイミングに気をつける必要があります。
なぜなら、set_callback
を呼ぶ前に、skip_callback
を呼んでしまうと、skipしたいコールバックメソッドはCallBack
配列内にまだ追加されていないため、skipできないです。
3.2. 上で取り上げた問題のケース
おそらく、skip_callback
を呼ぶとき、CallBack
にpush_notification
が追加されてないので、動かなかった。
set_callback
とskip_callback
が呼ばれる順番を確認しましょう。
class Article
has_one :draft_article, dependent: :destroy, class_name: TemporaryArticle
after_save :push_notification
end
class TemporaryArticle < Article
belongs_to :public_article, class_name: Article.name
skip_callback :save, :after, :push_notification
end
autoload_pathsにより、ファイルロードの流れは
1. models/temporary_article.rb
を読み込む
2. temporary_article.rb
の行1を読む
- TemporaryArticle
定数を見つけるが、この前、class
キーワードがありますので、定数ではないと判断されます。autoloading
がトリガーされなかった。
- 次、Article
定数を見つけます。これは定数なので、autoloading
がトリガーされます=>models/article.rb
をロードします。
3. models/article.rb
の行2を読む
- TemporaryArticle
定数を見つけます、これは定数なので、autoloading
がトリガーされます =>models/temporary_article.rb
をロードします。
4. temporary_article.rb
は引き続き読む
-行2Article
定数を見つけるが、前に見つけましたので、autoloading
がトリガーされない
-行3 skip_callback
を呼ぶ
5. article.rb
は引き続き読む
- after_save
コールバックを呼ぶ
結局、skip_callback
を呼んでから、after_save
コールバックを呼ばれます。
skip_callback
が効かなかった。
解決策
案1: implicit(暗黙定義)
class_name: TemporaryArticle
を使わずに直接参照します。
例
#has_one :draft_article, dependent: :destroy, class_name: TemporaryArticle
has_one :temporary_article, dependent: :destroy
案2: できれば、各コールバックは上に移動する
class Article
#move this line to the bellow of callback methods
#has_one :draft_article, dependent: :destroy, class_name: TemporaryArticle
after_save :push_notification
# move to here
has_one :draft_article, dependent: :destroy, class_name: TemporaryArticle
end
案3:CallBack
のアルゴリズムを変える
今現在、一つのCallBack
配列を利用して、管理していますが、デメリットがあります。
もし、callback
とskip_callback
メソッドを分けて、別々配列で管理すれば、問題解決できるかなと思います。
4.参考