こんにちは、サーバーワークス 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の開発に従事。ドラクエ部。推しナンバーはⅤ、推しモンスターはクックルー。