Xử lí N+1 với polymorphic associations
Bài đăng này đã không được cập nhật trong 6 năm
Chắc mọi người đã quá quen thuộc với vấn đề N+1 và dùng eager loading để xử lí, nhưng hôm trước mình có gặp một trường hợp hơi rắc rối đó là xử lí eager loading polymorphic associations, google thì cũng ra được một cách, không biết có phải là hay nhất hay ko, nhưng cũng xin phép chia sẻ với mọi người.
Để ví dụ thì mình có một số model với quan hệ như sau:
class User < ApplicationRecord
has_many :orders
end
class Shop < ApplicationRecord
has_many :products
end
class Product < ApplicationRecord
belongs_to :shop
has_many :notis, as: :resource
end
class Order < ApplicationRecord
belongs_to :user
has_many :notis, as: :resource
end
class Noti < ApplicationRecord
belongs_to :resource, polymorphic: true
end
Seed một ít dữ liệu:
5.times do |n|
User.create name: "Nara-#{n+1}"
end
User.all.each do |user|
10.times do
user.orders.create
end
end
5.times do |n|
Shop.create name: "Shop-#{n+1}"
end
Shop.all.each do |shop|
10.times do
shop.products.create
end
end
Product.all.each do |product|
10.times do
product.notis.create
end
end
Order.all.each do |order|
10.times do
order.notis.create
end
end
Chắc mình không cần giải thích nhiều về những model này, vậy vấn đề là gì?
Giả sử chúng ta cần xử lí đoạn code sau:
notis = Noti.all
notis.each do |noti|
puts case noti.resource
when Product
noti.resource.shop.name
when Order
noti.resource.user.name
end
end
Có thể thấy N+1 rõ ràng, mà không những N+1, mà là 2N+1
....
Product Load (0.3ms) SELECT "products".* FROM "products" WHERE "products"."id" = ? LIMIT ? [["id", 50], ["LIMIT", 1]]
Shop Load (0.2ms) SELECT "shops".* FROM "shops" WHERE "shops"."id" = ? LIMIT ? [["id", 5], ["LIMIT", 1]]
Shop-5
Order Load (0.2ms) SELECT "orders".* FROM "orders" WHERE "orders"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]]
User Load (0.5ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]]
Nara-1
....
Xử lí một tí:
notis = Noti.includes(:resource).all
Có vẻ tốt hơn, ko còn 2N+1, chỉ còn N+1 (yaoming)
....
SELECT "shops".* FROM "shops" WHERE "shops"."id" = ? LIMIT ? [["id", 5], ["LIMIT", 1]]
Shop-5
Shop-5
SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]]
Nara-1
Nara-1
....
Giờ còn Shop với User, làm sao để eager load nó (??)
Tìm hiểu một hồi thì có người bày dùng ActiveRecord::Associations::Preloader
class Preloader
def self.noti_preload notis
preloader = ActiveRecord::Associations::Preloader.new
preloader.preload notis.select{|noti| noti.resource_type.eql?(Product.name)}, {resource: :shop}
preloader.preload notis.select{|noti| noti.resource_type.eql?(Order.name)}, {resource: :user}
return
end
end
Sử dụng nó:
notis = Noti.all
Preloader.noti_preload notis
Ngon ơ (hehe)
Noti Load (2.9ms) SELECT "notis".* FROM "notis"
Product Load (0.8ms) SELECT "products".* FROM "products" WHERE "products"."id" IN (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,.....)
Shop Load (0.9ms) SELECT "shops".* FROM "shops" WHERE "shops"."id" IN (1, 2, 3, 4, 5)
Order Load (0.8ms) SELECT "orders".* FROM "orders" WHERE "orders"."id" IN (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,......)
User Load (1.1ms) SELECT "users".* FROM "users" WHERE "users"."id" IN (1, 2, 3, 4, 5)
Tìm hiểu một vòng lại thấy người ta khuyến cáo không nên dùng thằng này, vậy mọi người có cách nào hay hơn có thể chia sẽ ko ạ (hoho)
Tham khảo: https://ksylvest.com/posts/2017-08-23/eager-loading-polymorphic-associations-with-ruby-on-rails
Mr.Nara
All rights reserved