如何 preload polymorphic associations
- Backend
- 10 Apr, 2021
因為想要用 includes preload polymorphic associations,但是不同 model 後面的 association 對象不一樣時不能直接用。後來在網路上找到可以用 ActiveRecord::Associations::Preloader preload 已經初始化後的 records 來達到目的。
備註:Rails 6 已經支援 preload polymorphic associations 了(Ref)
假設有這樣的 polymorphic associations:
class Picture < ApplicationRecord
belongs_to :imageable, polymorphic: true
end
class Product < ApplicationRecord
has_many :pictures, as: :imageable
end
class Employee < ApplicationRecord
has_many :pictures, as: :imageable
belongs_to :department
end
class Department < ApplicationRecord
has_many :employees
end
可以透過 picture 取得 Product 或是 Employee:
picture.imageable
如果 imageable 是 Employee 的話,還可以拿到 department:
picture.imageable.department
但如果想要一次 preload 的話,會出問題,因為 Product 不會有 Department :
Picture.includes(imageable: :department)
=> ActiveRecord::AssociationNotFoundError: Association named ‘department’ was not found on Product; perhaps you misspelled it?
使用 ActiveRecord::Associations::Preloader
後來找到一個解法,是在 records 取出來後,再根據 polymorphic 的 type,執行 preload。實際上就是用到 ActiveRecord::Associations::Preloader 可以做到這件事。
ActiveRecord::Associations::Preloader#preload 的第一個參數是 records 陣列,第二的參數是 association 名稱,使用起來像是這樣:
ActiveRecord::Associations::Preloader.new.preload(pictures, :imageable)
preload 之後,pictures 裡每個 picture 的 imageable association 就會被載入了。
使用在上面 polymorphic associations 的範例,會是長得像這樣:
pictures = Picture.all.load
preloader = ActiveRecord::Associations::Preloader.new
employee_pictures = pictures.select{ |picture| picture.imageable_type == 'Employee' }
preloader.preload(employee_pictures, imageable: :department)
# 不會 N + 1
employee_pictures.map{ |picture| picture.imageable.department.name }
Rails 6 已經支援 preload polymorphic associations 了
在 Rails 6,這樣做是沒問題的:
Picture.includes(imageable: :department)