Testing preloading/eager-loading của ActiveRecord associations trong Rails
Bài đăng này đã không được cập nhật trong 6 năm
Là một developer quan tâm đến hiệu suất của hệ thống thì một trong những điều bạn cần lưu ý đó là loại bỏ N+1 query bằng cách sử dụng các method #includes, #preload hoặc #eager_load. Nhưng có bao giờ bạn nghĩ là làm thế nào để bạn biết bạn đã thực hiện thành công điều đó hay chưa? Có cách nào để test chúng hay không? Sau đây là một số phương pháp:
Ví dụ bạn có 2 model Oder và OderLine như sau:
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
Bây giờ ta muốn kiểm tra xem method Oder.last_ten đã thực hiện eager loading thành công hay chưa? Lưu ý: Các ví dụ sau đều được thực hiện với Rspec.
Sử dụng association(:name).loaded?
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)
# hoặc
expect(orders[0].association(:order_lines)).to be_loaded
end
end
Bởi vì chúng ta sử dụng preload(:order_lines) nên chúng ta quan tâm xem order_lines có được load thật không. Để kiểm tra chúng ta cần 1 object Order (ở đây là orders[0]) để xác minh điều đó
Đếm số lượng query với ActiveSupport::Notifications
Đôi khi bạn muốn làm việc với một biểu đồ các object ActiveRecord nhưng kết quả bạn nhận về không phải là nó mà là một giá trị được tính toán dựa trên chúng. Vậy làm thế nào để kiểm tra xem nó có xuất hiên N+1 query ở đây? Không thể kiếm tra trên kết quả trả về, giá trị trả về không được load thêm gì. Vậy chúng ta cần làm gì?
Vẫn với ví dụ trên ta thay đổi một chút như sau:
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
Điều mà chúng ta muốn biết ở đây là Order.average_line_gross_price_today có bị N+1 query hay không? Đoạn code order.order_lines.map(&:gross_price) có phát sinh truy vấn vào DB hay ko? Có một cách đó là sử dụng ActiveSupport::Notifications và nhận thông báo về mọi câu SQL được thực thi.
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 = ->(name, started, finished, unique_id, payload) {
unless %w[ CACHE SCHEMA ].include?(payload[:name])
count += 1
end
}
ActiveSupport::Notifications.subscribed(
counter_f,
"sql.active_record",
&block
)
count
end
end
Một điều mà bạn cần lưu ý khi làm theo cách này là bạn phải tạo đủ số lượng bản ghi để có thể phát hiện được các vấn đề của eager loading. Một order với một order_line không đủ để đảm bảo có hay không có eager loading. Trong trường hợp này bạn cần ít nhất 2 order_line để có thể thấy sự khác biệt của query trước và sau khi sử dụng eager loading.
Bạn cũng có thể sử dụng gem "db-query-matchers" https://github.com/brigade/db-query-matchers để kiếm tra điều đó. Gem "db-query-matchers" cung cấp một số Rspec matcher cho phép bạn kiểm tra sự tương tác với DB. Ví dụ:
context 'when we expect no queries' do
it 'does not make database queries' do
expect { subject.make_no_queries }.to_not make_database_queries
end
end
context 'when we expect queries' do
it 'makes database queries' do
expect { subject.make_some_queries }.to make_database_queries
end
end
context 'when we expect exactly 1 query' do
it 'makes database queries' do
expect { subject.make_one_query }.to make_database_queries(count: 1)
end
end
context 'when we expect max 3 queries' do
it 'makes database queries' do
expect { subject.make_several_queries }.to make_database_queries(count: 0..3)
end
end
context 'when we expect a possible range of queries' do
it 'makes database queries' do
expect { subject.make_several_queries }.to make_database_queries(count: 3..5)
end
end
Tài liệu tham khảo
All rights reserved