使用 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