ActiveRecord count v.s. size v.s length

2016-10-17 09:47:14来源:作者:mz026's blog人点击

ActiveRecord的 association 有三個長得很像的 methods: #count , #size , #length 。 使用起來的功能差不多,但在 database 的 query 上則是有明確的不同。 如果一不小心有可能會誤中地雷發出意想不到的 query 阿!

地雷說明

我們有一個 Usermodel, 在一個 usermodel 下面會對應到多個 UserSupportMessagemodel ( has_many)。 在一個 query 裡面,我們希望同時 query 出一組 user( id屬於 user_id_pool) 和其對應的 user_support_messages的個數 ()

users = User.where(id: user_id_pool).eager(:user_support_messages)data = users.map do |user| { user: user, message_count: user.user_support_messages.count }end

如果實際把上面的程式拿去執行的話, 會發現 即使我們已經 eager load messages了,但 user.messages.count還是會發出 database query 但如果改成用 user.messages.size或者是 user.messages.length則不會。

Ruby Array methods

會有這樣的誤會,可能要從 Ruby Array#count , Array#size , Array#length談起。 基本上後兩者是完全一樣的 method (alias), 而 #count則是可以接參數做到類似 select的事情 (但是是回傳符合條件的個數而不是 subset)。

那麼在 ActiveRecord 下面的這三個 methods 呢? 我們可以來做一下下面的實驗

1) ActiveRecord_Associations_CollectionProxy#count

pry(main)> user = User.firstpry(main)> user.user_support_messages.countSELECT COUNT(*) FROM "user_support_messages" WHERE ...=> 1pry(main)> user.user_support_messagesSELECT "user_support_messages".* FROM "user_support_messages" WHERE ... ...pry(main)> user.user_support_messages.countSELECT COUNT(*) FROM "user_support_messages" WHERE ...=> 1pry(main)> user.user_support_messages.countSELECT COUNT(*) FROM "user_support_messages" WHERE ...

在這邊我們要注意的是, 重覆呼叫 count的時候,不論 association 被 load 下來了沒,一律都會發出 db query 去問 count(*)

2) ActiveRecord_Associations_CollectionProxy#size

pry(main)> user.user_support_messages.sizeSELECT COUNT(*) FROM "user_support_messages" WHERE ...=> 1pry(main)> user.user_support_messages.sizeSELECT COUNT(*) FROM "user_support_messages" WHERE ...=> 1pry(main)> user.user_support_messages.sizeSELECT COUNT(*) FROM "user_support_messages" WHERE ...=> 1pry(main)> user.user_support_messagesSELECT "user_support_messages".* FROM "user_support_messages" WHERE ... ...pry(main)> user.user_support_messages.size=> 1pry(main)> user.user_support_messages.size=> 1

在這邊我們會發現, association 被 load 下來之前,基本上 size 的行為和 count 一樣,就是每呼叫一次就會問 db 一次。但如果 association 已經被 load 下來了,那麼 size則不會再另外發出 db query

3) ActiveRecord_Associations_CollectionProxy#length

pry(main)> user.user_support_messages.lengthSELECT "user_support_messages".* FROM "user_support_messages" WHERE ...=> 1pry(main)> user.user_support_messages.length=> 1pry(main)> user.user_support_messages.length=> 1

至於 length的行為,則是 先把 association 所有的欄位都先 load 下來 (而不是只有 query count), 再去算個數。所以後續的呼叫是不會另外發出 db query 的。

實驗結果

統合上面的結果,我們可以整理出以下的表格:

method query 內容 行為 count count(*) 每一次呼叫都會 query db size(without association loaded) count(*) 每一次呼叫都會 query db size(with association loaded) - 不會 query db length select * 第一次會把 association load 下來,之後不會 query db 結論

ActiveRecord有很多設計看來是故意希望做成像一般的 array 介面一樣, 但說到底,ORM 還是拿來 query db 的,所以使用上如果真的把它當成 array 在用,可能會發生很多可怕的事情。

從程式設計的角度來說,我其實不確定這樣的設計到底是好還是不好, 理想上應該是要從 API 的介面上避免 developer 犯錯的才對。 但從實務上來說,現在 ActiveRecord 也是很難避免,所以就是自己小心一點ㄎㄎ。

註1

以這邊 demo 的 usecase, 其實如果直接下 sql query, 應該比用 count, size, length都來得更簡單

粗暴

。 但這樣的作法就又面臨到抽象層的選擇和 code readability 等等的取捨。 但總之又是另一個問題惹。

最新文章

123

最新摄影

微信扫一扫

第七城市微信公众平台