Edited at

Active Supportのskip_callback動かない問題

More than 1 year has passed since last update.


1. 問題

TemporaryArticleArticleからextendしています。

Articleモデル内に、push_notificationコールバックを定義しています。


models/article.rb

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



models/temporary_article.rb

class TemporaryArticle < Article

belongs_to :public_article, class_name: Article.name
skip_callback :save, :after, :push_notification
end

TemporaryArticlepush_notificationを呼びたくないので、skip_callbackを利用して、push_notificationを呼ばないようにします。

しかし、skip_callbackがうまく動かなくて、TemporaryArticle#saveするとき、 push_notificationはまだ呼ばれています。


2. TL;DR

解決方法: TemporaryArticle定数がある行は各コールバックメソッドの下に置いてください。


models/article.rb

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のソースコードを見てみましょう。


rails/activesupport/lib/active_support/callbacks.rb

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を呼ぶとき、CallBackpush_notificationが追加されてないので、動かなかった。

set_callbackskip_callbackが呼ばれる順番を確認しましょう。


models/article.rb

class Article

has_one :draft_article, dependent: :destroy, class_name: TemporaryArticle
after_save :push_notification
end


models/temporary_article.rb

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: できれば、各コールバックは上に移動する


models/article.rb

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配列を利用して、管理していますが、デメリットがあります。

もし、callbackskip_callbackメソッドを分けて、別々配列で管理すれば、問題解決できるかなと思います。


4.参考

http://guides.rubyonrails.org/autoloading_and_reloading_constants.html