はじめに
Railsのコードリーディングを少しだけうまく進められるようになってきたため、やり方を備忘録としてまとめます。
対象読者は、Railsのコードリーディングをしていきたいけど、そもそもメソッドがどこにあるかすら見つけられない、といった状況のエンジニアです。
この記事を最後まで読めば、気になるメソッドの定義場所を1人で探せるようになるかなと思います。
※バージョンはRuby 3.3, Rails 7.2です。
問題
普段使っているフレームワークをより深く学びたいと思って、Railsのコードリーディングを始めています。
あまりにも規模が大きくどこから読んでいいか分からないため、まずはよく使うメソッドを1つずつ見ていこうと考えました。
しかし、そもそもメソッドがどこに定義されているかを発見するのにも一苦労です。
仮に特定できたとしても、その中でさらに他のメソッドが呼ばれており、追いかけていくことができなくなってしまいました。
解決策
method
メソッドとsource_location
メソッドを活用することで、Railsのコードリーディングが徐々に進められるようになりました。
method
メソッドとは?
method
メソッドとは、RubyのObject
クラスに定義されているメソッドです。
指定したメソッド名に対応するメソッドオブジェクトを返してくれます。
具体的には以下の機能を持っています。
- 引数で指定したメソッドを
Method
クラスのインスタンスとしてオブジェクト化する - 返された
Method
オブジェクトは元のメソッドと同じ挙動をとる - メソッドの詳細な情報(定義場所、引数など)へアクセスできるようになる
サンプルコード
class Sample
def hello(name)
"Hello, #{name}"
end
end
obj = Sample.new
# hello メソッドを Method オブジェクトとして取得
method_obj = obj.method(:hello)
# Method オブジェクトと call メソッドを使って hello メソッドを呼び出す
p method_obj.call("Rails") #=> "Hello, Rails"
# Method オブジェクトの情報を取得
p method_obj.name #=> :hello
p method_obj.owner #=> Sample
p method_obj.receiver #=> #<Sample:0x00000001048c7a08>
p method_obj.parameters #=> [[:req, :name]]
source_location
メソッドとは?
source_location
メソッドとは、RubyのMethod
クラスに定義されているメソッドです。
対象のメソッドが定義されているファイルのパスと行番号を配列で返してくれます。
具体的な機能は以下のとおりです。
- メソッドが定義されているソースファイルの絶対パスを返す
- そのファイル内でメソッドが定義されている行数も返す
- Rubyの組み込みメソッドの場合は
nil
を返す
サンプルコード
# 自作メソッドの場合
class OriginalClass
def original_method
"This is original method."
end
end
obj = OriginalClass.new
method_obj = obj.method(:original_method)
p method_obj.source_location
#=> ["sample.rb", 3]
# 組み込みメソッドの場合
p "hello".method(:upcase).source_location
#=> nil
# gemのメソッドの場合
require 'json'
p JSON.method(:parse).source_location
#=> ["/opt/homebrew/lib/ruby/gems/3.3.0/gems/json-2.7.2/lib/json/common.rb", 219]
method
とsource_location
の組み合わせ
上記のサンプルコードで注目してもらいたいのが次の箇所です。
# gemのメソッドの場合
require 'json'
p JSON.method(:parse).source_location
#=> ["/opt/homebrew/lib/ruby/gems/3.3.0/gems/json-2.7.2/lib/json/common.rb", 219]
これはJSON
クラスのparse
メソッドが/lib/json/common.rb"
の219行目に定義されていることを示しています。
このように、method
メソッドとsource_location
メソッドを組み合わせることで、探しているメソッドがgemのソースファイルのどこに定義されているか特定できるのです。
method
とsource_location
を使ってRailsのコードリーディングを進める
今回はActive Recordのcount
, size
, length
の3メソッドを題材にしていきます。
最初にposts = Post.all
でActive Recordのオブジェクトを取得しておきます。
rails72-app(dev)> posts = Post.all
Post Load (9.4ms) SELECT "posts".* FROM "posts" /* loading for pp */ LIMIT ? [["LIMIT", 11]]
=>
[#<Post:0x0000000125d3e570 id: 1, content: "Updated Content", created_at: "2024-07-21 08:27:20.341870000 +0000", updated_at: "2024-07-21 08:27:20.381694000 +0000">,
...
count
メソッドの定義場所を確認
まずはcount
メソッドの定義場所を確認します。
rails72-app(dev)> posts.method(:count).source_location
=> ["/opt/homebrew/lib/ruby/gems/3.3.0/gems/activerecord-7.2.0/lib/active_record/relation/calculations.rb", 94]
/activerecord-7.2.0/lib/active_record/relation/calculations.rb
の94行目に書かれているようです。
実際に見に行ってみると、たしかにcount
メソッドが定義されていました。
# Count the records.
#
# Person.count
# # => the total count of all people
#
# Person.count(:age)
# # => returns the total count of all people whose age is present in database
#
# Person.count(:all)
# # => performs a COUNT(*) (:all is an alias for '*')
#
# Person.distinct.count(:age)
# # => counts the number of different age values
#
# If #count is used with {Relation#group}[rdoc-ref:QueryMethods#group],
# it returns a Hash whose keys represent the aggregated column,
# and the values are the respective amounts:
#
# Person.group(:city).count
# # => { 'Rome' => 5, 'Paris' => 3 }
#
# If #count is used with {Relation#group}[rdoc-ref:QueryMethods#group] for multiple columns, it returns a Hash whose
# keys are an array containing the individual values of each column and the value
# of each key would be the #count.
#
# Article.group(:status, :category).count
# # => {["draft", "business"]=>10, ["draft", "technology"]=>4, ["published", "technology"]=>2}
#
# If #count is used with {Relation#select}[rdoc-ref:QueryMethods#select], it will count the selected columns:
#
# Person.select(:age).count
# # => counts the number of different age values
#
# Note: not all valid {Relation#select}[rdoc-ref:QueryMethods#select] expressions are valid #count expressions. The specifics differ
# between databases. In invalid cases, an error from the database is thrown.
#
# When given a block, loads all records in the relation, if the relation
# hasn't been loaded yet. Calls the block with each record in the relation.
# Returns the number of records for which the block returns a truthy value.
#
# Person.count { |person| person.age > 21 }
# # => counts the number of people older that 21
#
# Note: If there are a lot of records in the relation, loading all records
# could result in performance issues.
def count(column_name = nil)
if block_given?
unless column_name.nil?
raise ArgumentError, "Column name argument is not supported when a block is passed."
end
super()
else
calculate(:count, column_name)
end
end
このコードを見ると、最後にcalculate(:count, column_name)
が実行されているため、calculate
の定義を追う必要があります。
rails72-app(dev)> posts.method(:calculate).source_location
=> ["/opt/homebrew/lib/ruby/gems/3.3.0/gems/activerecord-7.2.0/lib/active_record/relation/calculations.rb", 217]
同じファイルの217行目に定義されていました。
def calculate(operation, column_name)
operation = operation.to_s.downcase
if @none
case operation
when "count", "sum"
result = group_values.any? ? Hash.new : 0
return @async ? Promise::Complete.new(result) : result
when "average", "minimum", "maximum"
result = group_values.any? ? Hash.new : nil
return @async ? Promise::Complete.new(result) : result
end
end
if has_include?(column_name)
relation = apply_join_dependency
if operation == "count"
unless distinct_value || distinct_select?(column_name || select_for_count)
relation.distinct!
relation.select_values = Array(model.primary_key || table[Arel.star])
end
# PostgreSQL: ORDER BY expressions must appear in SELECT list when using DISTINCT
relation.order_values = [] if group_values.empty?
end
relation.calculate(operation, column_name)
else
perform_calculation(operation, column_name)
end
end
一番最後の行のperform_calculation(operation, column_name)
も同様に、位置を特定できます。
rails72-app(dev)> posts.method(:perform_calculation).source_location
=> ["/opt/homebrew/lib/ruby/gems/3.3.0/gems/activerecord-7.2.0/lib/active_record/relation/calculations.rb", 418]
このようにmethod
とsource_location
を使うことで、メソッドの定義場所を追いかけていくことが可能です。
size
メソッドの定義場所を確認
次にsize
メソッドを確認します。
rails72-app(dev)> posts.method(:size).source_location
=> ["/opt/homebrew/lib/ruby/gems/3.3.0/gems/activerecord-7.2.0/lib/active_record/relation.rb", 346]
/activerecord-7.2.0/lib/active_record/relation.rb
の346行目に書かれているようです。
実際に見にいってみると、たしかにsize
メソッドが定義されていました。
# Returns size of the records.
def size
if loaded?
records.length
else
count(:all)
end
end
loaded?
の定義を追いかけることももちろん可能です。
rails72-app(dev)> posts.method(:loaded?).source_location
=> ["/opt/homebrew/lib/ruby/gems/3.3.0/gems/activerecord-7.2.0/lib/active_record/relation.rb", 71]
また、if loaded?
の条件分岐の結果を知りたい場合は、次のようにsend
メソッドを活用できます。
rails72-app(dev)> posts.send(:loaded?)
=> true
send
メソッドについては下記の記事にまとめています。
length
メソッドの定義場所を確認
最後にlength
メソッドです。
rails72-app(dev)> posts.method(:length).source_location
=> ["/opt/homebrew/lib/ruby/gems/3.3.0/gems/activerecord-7.2.0/lib/active_record/relation/delegation.rb", 98]
/activerecord-7.2.0/lib/activerecord-7.2.0/lib/active_record/relation/delegation.rb
の98行目に書かれているようです。
実際に見にいきます。
# This module creates compiled delegation methods dynamically at runtime, which makes
# subsequent calls to that method faster by avoiding method_missing. The delegations
# may vary depending on the model of a relation, so we create a subclass of Relation
# for each different model, and the delegations are compiled into that subclass only.
delegate :to_xml, :encode_with, :length, :each, :join, :intersect?,
:[], :&, :|, :+, :-, :sample, :reverse, :rotate, :compact, :in_groups, :in_groups_of,
:to_sentence, :to_fs, :to_formatted_s, :as_json,
:shuffle, :split, :slice, :index, :rindex, to: :records
これまで見てきた2つとは異なり、delegate
を用いて委譲されています。
to: :records
となっているため、実質的にはrecords.length
と定義されている状態です。
そこでrecords
メソッドを探索します。
rails72-app(dev)> posts.method(:records).source_location
=> ["/opt/homebrew/lib/ruby/gems/3.3.0/gems/activerecord-7.2.0/lib/active_record/relation.rb", 335]
/activerecord-7.2.0/lib/activerecord-7.2.0/lib/active_record/relation.rb
の335行目を見にいきます。
def records # :nodoc:
load
@records
end
この箇所で@records
オブジェクトを返しています。
この@records
オブジェクトはArrayクラスの配列のようです。
rails72-app(dev)> posts.instance_variable_get(:@records).class
=> Array
つまり@records.length
のlength
は、最終的にはRubyのArrayクラスのlength
メソッドを実行しているのですね。
おわりに
この記事ではmethod
メソッドとsource_location
メソッドを用いてRailsのメソッド定義の場所を探す方法をまとめました。
これを知ってから少しずつRailsのコードリーディングが進められるようになりました。
やったことがない方はぜひ取り組んでみてください。
また、この記事で題材としたcount
, size
, length
の違いについては以下の記事にまとめているため、こちらも読んでみてください。
最後に、記事の内容に誤りがありましたらご指摘いただけますと幸いです。
参考資料