Нюансы использования rails-консоли
Sergei Malykh
Posted on September 4, 2023
Все мы постоянно пользуемся rails-консолью. Она незаменима при отладке в development. Но и в боевом окружении временами тоже что-то требуется посмотреть. Недавно наткнулся на один неочевидный нюанс, который заставил поразбираться.
Есть некий код, который использует ActiveRecord::Associations::Preloader
для кастомной подгрузки ассоциации (выборочный select
). Отлаживаю его в консоли:
products_scope = Product.select(:id, :name)
ActiveRecord::Associations::Preloader.new(records: line_items, associations: :product, scope: products_scope).call
На проде можно предварительно для удобства перенаправить в консоль SQL-запросы:
ActiveRecord::Base.logger = Logger.new(STDOUT)
Что мы видим, когда запускаем этот код?
DEBUG -- : Product Load (3.8ms) SELECT "products"."id", "products"."name" FROM "products" WHERE "products"."id" = $1 [["id", 1092717]]
DEBUG -- : Product Load (1326.4ms) SELECT "products"."id", "products"."name" FROM "products"
Товары подгружаются дважды. Первый раз, как и ожидается с фильтрацией по id товара. А вот второго быть вообще не должно, к тому же запускается full scan таблицы. И это production-окружении!. Получается AR-preloader не работает? почему так происходит?
Покопался в исходниках и понял, что прелоадер все-таки ни при чем. Проблема возникает при отображении результата выполнения команды в консоли. При этом под капотом дергается некий метод, который форматирует вывод. Какой именно, не понятно. Теоретически, это может быть метод этого самого результата, либо другого объекта с аргументом в виде результата. Попробуем выяснить, как именно.
Вызываемые методы могут быть залогированы при помощи модуля TracePoint. Он позволяет отлавливать разные типы событий, в том числе события call
, вызываемые на объекте типа Method. То есть, таким образом мы можем залогировать вызовы любых методов.
result = ActiveRecord::Associations::Preloader.new(records: line_items, associations: :product, scope: products_scope).call; 1
calls = []
trace = TracePoint.new(:call, :c_call) do |tp|
if tp.binding.eval('self') == result || tp.parameters.any? { tp.binding.eval(_1.last) == result rescue false }
calls << "#{tp.defined_class}##{tp.method_id} got called (#{tp.path}:#{tp.lineno})"
end
end
trace.enable
result
Результат, вывод которого хотим залогировать, присваиваем переменной, и гасим вывод команды присваивания. Далее отлавливаем вызовы методов, в которых либо аргументом является result
либо принадлежащие самому result
. Включаем логирование, инициируем вывод результата в консоль.
Далее нужно отключить логирование и посмотреть, что накопилось в calls
:
trace.disable
calls
=> ["Array#pretty_print got called (~/.rbenv/versions/3.1.2/lib/ruby/3.1.0/pp.rb:366)"]
Уже теплее, result
содержит массив и для вывода в консоль используется один из методов модуля PP
. Очевидно, что для отображения на каждом элементе массива вызывается еще какой-то метод. Изменим немного код:
result = ActiveRecord::Associations::Preloader.new(records: line_items, associations: :product, scope: products_scope).call[0]; 1
trace.enable
result
...
trace.disable
calls
=> ["PP::ObjectMixin#pretty_print got called (~/.rbenv/versions/3.1.2/lib/ruby/3.1.0/pp.rb:317)",
"Module#instance_method got called (~/.rbenv/versions/3.1.2/lib/ruby/3.1.0/pp.rb:318)",
"UnboundMethod#bind_call got called (~/.rbenv/versions/3.1.2/lib/ruby/3.1.0/pp.rb:320)",
"Kernel#method got called (~/.rbenv/versions/3.1.2/lib/ruby/3.1.0/pp.rb:320)",
"Method#owner got called (~/.rbenv/versions/3.1.2/lib/ruby/3.1.0/pp.rb:323)",
"BasicObject#!= got called (~/.rbenv/versions/3.1.2/lib/ruby/3.1.0/pp.rb:323)",
"Module#== got called (~/.rbenv/versions/3.1.2/lib/ruby/3.1.0/pp.rb:323)",
"BasicObject#! got called (~/.rbenv/versions/3.1.2/lib/ruby/3.1.0/pp.rb:325)",
"PP::ObjectMixin#pretty_print_instance_variables got called (~/.rbenv/versions/3.1.2/lib/ruby/3.1.0/pp.rb:345)",
"Kernel#instance_variables got called (~/.rbenv/versions/3.1.2/lib/ruby/3.1.0/pp.rb:346)",
"Array#sort got called (~/.rbenv/versions/3.1.2/lib/ruby/3.1.0/pp.rb:346)",
"Symbol#<=> got called (~/.rbenv/versions/3.1.2/lib/ruby/3.1.0/pp.rb:346)"]
Немного покопавшись в исходниках PP
, не сложно найти, что для вывода объекта все его instance-переменные выбираются и отображаются при помощи pp
:
pp(obj.instance_eval(v))
Объект ActiveRecord::Associations::Preloader
содержит instance-переменную @preload_scope
, в которой сохранен products_scope
. При отображении с помощью pp
происходит выполенение запроса к базе, что и приводит к этой неочевидной ситуации.
pp Product.select(:id, :name)
D, [2023-09-04T15:55:01.574901 #28800] DEBUG -- : Product Load (1185.7ms) SELECT "products"."id", "products"."name" FROM "products"
Posted on September 4, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 30, 2024