酒と泪とRubyとRailsと

Ruby on Rails と Objective-C は酒の肴です!

Switch_point ActiveRecordにメソッドを追加・変更する部分を勉強してみた

昨日DB関連の資料を調べていく中で興味があった『eagletmt/switch_point』がどんなふうに実装されているのかが、興味あって、ソースコードを読んでみました。特に興味があったのは、ActiveRecord::Baseなどにメソッドを追加したり、既存のメソッドに手を加える部分です。まだわかっていない部分も多いのでメモ書きレベルですが、一応せっかくなのでアウトプットしておきます!


既存のActiveRecod::Baseにメソッドを生やす

eagletmt/switch_point - GitHub』のswitch_point/lib/switch_point.rbで既存のActiveRecod::Baseにメソッドを生やす目的で次のようなコードが書かれていました。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
require 'active_support/lazy_load_hooks'
# ...(省略)...

# ActiveRecordが読み込まれたら以下のコードを実行する
# 第一引数(active_record)がフックのキー
# 第二引数のブロックが、ブロックを実行するためのコンテキスト
ActiveSupport.on_load(:active_record) do
  require 'switch_point/model'
  require 'switch_point/connection'

  # SwitchPoint::ModelのメソッドをActiveRecord::Baseにinclude
  ActiveRecord::Base.send(:include, SwitchPoint::Model)

  # class_evalで動的にクラス・メソッドを定義
  ActiveRecord::ConnectionAdapters::AbstractAdapter.class_eval do
    include SwitchPoint::Connection

    # insert, update, deleteメソッドのメソッドを上書き
    # 中ではwritableなDBにつなぎに行っているかのチェックとクエリー・キャッシュをクリアしてからメソッドを実行
    SwitchPoint::Connection::DESTRUCTIVE_METHODS.each do |method_name|
      alias_method_chain method_name, :switch_point
    end
  end
end

遅延読み込みフック lazy_load_hooks#on_load について

on_loadは、ActiveSupportのLazyLoadの機能の一つで、ライブラリの読み込み後に実行したいコードを登録するための機能だそうです。サンプルとしてはこんなかんじです。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
require 'active_support/lazy_load_hooks'

# run_load_hooksが実行されていないので呼ばれない
ActiveSupport.on_load :fuga do
  puts 'start 1st on load'
  fuga
  puts 'end 1st on load'
end

class Fuga
  def self.fuga
    puts 'fugafuga'
  end
end

puts 'before run_load_hooks'
# run_load_hooksを実行すると、4行目の ActiveSupport.on_load が呼ばれる
ActiveSupport.run_load_hooks :fuga, Fuga
puts 'after run_load_hooks'

# run_load_hooks が実行されているのですぐに呼ばれる
ActiveSupport.on_load :fuga do
  puts 'start 2st on load'
  fuga
  puts 'end 2st on load'
end

# 実行結果
# before run_load_hooks
# start 1st on load
# fugafuga
# end 1st on load
# after run_load_hooks
# start 2st on load
# fugafuga
# end 2st on load

コードにちょこちょこ書いていますが、要はActiveSupport.run_load_hooksが実行されないと呼ばれないし、 ActiveSupport.run_load_hooksが呼ばれれば一緒に実行してくれるということっぽいです。

ここいらは、@eielさんのRails Docの記事『RailsDoc - Lazy Load Hooks』を参考にさせて頂きました。最近Rails Docさんにはお世話になりっぱなしです!

alias_method_chain について

alias_method_chainは、既存のメソッドを置き換えをしてくれます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# active_supportのメソッドなのでrequireが必要
require 'active_support/all'

module LogHelper

  def log(message)
    puts message
  end

  def log_with_timestamp(message)
    log_without_timestamp("[#{Time.now}] #{message}")
  end

  # 以下と同義
  # alias_method :log, :log_with_timestamp
  # alias_method :log_without_timestamp, :log
  alias_method_chain :log, :timestamp
end

include LogHelper
log('Hello') #=> [2015-01-02 11:45:01 +0900] Hello

このメソッドには次のルールがあるそうです。

 (A) 前提
   alias_method_chain :xxx, :yyy

 (B) ルール
   (1) 上書きして呼び出すメソッドは、 xxx_with_yyy とする
   (2) 元となるメソッドは、 xxx_without_yyy とする

ちなみにActiveSupport(v4.2.0)のalias_method_chainのソースコードはこんなかんじです。 (わかりやすい^^)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
  def alias_method_chain(target, feature)
    # Strip out punctuation on predicates, bang or writer methods since
    # e.g. target?_without_feature is not a valid method name.
    aliased_target, punctuation = target.to_s.sub(/([?!=])$/, ''), $1
    yield(aliased_target, punctuation) if block_given?

    with_method = "#{aliased_target}_with_#{feature}#{punctuation}"
    without_method = "#{aliased_target}_without_#{feature}#{punctuation}"

    alias_method without_method, target
    alias_method target, with_method

    case
    when public_method_defined?(without_method)
      public target
    when protected_method_defined?(without_method)
      protected target
    when private_method_defined?(without_method)
      private target
    end
  end

シナジーマーケティングさんの、» Rails: alias_method_chain: 既存の処理を修正する常套手段 TECHSCORE BLOG がすごくわかりやすい説明を書いて頂けていたので参考にさせて頂きました。有難うございます!

module#prepend について

module#prepend自体はswitch_pointには出てきませんが、調べていく中でRuby 2.0で導入されたメソッドのmodule#prependalias_method_chainを置き換える事ができるのを知りました。使い方としてはこんな感じ。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
module LoggingWithTimestamp

  def log(message)
    # 呼び出し元のメソッドを呼び出す
    super("[#{Time.now}] #{message}")
  end

end

class Logging
  # LoggingWithTimestampのlogが、Loggingのlogメソッドよりも優先して呼ばれる
  prepend LoggingWithTimestamp

  def log(message)
    puts message
  end
end

Logging.new.log('Hello') #=> [2015-01-02 12:23:15 +0900] Hello

includeprependとの使い分けは次の通り。

* include: 新しい機能を追加するために使う
  * モジュール側のメソッドはクラスのメソッドを上書きできない
  * クラス側のメソッドはモジュールのメソッドを上書きできる

* prepend: 既存の機能の変更をするために使う
  * モジュール側のメソッドはクラス側のメソッドを上書きでる
  * クラス側のメソッドはモジュール側のメソッドを上書きできない

こちらもシナジーマーケティングさんのブログ記事『» Ruby2.0のModule#prependは如何にしてalias_method_chainを撲滅するのか!?』が図解も含めてすごくわかりやすい解説をしてくれています!

あとがき

今までなんの気なしに使ってきたライブラリですが、よく読んでみると色々と勉強になる部分が多くあります。 自分がライブラリ書くときに使えそうなTipsがあってすごい勉強になります!今度はテストとかも読んでみるつもりです!

Special Thanks

Rubyのdefine_method、class_evalで動的に定義されたメソッドの呼出コストを調べてみた - Qiita

おすすめの書籍