Rails: ActiveRecord関連付けのpreload/eager-loadをテストする2つの方法(翻訳)

概要 原著者の許諾を得て翻訳・公開いたします。 英語記事: Two ways for testing preloading/eager-loading of ActiveRecord associations in Rails 原文公開日: 著者: Robert Pankowecki サイト: Arkency — RailsやReact.jsの開発・教育・書籍を手がけています。 Rails: ActiveRecord関連付けのpreload/eager-loadをテストする2つの方法(翻訳) パフォーマンスに気を配っている開発者なら、#includes、#preload、#eager_loadなどの読み込みメソッドでN+1クエリを回避する方法をご存知でしょう。しかし自分の仕事が正しかったかどうか、期待する関連付けが本当にpreloadされているかどうかをチェックする方法はあるのでしょうか。そもそもどうやってテストすればよいのでしょうか。方法は2つあります。 Railsアプリに次の2つのクラスがあるとします。1つのorderは複数のorder line(order_lines)を持つことができます。 class Order < ActiveRecord::Base has_many :order_lines def self.last_ten limit(10).preload(:order_lines) end end class OrderLine < ActiveRecord::Base belongs_to :order end ここでOrder.last_tenというメソッドを実装しました。これはeager-loadingする関連付けを1つ使って、最新のorderを10件返します。このコードを呼び出した後でちゃんとpreloadされるかどうかを確認してみましょう。 1. association(:name).loaded? require ‘test_helper’ class OrderTest < ActiveSupport::TestCase test “#last_ten eager loading” do o = Order.new() o.order_lines.build o.order_lines.build o.save! orders = Order.last_ten assert orders[0].association(:order_lines).loaded? end end preload(:order_lines)を行ったので、order_linesが読み込まれているのかどうかを知りたいと思います。orders[0]などのOrderオブジェクトを1つ取得する必要があることをチェックするには、オブジェクトの照合を行います。ordersコレクションをチェックしても関連付けが読み込まれているかどうかはわからないため、コレクションのチェックは不要です。 RSpecでのテストは以下のような感じになります。 require ‘rails_helper’ RSpec.describe Order, type: :model do specify “#last_ten eager loading” do o = Order.new() o.order_lines.build o.order_lines.build o.save! orders = Order.last_ten expect(orders[0].association(:order_lines).loaded?).to eq(true) # 次でもよい expect(orders[0].association(:order_lines)).to be_loaded end end 2. ActiveSupport::Notificationsでクエリをカウントする ActiveRecordライブラリにはassert_queriesという便利なヘルパーメソッドがあり、ActiveRecord::TestCaseに含まれているのですが、惜しいことに、ActiveRecord::TestCaseはActiveRecordに含まれていません。これはRailsの内部テストで振舞いをチェックする目的にのみ利用できます。しかし今回の目的に合わせてassert_queriesをエミュレートするのは意外に簡単です。 いくつかのActiveRecordオブジェクトのグラフを操作するが、オブジェクトを返さずに計算値だけを返すという状況を考えてみましょう。このときにN+1問題が発生していないことをどうやって確認すればよいでしょうか。副作用は見当たらず、loaded?かどうかをチェックできるレコードも返されません。何か方法はないものでしょうか。 class Order < ActiveRecord::Base has_many :order_lines def self.average_line_gross_price_today lines = where(“created_at > ?”, Time.current.beginning_of_day). preload(:order_lines). flat_map do |order| order.order_lines.map(&:gross_price) end lines.sum / lines.size end end class OrderLine < ActiveRecord::Base belongs_to :order def gross_price # … end end 上の状況で、Order.average_line_gross_price_todayがN+1クエリ問題を抱えていないかどうかをどのように確認すればよいでしょうか。order_lines?を読み取るときにorder.order_lines.map(&:gross_price)がSQLクエリをトリガしないことをどのように確認すればよいでしょうか(実はN+1問題が起きています)。 ActiveSupport::Notificationsを使えば、SQL文が実行されるたびに通知を受け取ることができます。 require ‘rails_helper’ RSpec.describe Order, type: :model do specify “#average_line_gross_price_today eager loading” do o = Order.new() o.order_lines.build o.order_lines.build o.save! count = count_queries{ Order.average_line_gross_price_today } expect(count).to eq(2) end private def count_queries &block count = 0 counter_f = … Continue reading Rails: ActiveRecord関連付けのpreload/eager-loadをテストする2つの方法(翻訳)