2 cách để test 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
Chắc nhiều bạn đã từng dùng #includes, #preload or #eager_load
để tăng performance và tránh truy vấn N+1. Nhưng trong đó chưa chắc code đã thực hiện đúng đắn và có association preloaded như ý mong muốn hay không? làm sao để test nó? Dưới đấy có 2 cách có thể giúp test.
Hãy tưởng tượng rằng chúng ta có 2 class sau trong Rails Application đó là order
có thể có nhiều order_lines
.
class Order < ActiveRecord::Base
has_many :order_lines
class << self
def last_ten
limit(10).preload(:order_lines)
end
end
end
class OrderLine < ActiveRecord::Base
belongs_to :order
end
Chúng ta thực hiện phương thức Order.last_ten
mà nó sẽ trả về 10 order cuối cùng với một eager loaded association. Hãy xem làm sao để chắc chắn các dòng đó preload sau khi gọi nó.
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
Điểm cần chú ý đó là order_lines
được load hay không do chúng ta gọi preload(:order_lines)
. Để kiểm tra nó chúng ta cần lấy một đối tượng order
như orders[0]
để xách nhận trên nó. Ở đây không thể check với collection order
để biết được là association được load hay không.
Bây giờ test trong Rspec sẽ thực hiện như sau:
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)
# or alternatively
expect(orders[0].association(:order_lines)).to be_loaded
end
end
Đếm các câu truy vấn với ActiveSupport::Notifications
Thư viện ActiveRecord có một helper method gọi là assert_queries
là một phần của ActiveRecord::TestCase
. Thật là không may ActiveRecord::TestCase
không có sẵn là thành phần trong ActiveRecord
. Nó chỉ có sẵn trong nội bộ của rails để test xách thực hành vi của nó. Tuy nhiên chúng ta vẫn có thể bắt chước nó theo nhu cần của chúng ta.
Tưởng tượng một kịch bản trong đó bạn phải hoạt động trên một đồ thị của các đối tượng ActiveRecord
nhưng bạn không trả chúng về. Bạn chỉ trả về các giá trị đã tính toán. Vậy làm thế nào để chứng tỏ rằng trong trường hợp đó bạn không gặp vấn đề về N+1? Không có phản ứng phụ nào có thể quan sát được, không có các bản khi trả về để check xem nếu nó loaded?
Nhưng có phải thế không?
class Order < ActiveRecord::Base
has_many :order_lines
class << self
def 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
end
class OrderLine < ActiveRecord::Base
belongs_to :order
def gross_price
# ...
end
end
Trong tính huống này chúng ta làm thế nào để có thể test Order.average_line_gross_price_today
không gặp vấn đề truy vấn N+1
? Có cách nào để chắc chắn order.order_lines.map(&:gross_price)
không gọi một câu truy vấn SQL khi đọc order_lines
? Kết quả thì nó có gọi.
Chúng ta có thể sử dụng ActiveSupport::Notifications
để lấy thông báo về tất cả SQL statement được thi hành.
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
Nếu bạn dùng cách trên chắc chắn để tạo đủ các bản ghi để phát hiện các vấn đề tiềm ẩn với eager loading. Với một order
một dòng là không đủ mặc dù có hay không có eager loading số lượng cầu truy vấn vẫn như nhau. Trong trường hợp này bạn có 2 dòng order
bạn có thể thấy sự khác biết về số cầu truy vấn với preloading(2 một cho tất cả các orders
và một nữa cho tất cả lines
) ngược lại nếu không có preloading(3, một cho tất cả các orders
và một cho mỗi một dòng tách biệt). Phải chắc chắn là test có kết quả là fail trước khi fix nó.
Trong khi sử dụng cách tiếp cận này là có khả năng báo cho chúng ta rằng là cách tốt nhất thì tách các trách nhiệm thành 2 methods nhỏ. Một phụ trách để trích ra các bản ghi đúng từ database(IO - related) và một nữa để biến đổi dữ liệu và tính toán(no IO, side-effect free). Bạn có thể tham khảo db-query-matchers gem cho Rspec matcher giúp bạn với kiểu test trên.
Tham khảo
All rights reserved