この記事はフィヨルドブートキャンプ Advent Calendar 2025の22日目の記事です。
昨日はodentakashiさんのgem の PR から学んだ ActiveRecord と lease_connection の話でした!
自分もおでんくんに引き続き、ActiveRecordの話題、いっちゃいます🍢
はじめに
皆さんはRuby on Rails使っていますか? Ruby on RailsにはActive RecordというORMがあります。
RailsのActive Recordを使う上で欠かせないのが、『Ruby on Railsガイド(以下Railsガイド)』です。この公式ドキュメントを読むことで、全体像を理解しながら体系的に学ぶことができます。
railsguides.jp
Ruby on Rails ガイド:体系的に Rails を学ぼう
Railsガイドには載っていないような細かい挙動を調べたいときもあります。そんなときは、『Ruby on Rails API』を調べるのが鉄板です。
api.rubyonrails.org
Ruby on Rails API
この2つのドキュメントを参考にするだけで、基本的には困らないのですが……もっと先へ進んでみたくないですか?
そうです。Ruby on Railsそのもののソースコードです。今回は find を取り上げて、このメソッドを読み解いていきましょう。
find ってそもそもなに?
まずはおさらいから。
ActiveRecord の find は 主キー検索 を行うためのメソッドです。
User.find(1)
User.find([1, 2, 3])
シンプルではありますが、内部ではキャッシュの利用、例外の扱い、SQL の組み立てなど様々な処理が行われます。
どこで定義されている?
では、findメソッドはどこで定義されているでしょうか?
GitHubにある rails/rails で調べてみましょう。
github.com
find メソッドは ActiveRecord の FinderMethods モジュールで定義されています。
具体的には次のファイルです。
それでもって、中のコードを読んでいくと、findが定義されています。
def find(*args)
return super if block_given?
find_with_ids(*args)
end
おっ、findは find_with_ids を呼んでいますね。今度はこのメソッドを見てみましょう。
def find_with_ids(*ids)
case ids.size
when 0
error_message = "Couldn't find #{model_name} without an ID"
raise RecordNotFound.new(error_message, model_name, primary_key)
when 1
result = find_one(ids.first)
expects_array ? [ result ] : result
else
find_some(ids)
end
end
このメソッドの最後の方を見ると、idsのサイズが0のときはエラー、1のときはfind_one、それ以外のときはfind_someを呼んでいます。
つまり、findはfind_oneかfind_someを呼んでいる!ということです。*1
両方とも追いかけるとややこしいので、今回はidを一つしか渡さないと仮定して、find_oneを潜っていきましょう。
find_oneはこのように定義されています。
def find_one(id)
relation = if model.composite_primary_key?
where(primary_key.zip(id).to_h)
else
where(primary_key => id)
end
record = relation.take
raise_record_not_found_exception!(id, 0, 1) unless record
record
end
複合キーではない場合と仮定して、ものすごくざっくりと解説してみますが、つまり、whereメソッドで条件に合うものを検索して、そのなかから一つだけを take する。これがfind_oneがやっていることです。
whereはどうなってる?
つまりfindは内部的にwhereを呼んでいることが、Railsの実装を見ていくとわかります。
次にwhereを紹介していきます。*2
whereメソッドはこのファイルで定義されています。
具体的なコードだとここになります。
def where(*args)
if args.empty?
WhereChain.new(spawn)
elsif args.length == 1 && args.first.blank?
self
else
spawn.where!(*args)
end
end
引数はempty?でもblank?でもないはずなので、とりあえずwhere!を見てみましょう。
def where!(opts, *rest)
self.where_clause += build_where_clause(opts, rest)
self
end
どうやら、build_where_clauseっていうメソッドの結果を、どんどんオブジェクトに追加していっているらしいということがわかります。
build_where_clauseへ移動します。
def build_where_clause(opts, rest = [])
opts = sanitize_forbidden_attributes(opts)
if opts.is_a?(Array)
opts, *rest = opts
end
case opts
when String
if rest.empty?
parts = [Arel.sql(opts)]
elsif rest.first.is_a?(Hash) && /:\w+/.match?(opts)
parts = [build_named_bound_sql_literal(opts, rest.first)]
elsif opts.include?("?")
parts = [build_bound_sql_literal(opts, rest)]
else
parts = [Arel.sql(model.sanitize_sql([opts, *rest]))]
end
when Hash
opts = opts.transform_keys do |key|
if key.is_a?(Array)
key.map { |k| model.attribute_aliases[k.to_s] || k.to_s }
else
key = key.to_s
model.attribute_aliases[key] || key
end
end
references = PredicateBuilder.references(opts)
self.references_values |= references unless references.empty?
parts = predicate_builder.build_from_hash(opts) do |table_name|
lookup_table_klass_from_join_dependencies(table_name)
end
when Arel::Nodes::Node
parts = [opts]
else
raise ArgumentError, "Unsupported argument type: #{opts} (#{opts.class})"
end
Relation::WhereClause.new(parts)
end
alias :build_having_clause :build_where_clause
一気に難しくなってきました。
この引数に、例えば文字列を渡すとします(User.where("age > 20")みたいなやつ)。すると、when Stringの条件に入ります。
when String
if rest.empty?
parts = [Arel.sql(opts)]
elsif rest.first.is_a?(Hash) && /:\w+/.match?(opts)
parts = [build_named_bound_sql_literal(opts, rest.first)]
elsif opts.include?("?")
parts = [build_bound_sql_literal(opts, rest)]
else
parts = [Arel.sql(model.sanitize_sql([opts, *rest]))]
end
するとparts = [Arel.sql(opts)]が呼ばれるはず。そして、[Arel.sql("age > 20")]がpartsの中に入ります。*3
最後にRelation::WhereClause.new(parts)が呼び出され、WhereClause オブジェクトが生成されます。
この WhereClause オブジェクトが、Relation オブジェクトの where_clause フィールドに+=でどんどん貯まっていき、最終的にはSQLに変換されて、実行されます。*4
SQLに変換される仕組みや、実行される仕組みまで解説すると、かなり長くなってしまうので、このあたりで止めたいと思います。
おわりに
いかがでしたでしょうか? 後半はちょっとむずかしくなってしまいましたが、前半は分かりやすかったと思います。
RailsはRubyのコードでできています。なので、Rubyに慣れて親しんでいれば、Railsのコードは理解できるようになっています。
もし、公式ドキュメントなどを読んでも分からない挙動に遭遇した場合は、Railsそのもののソースコードを読んでみると、原因を特定できるかもしれません。*5
コードを読み解くことで、勉強になる面は多いですし、貢献できる可能性もあります。みなさんもぜひ、トライしてみてください!
明日のAdvent Calendarは ゆーかさんと mh-mobile さんです。🎄