職場に浅草観光で買った食品サンプルを持ってきて、何も言われないのにホッとしながらも残念にも思っていたら、暫くして上長に初めて聞かれて6800円の値段に驚かれたHelloWorld!?です。
前回の記事で、ActiveRecord::Relationに関して書きましたが(ActiveRecord::Relationの落とし穴)、上辺しか見る事が出来ていなかった、原因はもっと別の場所にあった、という事が、先輩社員や同期からの指摘で分かりました。それから自分なりに色々調べた結果、理解が深められたので、それに関して書こうと思います。
本題:ActiveRecord whereメソッド
まず、焦点となったActiveRecord::Relation、を返すwhereメソッドに関して、その周辺も含めるソースコードを見てみました。
rails/activerecord/lib/active_record/relation/query_methods.rb
def where(opts = :chain, *rest) if :chain == opts WhereChain.new(spawn) elsif opts.blank? self else spawn.where!(opts, *rest) end end def where!(opts, *rest) # :nodoc: opts = sanitize_forbidden_attributes(opts) references!(PredicateBuilder.references(opts)) if Hash === opts self.where_clause += where_clause_factory.build(opts, rest) self end
......これだけ見ても、何してるのか全く分からないです。少なくとも、ここ辺りのコードを見た事が無い自分にとっては。
とりあえず、このコードを色々調べてみて、自分なりの擬似コードにしてみました。
def where(条件、プレースホルダの値) if 引数が無かったら WhereChainの新しいインスタンスを返す #WhereChain : 条件がなかった時に、プレースホルダとして働くもの elsif 条件が空だったら 自身を返す else where!(条件、プレースホルダの値)を呼ぶ end end def where!(条件、プレースホルダの値) 条件の属性を確かめる 条件がハッシュで構成されたものならば、文字列へ加工を施す 条件を付け加える end
という感じでした。
そしてその加えられた条件は、最終的に値を取り出そうとする時にbound_attributesが呼ばれることによってSQLに代入され、レコードが取り出される、という過程を踏みます。
自分が前回の記事で間違えた点は、その記事にも書き加えましたが、whereを呼んだだけでは条件が追加されるだけで、ActiveRecord::Relationに実際に値は入らないという事です。
なので、それで最初から要素を入れたい時は、where(...)と呼ぶのではなくその後にloadなり適当な値を取り出すメソッドなりを呼んであげる必要がある、という事でした(where(...).loadとか)。loadをしなくとも、最終的な計測時間は変わらない訳なのですが、そこで自分は勘違いして、変な計測結果を出していたりした訳です。
後、計測時間を、Load有りと無しでまた測ってみました。(railsコンソール上で計測)
横軸:レコード数
縦軸:経過時間(秒)
ロード有りだと、ほぼほぼ時間は変わらない結果になりました。
ただ、無しだと結構差が出ているので、ロードしてから何度も走査したりする必要があるのならば、ハッシュや配列の形式にして出力した方が良いかもしれません。
そして、そこからもう一つ、疑問が浮かびました。
前回の記事では、hashなどに形式を変換したら、劇的に動作速度が向上した、と書きました。実際その通りだったのですが、上記の結果を鑑みると、Load有りでは、ほぼほぼ時間は変わりませんし、Load無しにしたとしても、よほど走査を繰り返さない限り、目に見える程の差は出ないはずです。
なら、何故......?
と思ってその遅いコードを眺めてみようと思ったら、commitしていなかったのでもうありませんでした。
......。
でも推測として、多分SQLの呼び出しを一回のループの中で複数やるようなコードを書いてしまっていたのではないか、という事が理由なのではないか、と思っています。それ以外で余り劇的な動作速度の向上が起こるような原因も思い当たりませんし。また、データを色々こねくりまわしていた所もありましたので。(またcommitに対しても、出来たコードがおかしなコードでも、キリのいいところならばコミットするようにします。過去の自分がどのように実装したか、また、他人が見た時にもcommitが小まめにされていれば、そのコーディングの軌跡が分かり易いですし。)
結論
whereを呼んで新しく作成されるものは、SQLの条件であり、データ本体では無い。SQLを呼んでデータをロードする為には、何かしらこちらから手を加えないといけない(それに関しては、こちらの記事に詳しく書いてあります:ActiveRecord::Relationとは一体なんなのか)。
特に、ボトルネックを調べる場合など、それが原因で混乱する可能性がある。