LoginSignup
12

More than 5 years have passed since last update.

ActiveRecord::QueryMethods#limitとActiveRecord::FinderMethods#take

Last updated at Posted at 2017-05-10

はじめに

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>]

返り値

limitActiveRecord::Relation を返しているのに対して、
takeArray を返します。

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 では、takelimitと同じように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人とれてしまうということですね。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12