はじめに
この記事は今の自分が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
Gemfile
と24203.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"
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は自分が使っているエディタに書き換えてください。
# 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つで
- 何かインスタンス変数に対してメソッドが呼ばれているか?
- 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: :all
はActiveRecord::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_scope
と default_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が呼ばれる。
以上。
最後に
手を動かしながらやったほうが理解が深まると思います。