こんにちは、サーバーワークス OSS探求部のくればやしです。(「部」は部活の部)
以下の記事ではActive Recordの findメソッドを例に、生成されるモデルのオブジェクトにメソッドがどのように動的に定義されるか等を探っていきました。
【OSSリーディング】Active Recordの内部実装を覗いてみた - サーバーワークスエンジニアブログ
find メソッドの場合は、メソッドが実行されると同時にDBにクエリを実行するため、DBへの処理タイミングといった意味では比較的シンプルに捉えることができました。
他方、Active Record で提供される where メソッド等は、それ単体で実行されたタイミングではDBにアクセスせず、レコードが必要になったタイミングでクエリが実行されます。
When an Active Record method is called, the query is not immediately generated and sent to the database. The query is sent only when the data is actually needed. guides.rubyonrails.org
例えば、
- ①のように .where を実行しただけではDBにアクセスはせずActiveRecord::Relationインスタンスが生成されるだけです
- ②のように .where を実行した結果を出力しようとするとDBにアクセスします
- ③のように別のメソッドを実行してレコードを取得しようとするとDBにアクセスします
User.where(name: "akira") # => ①DBへのアクセスはしない(ActiveRecord::Relationインスタンスが生成される) puts User.where(name: "akira") # => ②DBへアクセスする User.where(name: "akira").first #=> ③DBへアクセスする
今回、①②③それぞれについて、以下に焦点を当ててソースコードを探っていきたいと思います。
① ActiveRecord::Relation インスタンスがどのように生成されるか
②③ DBへアクセスする部分はどのように実装されているか
環境は前述の記事と同様とします。
ソースコードリーディング
① ActiveRecord::Relation インスタンスがどのように生成されるか
User.where の結果を展開すると User::ActiveRecord_Relation のインスタンスオブジェクトとなっています。
User は今回のサンプルコード内で ActiveRecord::Base を継承したクラス名です。つまり、ユーザーが定義したクラス名の名前空間で、 ActiveRecord_Relation クラスが動的に定義されていると分かります。
class User < ActiveRecord::Base` end
where メソッドを実行したところからソースコードを探っていってみます。 source_location メソッドで定義場所を確認すると、以下 ActiveRecord::Querying モジュールに定義されていると分かります。
delegate されているため、実際には all.where が実行されていると分かるため、 .all を見てみます。
module ActiveRecord module Querying QUERYING_METHODS = [ :find, :find_by, :find_by!, :take, :take!, :sole, :find_sole_by, :first, :first!, :last, :last!, :second, :second!, :third, :third!, :fourth, :fourth!, :fifth, :fifth!, :forty_two, :forty_two!, :third_to_last, :third_to_last!, :second_to_last, :second_to_last!, :exists?, :any?, :many?, :none?, :one?, :first_or_create, :first_or_create!, :first_or_initialize, :find_or_create_by, :find_or_create_by!, :find_or_initialize_by, :create_or_find_by, :create_or_find_by!, :destroy_all, :delete_all, :update_all, :touch_all, :destroy_by, :delete_by, :find_each, :find_in_batches, :in_batches, :select, :reselect, :order, :in_order_of, :reorder, :group, :limit, :offset, :joins, :left_joins, :left_outer_joins, :where, :rewhere, :invert_where, :preload, :extract_associated, :eager_load, :includes, :from, :lock, :readonly, :and, :or, :annotate, :optimizer_hints, :extending, :having, :create_with, :distinct, :references, :none, :unscope, :merge, :except, :only, :count, :average, :minimum, :maximum, :sum, :calculate, :pluck, :pick, :ids, :strict_loading, :excluding, :without ].freeze # :nodoc: delegate(*QUERYING_METHODS, to: :all)
rails/activerecord/lib/active_record/querying.rb at v7.0.5 · rails/rails · GitHub
all は ActiveRecord::Scoping::Named::ClassMethods モジュールで定義されています。
rails/activerecord/lib/active_record/scoping/named.rb at v7.0.5 · rails/rails · GitHub
さらに進むと、同モジュールの default_scoped メソッドが呼ばれ、この引数の relation メソッドの結果が最終的に返っているようです。この relation メソッドをさらに読んでいきます。
def default_scoped(scope = relation, all_queries: nil) build_default_scope(scope, all_queries: all_queries) || scope end
rails/activerecord/lib/active_record/scoping/named.rb at v7.0.5 · rails/rails · GitHub
relation メソッドは ActiveRecord::Core モジュールに定義されています。返り値は Relation.create メソッドの結果となっていました。これをさらに探っていきます。
Relation は ActiveRecord::Relation で定義されたクラスです。
def relation relation = Relation.create(self)
rails/activerecord/lib/active_record/core.rb at v7.0.5 · rails/rails · GitHub
すると、 ActiveRecord::Delegation::DelegateCache モジュールで定義されている create メソッドが呼ばれ、
def create(klass, *args, **kwargs) relation_class_for(klass).new(klass, *args, **kwargs) end
rails/activerecord/lib/active_record/relation/delegation.rb at v7.0.5 · rails/rails · GitHub
initialize_relation_delegate_cache メソッドの結果が返ります。ここで、 ActiveRecord::Relation のインスタンスを生成しているようなので読んでいきます。
- 9行目、
delegate = Class.new(klass)でActiveRecord::Relationを親クラスに持つクラスがdelegateに動的に定義されています。mangled_nameの中身はActiveRecord_Relationです。
- 14行目、
const_set mangled_name, delegateで、self= User を名前区間に持っているためdelegateのクラス名がUser::ActiveRecord_Relationとして定義されます。
def initialize_relation_delegate_cache @relation_delegate_cache = cache = {} [ ActiveRecord::Relation, ActiveRecord::Associations::CollectionProxy, ActiveRecord::AssociationRelation, ActiveRecord::DisableJoinsAssociationRelation ].each do |klass| delegate = Class.new(klass) { include ClassSpecificRelation } include_relation_methods(delegate) mangled_name = klass.name.gsub("::", "_") const_set mangled_name, delegate private_constant mangled_name cache[klass] = delegate end end
rails/activerecord/lib/active_record/relation/delegation.rb at v7.0.5 · rails/rails · GitHub
これにより、 ActiveRecord::Relation を親クラスに持つ User::ActiveRecord_Relation インスタンスが生成されることを確認できました。
② DBへアクセスする部分はどのように実装されているか
さて、では putsした時にどのようにDBにアクセスしているかを見ていきたいと思います。
puts User.where(name: "akira") #=> DBにアクセスする
まず puts メソッドですが、
配列や文字列以外のオブジェクトが引数として与えられた場合には、当該オブジェクトを最初に to_ary により配列へ、次に to_s メソッドにより文字列へ変換を試みます。 Kernel.#puts (Ruby 3.2 リファレンスマニュアル)
の通り、今回の場合は対象が User::ActiveRecord_Relation のインスタンスのため、 to_ary が呼ばれます。 ActiveRecord::Relation クラスに実装されています。
def to_ary records.dup end
rails/activerecord/lib/active_record/relation.rb at v7.0.5 · rails/rails · GitHub
同クラスの #records メソッドが呼ばれています。
def records # :nodoc: load @records end
rails/activerecord/lib/active_record/relation.rb at v7.0.5 · rails/rails · GitHub
この load メソッドをたどっていくと、同クラスの exec_main_query メソッドにいきます。
今回の場合は下記16行目の処理にいきます。
def exec_main_query(async: false) skip_query_cache_if_necessary do if where_clause.contradiction? [].freeze elsif eager_loading? apply_join_dependency do |relation, join_dependency| if relation.null_relation? [].freeze else relation = join_dependency.apply_column_aliases(relation) @_join_dependency = join_dependency connection.select_all(relation.arel, "SQL", async: async) end end else klass._query_by_sql(arel, async: async) end end end
rails/activerecord/lib/active_record/relation.rb at v7.0.5 · rails/rails · GitHub
この16行目の _query_by_sql メソッドですが、このメソッドは前回の記事で見覚えがあります。
前回記事を参照すると分かる通り、このメソッドを通してSQLクエリが実行されます。
③ DBへアクセスする部分はどのように実装されているか
では、次に first メソッドでどのようにDBにアクセスしているか見ていきます。
User.where(name: "akira").first
first の定義場所を見ていくと、 ActiveRecord::FinderMethods モジュールに定義されています。このモジュールは ActiveRecord::Relation に include されています。
def first(limit = nil) if limit find_nth_with_limit(0, limit) else find_nth 0 end end
rails/activerecord/lib/active_record/relation/finder_methods.rb at v7.0.5 · rails/rails · GitHub
ここを読み進めていくと、同モジュールの find_nth_with_limit メソッドにいきます。今回の例では、11行目の処理にいきます。
ここで to_a メソッドが呼ばれますが、実はこれは先ほど見た to_ary メソッドのエイリアスとなっています。したがって先ほどの処理が進み、SQLクエリが実行されることとなります。
def find_nth_with_limit(index, limit) if loaded? records[index, limit] || [] else relation = ordered_relation if limit_value limit = [limit_value - index, limit].min end if limit > 0 relation = relation.offset((offset_value || 0) + index) unless index.zero? relation.limit(limit).to_a else [] end end end
おわりに
Active Recordのメソッド(の一部)はすぐにDBにアクセスするわけではなく必要なタイミングでアクセスする、というのはRailsを開発に使う場合でも意識することはあると思いますが、実際に中の実装を覗いたことは無かったので、今回具体的にイメージすることが出来ました。 一度でも雑に仕組みを読んでおくと、何かあった時に再度細かいところを確認できると思えるので、ライブラリを使うという意味でも安心して使えるようになると思います。
またまたコードリーディングの殴り書きのような形になりましたが、Active Recordの内部実装が気になった時の参考になれば幸いです。
参考
ActiveRecord各メソッドのクエリ実行タイミングについて - Qiita
紅林輝(くればやしあきら)(サービス開発部) 記事一覧
サービス開発部所属。2015年にサーバーワークスにJOIN。クラウドインテグレーション部を経て、現在はCloud Automatorの開発に従事。ドラクエ部。推しナンバーはⅤ、推しモンスターはクックルー。