LoginSignup
3

More than 5 years have passed since last update.

railsのプルリクを読んでみた(#24203)

Last updated at Posted at 2017-09-24

はじめに

この記事は今の自分がrailsのプルリクを見た時にどう読むかをまとめたものであるので
効率のいい読み方であるとは限りません。
なんかいい方法あったら教えてほしいです。

この記事は、2017年09月24日に書きました。

結論が知りたい方はまとめを見て下さい。


#24203はどのようなプルリクか

deliveryのidが1〜2のものが何個あるかを取得するメソッド num_deliveries_1_2
select でなくて count で呼びたいというもの

class Order < ActiveRecord::Base
    has_many :deliveries

    def num_deliveries_1_2
-       deliveries.select { |delivery| delivery.id.in?(1..2) }.size
+       deliveries.count { |delivery| delivery.id.in?(1..2) }
    end
end

[参考]

ActiveRecord::Relation#countの修正: 従来は引数にブロックを渡すとエラーなしで無視されたが、RubyのEnumerable#countでレコード数をカウントするようになった
rails 5.1.0から出来るようになった

[注意]
一部(ActiveRecord::Associations::CollectionProxy Class)がこのプルリクで書き換わっている
Remove unnecessary count method for collection proxy


動作確認用のrubyスクリプト

まず動作確認用のrubyスクリプト(24203.rb)を作ります。
使うgem(特にrials)に binding.pry を使ったりしたいのでGemfileでgemを管理することにします。
railsのバージョンは5.1.0にします。
bundle && ruby 24203.rb してエラーが起きないことを確認します。

$ touch 24203.rb
$ bundle init
$ bundle install --path=vendor/bundle
$ $ tree -L 2 
.
├── .bundle
│   └── config
├── 24203.rb
├── Gemfile
├── Gemfile.lock
└── vendor
    └── bundle

Gemfile24203.rbは以下のようになります。

# frozen_string_literal: true
source "https://rubygems.org"

git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }

gem "rails", "5.1.0"
gem "arel", "~> 8.0"
gem "sqlite3"
gem "pry"

24203.rb
require 'bundler'
Bundler.require

require "active_record"
require "minitest/autorun"
require "logger"

# This connection will do for database-independent bug reports.
ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
ActiveRecord::Base.logger = Logger.new(STDOUT)

ActiveRecord::Schema.define do
  create_table :orders, force: true do |t|
  end

  create_table :deliveries, force: true do |t|
    t.integer 'order_id'
  end
end

class Order < ActiveRecord::Base
  has_many :deliveries

  def num_deliveries_1_2
    deliveries.count do |delivery|
      delivery.id.in?(1..2)
    end
  end
end

class Delivery < ActiveRecord::Base
end

# 適当にモデルを作成する
@o = Order.create
3.times { @o.deliveries.create }
p @o.num_deliveries_1_2 # 2


準備(大事)

以下の機能が使えるとソースコドリーディングがしやすくなるかもです。

1. cool-peco (シェルはzshである必要がある)

シェルをbashでなくzshにしてcool-pecoをいれておくと便利です。
cool-peco

2. bundle cd

Gemfileでいれたgemにbundle cdします。頻繁にrailsにbinding.pryするのであったがいいです。
Gemfileでいれたgemにbundle cdする

3. ff (シェルがzshが必要である必要がある)

findでgit grepみたいにファイルを検索できるようにする機能。
Gemfileでいれたgemなのでgit grepが使えないので必要です。
ffは適当に名前つけた。使うにはcool-pecoが必要。
EDITORは自分が使っているエディタに書き換えてください。

~/.zshrc
# pecoでfind
export EDITOR=subl
function peco-find-grep() {
  local res
  local base=$(find -type f | xargs grep -n $1 | peco)
  local file=$(echo $base | awk -F: '{print $1}')
  local file_index=$(echo $base | awk -F: '{print $2}')
  if [ -n "$file" ]; then
    res="$EDITOR $file:$file_index"
  fi

  _cool-peco-insert-command-line $res
}
alias ff=peco-find-grep

4. extended_moduels

extendしたmoduleを表示できるようにするメソッドをModuleクラスに生やす。
gistで見つけたました。24203.rbにこんな感じで書きます。

class Delivery < ActiveRecord::Base
end

+# https://gist.github.com/adzap/158109
+class Module
+  def extended_modules
+    # this exposes the metaclass and for any code inside, +self is scoped to the metaclass
+    class << self
+      self.included_modules
+    end
+  end
+end

さぁ、いよいよ始めていきましょう。


deliveries.count { |delivery| delivery.id.in?(1..2) }の理解

結果的に理解するべきものは以下の項目である。

No. 項目 内容
1 deliveries.count active_record/relations/caluculations.rb(モジュール)
2 ActiveRecord::Base#count ActiveRecord::Querying(モジュール)
3 delegate :count, to: :all Module(クラス)
4 all.count(*args, &block)
5 Enumerable.count(*args, &block) rubyのEnumerableモジュール
6 ActiveRecord::Base#all ActiveRecord::Scoping::Named::ClassMethods(モジュール)
7 ActiveRecord::Base#default_scoped ActiveRecord::Scoping::Named::ClassMethods(モジュール)
8 ActiveRecord::Base#relation ActiveRecord::Core::ClassMethods(モジュール)

適当な所で binding.pry して調べる。
だが、適当に binding.pry すると理解するまでに時間がかかってしまう。
ではどうすればいいのか?

binding.pryを効果的にかける方法

binding.pry を掛けて何がしたいか?」
それは deliveries.countの詳細をみるためである。そのため、①Order#num_deliveriesの内部で一箇所まずかけている。
私はbinding.pryの他に$binding_pry=trueというグローバル変数を定義した。これをどう使うのかというと、@o.deliveriesが定義されたあとのrailsのコードにbinding.pryを掛けるために使うのである。
こんな感じに使う。

def hoge
    binding.pry if $binding_pry
end
class Order < ActiveRecord::Base
  has_many :deliveries

  def num_deliveries_1_2
+   binding.pry
    deliveries.count do |delivery|
      delivery.id.in?(1..2)
    end
  end
end

class Delivery < ActiveRecord::Base
end

# 適当にモデルを作成する
@o = Order.create
10.times { @o.deliveries.create }
+ $binding_pry = true
p @o.num_deliveries_1_2 # 2


1. deliveries.count

[2] pry(main)> $ @o.deliveries.count

From: ./vendor/bundle/ruby/2.4.0/gems/activerecord-5.1.0/lib/active_record/relation/calculations.rb @ line 39:
Owner: ActiveRecord::Calculations
Visibility: public
Number of lines: 5

def count(column_name = nil)
  return super() if block_given?
  calculate(:count, column_name)
end
[3] pry(main)> 

active_record/relation/calculations.rbに定義してあるとある。ここで私が見るべきポイントは2つで

  1. 何かインスタンス変数に対してメソッドが呼ばれているか?
  2. superでメソッドが呼ばれているか?

1か2かの場合は、このファイルに直接 binding.pry if $binding_pry を書いて、superが何かなどを調べるようにしている。
gemにbinding.pryする便利な方法がある

実際に binding.pry if $binding_pry して止めると以下のようになる。

From: ./vendor/bundle/ruby/2.4.0/gems/activerecord-5.1.0/lib/active_record/relation/calculations.rb @ line 40 ActiveRecord::Calculations#count:

    39: def count(column_name = nil)
 => 40:   binding.pry if $binding_pry
    41:   return super() if block_given?
    42:   calculate(:count, column_name)
    43: end

[14] pry(#<Delivery::ActiveRecord_Associations_CollectionProxy>)> self.class.ancestors.select { |a| a.class==Class }
=> [
Delivery(id: integer, order_id: integer), 
ActiveRecord::Base, 
Object, 
BasicObject
] # 見やすいように表示を加工している
[15] pry(#<Delivery::ActiveRecord_Associations_CollectionProxy>)>
[4] pry(#<Delivery::ActiveRecord_Associations_CollectionProxy>)> self.superclass
=> ActiveRecord::Base

self.ancestors.select { |a| a.class==Class } は selfの先祖でクラスのものを調べている。super()ActiveRecord::Base クラスに定義して有りそうだ。(前提知識としてActiveRecord::Baseクラスを継承したクラスでcountクラス・メソッドを呼べることを知ってたら想像がつく。)


2. ActiveRecord::Base#count

pryに続きで以下のようにすると定義が得られる。

[4] pry(#<Delivery::ActiveRecord_Associations_CollectionProxy>)> cd ActiveRecord::Base
[5] pry(ActiveRecord::Base):1> $ count

From: /Users/fukudayukihiro/RubymineProjects/rails-issue/vendor/bundle/ruby/2.4.0/gems/activerecord-5.1.0/lib/active_record/querying.rb @ line 16:
Owner: ActiveRecord::Querying
Visibility: public
Number of lines: 1

delegate :count, :average, :minimum, :maximum, :sum, :calculate, to: :all

all?ってなるかもしれないがこれもpryで調べれば良い。
だがそれは一旦置いといてdelegate :count, to: :allが何かを調べないと答えにはたどり着かない。

3. delegate :count, to: :all

以下のように調べれば良い。

[8] pry(ActiveRecord::Base):1> $ delegate

From: /Users/fukudayukihiro/RubymineProjects/rails-issue/vendor/bundle/ruby/2.4.0/gems/activesupport-5.1.0/lib/active_support/core_ext/module/delegation.rb @ line 156:
Owner: Module
Visibility: public
Number of lines: 65

  def delegate(*methods, to: nil, prefix: nil, allow_nil: nil)
    unless to
      raise ArgumentError, "Delegation needs a target. Supply an options hash with a :to key as the last argument (e.g. delegate :hello, to: :greeter)."
    end

    if prefix == true && /^[^a-z_]/.match?(to)
      raise ArgumentError, "Can only automatically set the delegation prefix when delegating to a method."
    end

    method_prefix = \
      if prefix
        "#{prefix == true ? to : prefix}_"
      else
        ""
      end

    location = caller_locations(1, 1).first
    file, line = location.path, location.lineno

    to = to.to_s
    to = "self.#{to}" if DELEGATION_RESERVED_METHOD_NAMES.include?(to)

    methods.each do |method|
      # Attribute writer methods only accept one argument. Makes sure []=
      # methods still accept two arguments.
      definition = /[^\]]=$/.match?(method) ? "arg" : "*args, &block"

      # The following generated method calls the target exactly once, storing
      # the returned value in a dummy variable.
      #
      # Reason is twofold: On one hand doing less calls is in general better.
      # On the other hand it could be that the target has side-effects,
      # whereas conceptually, from the user point of view, the delegator should
      # be doing one call.
      if allow_nil
        method_def = [
          "def #{method_prefix}#{method}(#{definition})",
          "_ = #{to}",
          "if !_.nil? || nil.respond_to?(:#{method})",
          "  _.#{method}(#{definition})",
          "end",
        "end"
        ].join ";"
      else
        exception = %(raise DelegationError, "#{self}##{method_prefix}#{method} delegated to #{to}.#{method}, but #{to} is nil: \#{self.inspect}")

        method_def = [
          "def #{method_prefix}#{method}(#{definition})",
          " _ = #{to}",
          "  _.#{method}(#{definition})",
          "rescue NoMethodError => e",
          "  if _.nil? && e.name == :#{method}",
          "    #{exception}",
          "  else",
          "    raise",
          "  end",
          "end"
        ].join ";"
      end

      module_eval(method_def, file, line)
    end
  end

最後の module_eval(method_def, file, line) が全てである。
このメソッドはrailsが読み込まれる際に実行される。
さて、実際にdelegate :count, to: :all されると何が実行されるのか
見てみよう。


4. delegate :count, to: :all を追う。

+ require 'pry'
def delegate(*methods, to: nil, prefix: nil, allow_nil: nil)
#
# 省略
#
+      binding.pry if file.match?(/querying/) && method_def.match?(/def count/)

      module_eval(method_def, file, line)
    end
  end

delegate :count, to: :allActiveRecord::Queryingモジュールで定義されているのでfile.match?(/querying/)。見たいメソッドは、
def count; #省略 end;なのでmethod_def.match?(/def count/)
である。

さて、この状態で実行し直してみよう。
binding.pryで止めた場所でmethod_defを実行しております。

From: /Users/fukudayukihiro/RubymineProjects/rails-issue/vendor/bundle/ruby/2.4.0/gems/activesupport-5.1.0/lib/active_support/core_ext/module/delegation.rb @ line 216 Module#delegate:

    211:           "  end",
    212:           "end"
    213:         ].join ";"
    214:       end
    215: 
 => 216:       binding.pry if file.match?(/querying/) && method_def.match?(/def count/)
    217: 
    218:       module_eval(method_def, file, line)
    219:     end
    220:   end
    221: 

[1] pry(ActiveRecord::Querying)> method_def
=> "def count(*args, &block); _ = all;  _.count(*args, &block);rescue NoMethodError => e;  if _.nil? && e.name == :count;    raise DelegationError, \"ActiveRecord::Querying#count delegated to all.count, but all is nil: \#{self.inspect}\";  else;    raise;  end;end"
[2] pry(ActiveRecord::Querying)>

method_defがActiveRecord::Baseに動的に定義されるクラス・メソッドcountになる。文字列なので適当なファイルに書き出してみると、

def count(*args, &block)
    _ = all
    _.count(*args, &block)
rescue NoMethodError => e
    if _.nil? && e.name == :count
        raise DelegationError, \"ActiveRecord::Querying#count delegated to all.count, but all is nil: \#{self.inspect}\"
    else
        raise
    end
end

delegate :count, to: :allによってなされることは、
ActiveRecord::Baseを継承したクラスでcountを呼ぶと、
all.count(*args, &block) が呼ばれることになる。

all.count(*args, &block)を実施に呼び出すとどうなるのか
次に見ていく。


5. all.count(*args, &block)

all.count(*args, &block)のcountメソッドはActiveRecord::Calculations に定義してあるが既に見ているように
このメソッドの中にはsuper()がある。

From: ./vendor/bundle/ruby/2.4.0/gems/activerecord-5.1.0/lib/active_record/relation/calculations.rb @ line 39:
Owner: ActiveRecord::Calculations
Visibility: public
Number of lines: 5

def count(column_name = nil)
  return super() if block_given?
  calculate(:count, column_name)
end

このsuper()が何を呼び出すのかというとrubyで定義してあるEnumerable.count(*args, &block)を呼び出すのである。
なぜなら先祖が以下のようになっているからである。
名前からある程度想像出来るが、実際Enumerableのやつが呼ばれているのを調べる

[4] pry(Delivery)> all.class.ancestors
[1] pry(Delivery)> 
=> [Delivery::ActiveRecord_Relation,
 ActiveRecord::Delegation::ClassSpecificRelation,
 ActiveRecord::Relation,
 ActiveRecord::FinderMethods,
 ActiveRecord::Calculations,
 ActiveRecord::SpawnMethods,
 ActiveRecord::QueryMethods,
 ActiveModel::ForbiddenAttributesProtection,
 ActiveRecord::Batches,
 ActiveRecord::Explain,
 ActiveRecord::Delegation,
 Enumerable,
 ActiveSupport::ToJsonWithActiveSupportEncoder,
 Object,
 ActiveSupport::Dependencies::Loadable,
 Minitest::Expectations,
 JSON::Ext::Generator::GeneratorMethods::Object,
 ActiveSupport::Tryable,
 PP::ObjectMixin,
 Kernel,
 BasicObject]

先祖は上のようなのでcountが定義してあるかどうかは

ff 'module Calculations'
ff 'module SpawnMethods'
ff 'module QueryMethods'
ff 'module ForbiddenAttributesProtection'
ff 'module Batches'
ff 'module Explain'
ff 'module Delegation'
ff 'module Enumerable'

としていってファイルを開いてdef countがあるかどうかを調べれば良い。


最後に、allが何なのかを調べて終わることにする。
結論的には、relation。ActiveRecord::Relationクラスのインスタンスになる。この場合だと、

[1] pry(Delivery)> relation
D, [2017-09-24T20:53:54.096435 #29138] DEBUG -- :   Delivery Load (0.2ms)  SELECT "deliveries".* FROM "deliveries"
=> [#<Delivery:0x007fa6934699c0 id: 1, order_id: 1>,
 #<Delivery:0x007fa693469858 id: 2, order_id: 1>,
 #<Delivery:0x007fa693469718 id: 3, order_id: 1>]
[2] pry(Delivery)> relation.class
=> Delivery::ActiveRecord_Relation


6. ActiveRecord::Base.all

[6] pry(ActiveRecord::Base):1> $ all

From: /Users/fukudayukihiro/RubymineProjects/rails-issue/vendor/bundle/ruby/2.4.0/gems/activerecord-5.1.0/lib/active_record/scoping/named.rb @ line 24:
Owner: ActiveRecord::Scoping::Named::ClassMethods
Visibility: public
Number of lines: 7

def all
  if current_scope
    current_scope.clone
  else
    default_scoped
  end
end

次は current_scopedefault_scoped を調べる必要がありそうだ。
今現在の状況でどっちの場合が実行されているかを調べるために、 binding.pry if $binding_pry を挟む

def all
  if current_scope
+   binding.pry if $binding_pry
    current_scope.clone
  else
+   binding.pry if $binding_pry
    default_scoped
  end
end

すると以下のようになるので default_scoped を調べれば良さそうだ。

[6] pry(#<Delivery::ActiveRecord_Associations_CollectionProxy>)>

From: /Users/fukudayukihiro/RubymineProjects/rails-issue/vendor/bundle/ruby/2.4.0/gems/activerecord-5.1.0/lib/active_record/scoping/named.rb @ line 29 ActiveRecord::Scoping::Named::ClassMethods#all:

    24: def all
    25:   if current_scope
    26:     binding.pry if $binding_pry
    27:     current_scope.clone
    28:   else
 => 29:     binding.pry if $binding_pry
    30:     default_scoped
    31:   end
    32: end

[1] pry(Delivery)>


7. ActiveRecord::Base#default_scoped

[1] pry(Delivery)> $ default_scoped

From: /Users/fukudayukihiro/RubymineProjects/rails-issue/vendor/bundle/ruby/2.4.0/gems/activerecord-5.1.0/lib/active_record/scoping/named.rb @ line 34:
Owner: ActiveRecord::Scoping::Named::ClassMethods
Visibility: public
Number of lines: 9

def default_scoped # :nodoc:
  scope = build_default_scope

  if scope
    relation.spawn.merge!(scope)
  else
    relation
  end
end

build_default_scopeのT/Fによって処理がわかれるので、
binding.pry if $binding_pryを挟む必要がある。

def default_scoped # :nodoc:
  scope = build_default_scope

  if scope
+   binding.pry if $binding_pry
    relation.spawn.merge!(scope)
  else
+   binding.pry if $binding_pry
    relation
  end
end

再度、実行し直すと

[1] pry(Delivery)> 

From: /Users/fukudayukihiro/RubymineProjects/rails-issue/vendor/bundle/ruby/2.4.0/gems/activerecord-5.1.0/lib/active_record/scoping/named.rb @ line 41 ActiveRecord::Scoping::Named::ClassMethods#default_scoped:

    34: def default_scoped # :nodoc:
    35:   scope = build_default_scope
    36: 
    37:   if scope
    38:     binding.pry if $binding_pry
    39:     relation.spawn.merge!(scope)
    40:   else
 => 41:     binding.pry if $binding_pry
    42:     relation
    43:   end
    44: end

relation が何かわかれば良さそうだ。


8. ActiveRecord::Base#relation

[2] pry(Delivery)> $ relation

From: /Users/fukudayukihiro/RubymineProjects/rails-issue/vendor/bundle/ruby/2.4.0/gems/activerecord-5.1.0/lib/active_record/core.rb @ line 307:
Owner: ActiveRecord::Core::ClassMethods
Visibility: private
Number of lines: 9

def relation
  relation = Relation.create(self, arel_table, predicate_builder)

  if finder_needs_type_condition? && !ignore_default_scope?
    relation.where(type_condition).create_with(inheritance_column.to_s => sti_name)
  else
    relation
  end
end
[3] pry(Delivery)> 

if文があるから、binding.pry if bindig.pry を描いて調べてみる

def relation
  relation = Relation.create(self, arel_table, predicate_builder)

  if finder_needs_type_condition? && !ignore_default_scope?
+  binding.pry if $binding_pry
    relation.where(type_condition).create_with(inheritance_column.to_s => sti_name)
  else
+    binding.pry
    relation
  end
end

再度実行しなおす。

From: /Users/fukudayukihiro/RubymineProjects/rails-issue/vendor/bundle/ruby/2.4.0/gems/activerecord-5.1.0/lib/active_record/core.rb @ line 314 ActiveRecord::Core::ClassMethods#relation:

    307: def relation
    308:   relation = Relation.create(self, arel_table, predicate_builder)
    309: 
    310:   if finder_needs_type_condition? && !ignore_default_scope?
    311:     binding.pry if $binding_pry
    312:     relation.where(type_condition).create_with(inheritance_column.to_s => sti_name)
    313:   else
 => 314:     binding.pry if $binding_pry
    315:     relation
    316:   end
    317: end

単純にrelationであるとわかる。

9. ActiveRecord::Relationのインスタンスrelation

relationはActiveRecord::Relationのインスタンスである。
何故ならば、

[3] pry(Delivery)> $ relation

From: /Users/fukudayukihiro/RubymineProjects/rails-issue/vendor/bundle/ruby/2.4.0/gems/activerecord-5.1.0/lib/active_record/relation.rb @ line 3:
Class name: ActiveRecord::Relation
Number of lines: 701

** Warning: Cannot find code for ActiveRecord::Relation. Showing superclass ActiveRecord::Relation instead. **

class Relation
  MULTI_VALUE_METHODS  = [:includes, :eager_load, :preload, :select, :group,
                          :order, :joins, :left_joins, :left_outer_joins, :references,
                          :extending, :unscope]

  SINGLE_VALUE_METHODS = [:limit, :offset, :lock, :readonly, :reordering,
:...skipping...

$ relationしたらRelationクラスが引っかかるからである。
実際に実行してみても

[10] pry(Delivery)> relation.class.ancestors.select{|a| a.class==Class}
=> [
Delivery::ActiveRecord_Relation, 
ActiveRecord::Relation, 
Object, 
BasicObject
]

ちなみに実態は、

[5] pry(Delivery)> relation
=> [#<Delivery:0x007fa6934699c0 id: 1, order_id: 1>,
 #<Delivery:0x007fa693469858 id: 2, order_id: 1>,
 #<Delivery:0x007fa693469718 id: 3, order_id: 1>]
[6] pry(Delivery)> 

となる。


まとめ

最後にまとめておく。

deliveries.count { |delivery| delivery.id.in?(1..2) }

ActiveRecord::Calculationsモジュールのcount

block_given?=trueなのでsuper()が呼ばれる

deliveries.superclass=ActiveRecord::Base

ActiveRecord::Base.countが呼ばれる

delegate :count, to: :allに定義してある

delegateが実行してあるのは、Moduleクラス

ActiveRecord::Base.countの実態は以下のようになる。

def count(*args, &block)
    _ = all
    _.count(*args, &block)
rescue NoMethodError => e
    if _.nil? && e.name == :count
        raise DelegationError, \"ActiveRecord::Querying#count delegated to all.count, but all is nil: \#{self.inspect}\"
    else
        raise
    end
end


all.count(*args, &blcok)が呼ばれる

all.class.ancestorsは

 [Delivery::ActiveRecord_Relation,
 ActiveRecord::Delegation::ClassSpecificRelation,
 ActiveRecord::Relation,
 ActiveRecord::FinderMethods,
 ActiveRecord::Calculations,
 ActiveRecord::SpawnMethods,
 ActiveRecord::QueryMethods,
 ActiveModel::ForbiddenAttributesProtection,
 ActiveRecord::Batches,
 ActiveRecord::Explain,
 ActiveRecord::Delegation,
 Enumerable,
 ActiveSupport::ToJsonWithActiveSupportEncoder,
 Object,
 ActiveSupport::Dependencies::Loadable,
 Minitest::Expectations,
 JSON::Ext::Generator::GeneratorMethods::Object,
 ActiveSupport::Tryable,
 PP::ObjectMixin,
 Kernel,
 BasicObject]


ActiveRecord::Calculationsモジュールのcountが呼ばれる

block_given?=trueなのでsuper()が呼ばれる

rubyのEnumerableのモジュールのcountが呼ばれる。

以上。

最後に

手を動かしながらやったほうが理解が深まると思います。

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
3