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


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


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

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

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は、Active SupportのLazyLoadの機能のひとつで、ライブラリの読み込み後に実行したいコードを登録するための機能だそうです。サンプルとしてはこんなかんじです。

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

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

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

🐯 alias_method_chainについて

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

# 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とする

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

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
がすごくわかりやすい説明を書いていただけていたので参考にさせていただきました。ありがたうございます!

🗻 モジュール#prependについて

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

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があってすごい勉強になります! 今度はテストとかも読んでみるつもりです!

🏈 参考リンク

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

🖥 VULTRおすすめ

VULTR」はVPSサーバのサービスです。日本にリージョンがあり、最安は512MBで2.5ドル/月($0.004/時間)で借りることができます。4GBメモリでも月20ドルです。 最近はVULTRのヘビーユーザーになので、「ここ」から会員登録してもらえるとサービス開発が捗ります!

📚 おすすめの書籍