Nic Lin's Blog

喜歡在地上打滾的 Rails Developer

淺談 Active Record 的 Lazy load 特性

使用 User.find(1) 會直接返回一個明確的 ruby object,但更多時候下 where 查詢條件時返回的像是一個 serialized 過後的 User object

[#<User id: 1, email: "foo@bar.com">]

這個回傳的東西看似 array,但如果實際訪問他的 class 其實是 ActiveRecord::Relation

User.where(id: 1).class

# ActiveRecord::Relation

Active Record 回傳 relation 時其實是 lazy load,因為這樣一來可以保留其彈性及可擴充性,能夠在 hits database 之前組合出更複雜的 query。

lazy load 的意思中文可能會翻惰性加載之類的,意思大概就是不到最後一刻不會去真正的執行 SQL 語句,這樣一來可以節省消耗 database 的資源。

舉例來說,你可能會在 controller 裡面寫到

@posts = Post.limit(5)

那其實你傳給 view 的是一個 relation。

也就是說,直到 view 裡面真正調用到第一個 @posts.first.title 時才會真正運行 SQL 語句,並且將該結果存到 memory 之中。

有趣的是,你在 rails console 之中可能沒辦法直接測出 lazy load,因為 query 在 console 基本上會直接執行,原因是 Rails console 會自動 call .inspect 並把結果輸出。

不過你可以試試這招

# ---non lazy load---
user = User.find(1); nil
#   User Load (1.4ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2  [["id", 1], ["LIMIT", 1]]
# nil
# 這裡可以發現,直接 hits database


# ---lazy load---
lazy_user = User.where('id = 1'); nil
# nil
# 完全沒有 hits database, 因為有 lazy load 特性
# 接著繼續執行

lazy_user.first.email
#   User Load (2.3ms)  SELECT  "users".* FROM "users" WHERE (id = 1) ORDER BY "users"."id" ASC LIMIT $1  [["LIMIT", 1]]
# 這時候會發現,已經執行 SQL 語句了,證明了 lazy load

所以我們可以因為 lazy load 特性,避免資源浪費,例如透過 request 決定要搜尋哪一種狀態的訂單

def index
  @orders = current_user.orders

  @orders = case params[:type]
            when "pending"
              @orders.pending
            when "success"
              @orders.success
            else
              @orders
            end

  @orders = @orders.limit(5)
end

在上述範例,最後拿到的 @orders 會是一個 relation,直到 view 裡面執行了 @orders.first.price 才會真正執行組合好的 SQL query

不過要注意的是,如果 relation 中有接 ActiveRecord::FinderMethods 的 methods 是會直接進行查詢的

  • #find
  • #find_by
  • #first
  • #last

這些 method 會回傳單一的 model instance,如果用 #take 則是回傳 array,然而這些直接回傳 instance 的並不會有 lazy load 特性,都是直接當下就進行查詢。

小結

lazy load 非常適合打組合技來組合出複雜的 SQL query,例如: association + scope + class method

沒有 lazy load 的部分是因為你組出來的 query 有 call 到其他 method 導致直接執行 SQL 語句而不是回傳 ActiveRecord::Relation

參考資源

comments powered by Disqus