tl;dr
-
find_by_...
は非推奨になったわけではない - 実際は
method_missing
でキャッチしてfind_by
呼んでるだけ -
find_by
がちょっと速い- 2回目以降はメソッド定義済みになるのでそこまで差は開かないような気もするんだけど…
- メソッドコールが1段多いせい?
find_by_...
ActiveRecordでレコードを1件取り出したいとき,find_by_...
とfind_by
という2種類のメソッドが利用できる.
このfind_by_...
,てっきりdeprecatedになったもんだと思っていたのですが,Rails 4.2時点でまだ生きていた.
Rails 4.0でも引き続き使えるfinderメソッド
User.find_by_email('foo@example.com') User.find_by_email_and_name('foo@example.com', 'Foo Bar')
じゃあfind_by
とどっち使ってもいいのか,雑に検証してみる.
仮説
find_by_...
は実装に際し,method_missing
やそれに類するようなメタプログラミングを利用していると考えられる.
ならばfind_by
がパフォーマンスに優れているような気がする.
Benchmark
環境はCloud9上,Ruby 2.2.1p85,activerecord 4.2.4,DBはsqlite.
ローカルマシンでやってないことに深い意味は無いです.
$ ruby -v
ruby 2.2.1p85 (2015-02-26 revision 49769) [x86_64-linux]
スクリプトは以下(pryで実行).適当に書いたので不適切ならごめんなさい.
N = 100000
Benchmark.bm 24 do |r|
r.report 'find_by(email: "")' do
N.times { User.find_by(email: email) }
end
r.report 'find_by_email("")' do
N.times { User.find_by_email(email) }
end
end
# => user system total real
# => find_by(email: "") 18.910000 3.970000 22.880000 ( 22.908217)
# => find_by_email("") 20.070000 4.300000 24.370000 ( 24.944700)
予想通りfind_by
がちょっと早い.
(ちなみにusers
tableにはindexを貼っていない.わすれてた.)
Implementation of find_by_...
10/23 JST 00:00時点で最新のcommitから.
activerecord/lib/active_record/dynamic_matchers.rb#L14-L23
module ActiveRecord
module DynamicMatchers #:nodoc:
# ...
def method_missing(name, *arguments, &block)
match = Method.match(self, name)
if match && match.valid?
match.define
send(name, *arguments, &block)
else
super
end
end
# ...
end
end
ActiveRecord::DynamicMatchers.method_missing
で実装されている.予想通り,むしろ当たり前のように(?)method_missing
.
Method.match(self, name)
が怪しい.少し下にMethod
classがある.
activerecord/lib/active_record/dynamic_matchers.rb#L31-L38
module ActiveRecord
module DynamicMatchers #:nodoc:
# ...
class Method
@matchers = []
class << self
attr_reader :matchers
def match(model, name)
klass = matchers.find { |k| name =~ k.pattern }
klass.new(model, name) if klass
end
def pattern
@pattern ||= /\A#{prefix}_([_a-zA-Z]\w*)#{suffix}\Z/
end
def prefix
raise NotImplementedError
end
def suffix
''
end
end
# ...
end # end of `Method` class
end
end
Match.prefix
とMatch.suffix
でメソッド名を探索してる.さらに下を見る.
activerecord/lib/active_record/dynamic_matchers.rb#L93-L119
module ActiveRecord
module DynamicMatchers #:nodoc:
# ...
class FindBy < Method
Method.matchers << self
def self.prefix
"find_by"
end
def finder
"find_by"
end
end
class FindByBang < Method
Method.matchers << self
def self.prefix
"find_by"
end
def self.suffix
"!"
end
def finder
"find_by!"
end
end
enc
end
Method
を継承したFindBy
というクラスがある.そこでprefx
とsuffix
を決めてる.
ところで,ActiveRecord::DynamicMatchers.method_missing
ではmatch
したあとにMethod#define
を読んでいる.
module ActiveRecord
module DynamicMatchers #:nodoc:
# ...
class Method
# ...
def define
model.class_eval <<-CODE, __FILE__, __LINE__ + 1
def self.#{name}(#{signature})
#{body}
end
CODE
end
# ...
end
end
end
そういうことです.
実際に実装されるメソッドの実体(Method#body
)を見てみる.
activerecord/lib/active_record/dynamic_matchers.rb#L72-L86
module ActiveRecord
module DynamicMatchers #:nodoc:
# ...
class Method
# ...
def body
"#{finder}(#{attributes_hash})"
end
def signature
attribute_names.map { |name| "_#{name}" }.join(', ')
end
def attributes_hash
"{" + attribute_names.map { |name| ":#{name} => _#{name}" }.join(',') + "}"
end
# ...
end
end
end
find_by_hoge("")
からfind_by(hoge: "")
を呼んでるだけですね.
(attribute_names
はname.match(self.class.pattern)[1].split('_and_')
: つまり,メソッド名をいい感じにバラしただけ)
Implementation of find_by
同様に10/23 JST 00:00時点で最新のcommitから.
activerecord/lib/active_record/core.rb#L174-L201
module ActiveRecord
module Core
module ClassMethods
# ...
def find_by(*args) # :nodoc:
return super if scope_attributes? || !(Hash === args.first) || reflect_on_all_aggregations.any?
hash = args.first
return super if hash.values.any? { |v|
v.nil? || Array === v || Hash === v || Relation === v
}
# We can't cache Post.find_by(author: david) ...yet
return super unless hash.keys.all? { |k| columns_hash.has_key?(k.to_s) }
keys = hash.keys
statement = cached_find_by_statement(keys) { |params|
wheres = keys.each_with_object({}) { |param, o|
o[param] = params.bind
}
where(wheres).limit(1)
}
begin
statement.execute(hash.values, self, connection).first
rescue TypeError => e
raise ActiveRecord::StatementInvalid.new(e.message, e)
rescue RangeError
nil
end
end
# ...
end
end
end