Rails の strict_loading を Deep Dive したら mysql2 client の C 実装に辿り着いた

ざっくり流れとしては、strict_loading を呼び出したらインスタンス変数を設定して、呼び出し時に設定有無によって raise するか決まる

find_target はアソシエーションを引っ張ってくるときに呼ばれてそう
少し追ってみる

Deep Dive したら MySQL2::Client gem の C 実装にたどり着いた

diff --git a/activerecord/test/cases/strict_loading_test.rb b/activerecord/test/cases/strict_loading_test.rb
index e7b848e3be..52a0bab808 100644
--- a/activerecord/test/cases/strict_loading_test.rb
+++ b/activerecord/test/cases/strict_loading_test.rb
@@ -21,22 +21,22 @@ def test_strict_loading!
developer = Developer.first
assert_not_predicate developer, :strict_loading?
- assert developer.strict_loading!
- assert_predicate developer, :strict_loading?
+ # assert developer.strict_loading!
+ # assert_predicate developer, :strict_loading?
- assert_raises ActiveRecord::StrictLoadingViolationError do
- developer.audit_logs.to_a
- end
+ # assert_raises ActiveRecord::StrictLoadingViolationError do
+ # developer.audit_logs.to_a
+ # end
- assert_not developer.strict_loading!(false)
- assert_not_predicate developer, :strict_loading?
+ # assert_not developer.strict_loading!(false)
+ # assert_not_predicate developer, :strict_loading?
assert_nothing_raised do
developer.audit_logs.to_a
end
- assert developer.strict_loading!(mode: :n_plus_one_only)
- assert_predicate developer, :strict_loading_n_plus_one_only?
+ # assert developer.strict_loading!(mode: :n_plus_one_only)
+ # assert_predicate developer, :strict_loading_n_plus_one_only?
end
Relotion 辿る最小限のテストケースに修正

to_a
は ActiveRecord::Relation
で to_ary
の alias として定義されてる
⚠️ in 8-0 stable

to_ary
は内部で records
メソッドを呼び出しており、records
メソッドは内部でお馴染み load
メソッドを呼び出してる
load
メソッドは exec_queries
メソッドを呼び出して @records
インスタンス変数に詰めており、ここで SQL クエリを発行してそう

exec_queries
メソッドの exec_main_query
が実際に処理してそう
ちなみに rows 変数は attributes のハッシュが入ってくる
{"id" => 1, "name" => "David", "salary" => 80000, "firm_id" => nil, "mentor_id" => nil, "legacy_created_at" => 2025-01-26 09:26:50.74603 UTC, "legacy_updated_at" => 2025-01-26 09:26:50.74603 UTC, "legacy_created_on" => 2025-01-26 09:26:50.74603 UTC, "legacy_updated_on" => 2025-01-26 09:26:50.74603 UTC}
その後の instantiate_records
でインスタンスの attributes として set してるよう
records
変数が Model のクラスで records.first.attributes
したら rows
のハッシュと同じものが取得できる
instantiate_records
は色々あって下記の instantiate_instance_of
メソッドを呼んでる
init_with_attributes
メソッドでインスタンス変数に詰めて ActiveRecord::Core
自身を返してるみたい

klass.allocate
が気になったので調べてみたら Ruby のメソッドみたい
コンストラクタを処理せずにクラスのインスタンスだけ返す
先ほどの例だと、ユーザー定義クラスのコンストラクタを呼び出すのではなく、空のインスタンスだけを作って初期化処理は FW 側で行うために利用してそう

SQL の実行部分に戻ってみると _query_by_sql
-> select_all
-> select
-> internal_exec_query
の順で呼んでそう
select
は async
の場合の読み込みがあるので気になる

select
-> internal_exec_query
-> internal_execute
-> raw_execute
-> perform_query
の順で呼び出されてそう

perform_query
は DB クライアントごとに違うけどテスト実行時には MySQL が読み込まれていたようなので
下記は mysql2 client の 処理みたい
result = raw_connection.query(sql)

ActiveSupport::Dependencies.interlock.permit_concurrent_loads
あとで読む

あったこれだ
⚠️ 現在の最新 ver 0.5.6

_query
は C 実装

これかね

do_send_query
呼んでそう
(VALUE)rb_thread_call_without_gvl(nogvl_send_query, query_args, RUBY_UBF_IO, 0)
戻り値を cast してることはわかった
mysql_client_wrapper *wrapper = query_args->wrapper;
これは何だ

What is rb_thread_call_without_gvl
?
Ruby 側の C 実装か
GVL 解放してコールするのかな

言及を忘れていたけど、find_target
側で SQL とか生成してるんだった
records
が呼び出されるまで call されないので遅延実行されてる

ActiveRecord::Relation
のメソッドを呼び出した際に SQL クエリが発行されてる
one?
とか many?
とか

当初の Rails の strict_loading を読むところから、Deep Dive しすぎた

Summary
-
ActiveRecord::Relation
のメソッドを呼び出した際に load メソッドが走って SQL クエリが発行されてる- いわゆる遅延実行
-
ActiveRecord::Relation
のメソッドだけなのかは不明
-
Class#allocate
はコンストラクタ実行せずにインスタンスだけ作る

More Deep Dive
-
ActiveSupport::Dependencies.interlock.permit_concurrent_loads
のrunning
ロックの話 -
rb_thread_call_without_gvl
の GVL の話