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.参考