Xử lí N+1 với polymorphic associations
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 ...
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