はじめに
RailsのActiveRecordのメソッド QueryMethods#limit
をあまり良く知らずに使っていたら、あれ?という挙動がでたので、それとRuby標準のメソッド ActiveRecord::FinderMethods#take
の使い分けについて考えました。
Rails ~> 5.1.0
limit
はQueryMethod
Webプログラミングの入門としてRailsから入った僕は、limitメソッド
の挙動だけをみると、Array#take
とそんなに変わらないじゃんと思う事がある。
が、limit
はそのクラスの名前通り、SQLのメソッドのlimitを実装したメソッドであることを意識するのは大事なことである。
年齢が12歳のユーザーを3人所得する時の例をみてみる。
twelves = User.where(age: 12)
# ActiveRecord::QueryMethods#limitを使用する
twelves.limit(3)
# User Load (4.9ms) SELECT `users`.* FROM `users` WHERE `users`.`age` = '12' LIMIT 3
#=> <#ActiveRecord::Relation [<#User id: 1, name: "ゴン=フリークス", age: 12>, <#User id: 2, name: "キルア=ゾルディック", age: 12>, <#User id: 3, name: "ピーター・パン", age: 12>]>
# ActiveRecord::FinderMethods#takeを使用する
twelves.take(3)
#=> [<#User id: 1, name: "ゴン=フリークス", age: 12>, <#User id: 2, name: "キルア=ゾルディック", age: 12>, <#User id: 3, name: "ピーター・パン", age: 12>]
返り値
limit
は ActiveRecord::Relation
を返しているのに対して、
take
は Array
を返します。
twelves.limit(3).class
# => User::ActiveRecord_Relation
twelves.take(3).class
# => Array
つまり、なんでもかんでも take
をしたあとは、ActiveRecord::Relation
の便利なメソッドたちが使えなくなります。
なので、take
は色々複雑な処理が終えてから最後に数を合わせたい時にするのがいいということでしょうか。少なくとも、複雑な処理の前でtake
メソッドはしようするものではないですね。
SQL文
take
は一度、DBから読み込んだレコードに対してはSQL文を発行しません。一番はじめのコードをみればわかるように、limit
は一度読み込まれたものに対しても、SQLを発行して再度DBからレコードを所得します。
この違いは、Rails 5.10.1
からのものだと思われます。Rails 5.0.2
では、take
もlimit
と同じようにSQLを発行し、再度DBから読み込みます。
では、下記のようなコードはどのようなSQL文が発行されるのでしょうか。
User.where(age: 12).take(3)
一見すると、
一度、年齢が12歳のUserを全て所得しする ー> その中から3つを選ぶ。
というコードに見えますが、実際のところは limit
を使い一度に
年齢が12歳のUserを3つ所得する
という内容のSQL文を発行しています。つまり
User Load (4.9ms) SELECT `users`.* FROM `users` WHERE `users`.`age` = '12'
ではなく
User Load (4.9ms) SELECT `users`.* FROM `users` WHERE `users`.`age` = '12' LIMIT 3
というSQL文を発行します。
この違いだけを見ると、SQL文を発行する必要が無いときは発行しなくて、SQL文に limit
を入れたほうが効率のいい時は、limit
をいれてくれる take
使っとけばいいじゃんとなりますが、返り値が配列であることを考えないといけません。
limit
再びSQL文を発行するということ
僕が、あれ?と思った動作です。
twelves = User.where(age: 12).limit(3)
twelves = twelves.limit(5)
さて、これは何人のレコードがとれるでしょうか。一度にlimit
をこのような形で続けて書く人はいないでしょうが、一度3つユーザーをとり、また別のメソッドでユーザーが5人または5人以下であることを確認したい時などにこのように書いてしまう可能性があります。
予測としては、3人の中から5人を取ろうとするのは無理なので、3人のユーザーを返してくれると期待してました。
ですが、上記でも書いたように limit
メソッドはSQL文を再発行します。つまり、
twelves = User.where(age: 12).limit(3)
で下記のような、SQL文とともに年齢が12歳のユーザーを3人集めてくれます。
User Load (4.9ms) SELECT `users`.* FROM `users` WHERE `users`.`age` = '12' LIMIT 3
ですが
twelves = twelves.limit(5)
このコードでは、一度下記のようなSQL文を再発行してしまいます。
User Load (4.9ms) SELECT `users`.* FROM `users` WHERE `users`.`age` = '12' LIMIT 5
つまり、ユーザーが5人とれてしまうということですね。