LoginSignup
15
3

More than 5 years have passed since last update.

Active Supportのskip_callback動かない問題

Last updated at Posted at 2017-12-19

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

15
3
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
15
3