預載主要是為了避免 N+1 問題,以下會先使用常搞混的 joins 看看,接著嘗試三種預載方式,來看他們實際產生的 Query 情況,比較之間的差異。
測試於 Rails 4.2、Rails 5.2
先產生兩個測試用的資料表
- Category - Book
+----+-------+ +----+-------------+----------+
| id | name | | id | category_id | title |
+----+-------+ +----+-------------+----------+
| 1 | child | | 1 | 1 | fish can |
| 2 | music | | 2 | 3 | etf |
| 3 | stock | +----+-------------+----------+
+----+-------+
joins 不會將 join 進來的 table 資料載入記憶體,主要用途是用來做資料的 filter
have_book_categories = Category.joins(:books)
# SELECT `categories`.*
# FROM `categories`
# INNER JOIN `books` ON `books`.`category_id` = `categories`.`id
have_book_categories.pluck(:id)
[1, 3]
preload 會拆成兩個 query,一個主 Model 的查詢跟一個 Association Model 的查詢,因為分成兩個查詢,在 ORM 中無法直接用 where 過濾 Association Model 的條件。
categories = Category.preload(:books).to_a
# SELECT `categories`.* FROM `categories`
# SELECT `books`.*
# FROM `books`
# WHERE `books`.`category_id` IN (1, 2, 3)
eager_load 會用 left join (left outer join) 的方式預載需要的表
> categories = Cateogry::eager_load(:books).to_a
# SELECT `categories`.`id` AS t0_r0,
# `categories`.`name` AS t0_r1,
# `books`.`id` AS t1_r0,
# `books`.`category_id` AS t1_r1,
# `books`.`title` AS t1_r2,
# FROM `categories`
# LEFT OUTER JOIN `books` ON `books`.`category_id` = `categories`.`id`
categories.first.books.loaded?
true
所以我們可以接著 where 對 Association Model 下條件
categories = Cateogry::eager_load(:books).where(books: { title: 'etf' }).to_a
# SELECT `categories`.`id` AS t0_r0,
# `categories`.`name` AS t0_r1,
# `books`.`id` AS t1_r0,
# `books`.`category_id` AS t1_r1,
# `books`.`title` AS t1_r2,
# FROM `categories`
# LEFT OUTER JOIN `books` ON `books`.`category_id` = `categories`.# `id`
# WHERE `books`.`title` = 'etf'
categories.first.books.first.title == 'etf'
true
includes 會依使用的情境採取不同的方式 單獨使用與 preload query 相同
Category::includes(:books).to_a
# SELECT `categories`.* FROM `categories`
# SELECT `books`.*
# FROM `books`
# WHERE `books`.`category_id` IN (1, 2, 3)
若加上 references 一同使用則與 eager_load 相同 update: 後面若使用 rails ORM 規範的查詢方式, includes 會自動採用 eager_load 的方式(參考)
Category::includes(:books).references(:books).to_a
# SELECT `categories`.`id` AS t0_r0,
# `categories`.`name` AS t0_r1,
# `books`.`id` AS t1_r0,
# `books`.`category_id` AS t1_r1,
# `books`.`title` AS t1_r2,
# FROM `categories`
# LEFT OUTER JOIN `books` ON `books`.`category_id` = `categories`.`id`
# 總結
使用 joins 用來 filter 查詢結果,可以和預載同時使用
preload 拆成兩個獨立查詢,無法對預載表做過濾 (etc. where)
eager_load 使用 left join 預載,所以同時可以下條件做過濾
includes 不加其他條件,預設會使用 preload
includes + reference 等同 eager_load
用 eager_load 做預載時,可能會因為 join 的 table 行數過多造成效能問題,這時候可以嘗試用 preload 的方式,看能不能獲得比較好的效能
# 討論
ActiveRecord 預載的資料表,若在其後對預載表使用 where 做子查詢,並不會使用已載入的 Model Collection 做搜尋,而是會再下新的 Query 到資料庫查詢,這使用只能改用 Array 的 method 例如 select 來進行條件過濾。
在傳入方與接收方不同地方的情況,若接受方已經預期傳入方會先對資料做預載,而使用 select 做條件過濾,但今天其他工程師看到接收方使用 select 而不是 where 查詢是不是會感到困惑?大家都怎麼解決這個問題的?
# references
- https://blog.arkency.com/2013/12/rails4-preloading/
- https://guides.rubyonrails.org/active_record_querying.html#specifying-conditions-on-eager-loaded-associations