Ruby
Rails
RubyOnRails
ソースコードリーディング

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

More than 1 year has passed since last update.


はじめに

この記事は今の自分が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が呼ばれる。

以上。


最後に

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