LoginSignup
16
11

More than 5 years have passed since last update.

`find_by_xyz("")`と`find_by(xyz: "")`

Last updated at Posted at 2015-10-23

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')

Rails 4で非推奨になった/なっていないfinderメソッドを整理する - Qiita

じゃあ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.prefixMatch.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というクラスがある.そこでprefxsuffixを決めてる.

ところで,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_namesname.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

References

16
11
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
16
11