2015年9月6日日曜日

[Rails] 遅延評価とか、any?で発行されるSQLとかでハマった件

コントローラーがファットでダサいから、ヘルパー作って処理をまとめようとしたら、勉強になったのでメモ。

コントローラーでeachして色々と処理をしている処理がいくつかのコントローラにあったので、リファクタしてみることに。

まず、とあるページでヘルパーに移してみて特に問題なかったので、全ページ対応してみた。するとエラーがでるページもでてきた。

同じように対応したのに、なぜ?って思って調べてたら、遅延評価とany?で発行されるSQLが原因だった。リファクタ前のコードではコントローラーで、load済みになっていたから、any?してもSQLが発行されてなかったけど、ヘルパーに移したせいでloadされていなくてカウントのSQLが発行されるようになってしまったのだ。で、元々のSQLがGROUP BYしてて、ORDER BYで集約関数使った存在しないカラムを指定していたものだから、any?で発行されたカウントのSQLでエラーが発生した。だいたい下記のような感じ。
[1] pry(main)> Tag.select("tags.id,tags.name,count(feeds.id) as count").joins(:feeds).group("tags.id").order("count desc").limit(3)
  Tag Load (2.1ms)  SELECT  tags.id,tags.name,count(feeds.id) as count FROM "tags" INNER JOIN "feed_tags" ON "feed_tags"."tag_id" = "tags"."id" INNER JOIN "feeds" ON "feeds"."id" = "feed_tags"."feed_id" GROUP BY tags.id  ORDER BY count desc LIMIT 3
=> [#<Tag id: 11, name: "監獄">,
 #<Tag id: 24, name: "がっこう">,
 #<Tag id: 3, name: "城下町">]

[2] pry(main)> Tag.select("tags.id,tags.name,count(feeds.id) as count").joins(:feeds).group("tags.id").order("count desc").limit(3).any?
   (0.6ms)  SELECT  COUNT(*) AS count_all, tags.id AS tags_id FROM "tags" INNER JOIN "feed_tags" ON "feed_tags"."tag_id" = "tags"."id" INNER JOIN "feeds" ON "feeds"."id" = "feed_tags"."feed_id" GROUP BY tags.id  ORDER BY count desc LIMIT 3
SQLite3::SQLException: no such column: count: SELECT  COUNT(*) AS count_all, tags.id AS tags_id FROM "tags" INNER JOIN "feed_tags" ON "feed_tags"."tag_id" = "tags"."id" INNER JOIN "feeds" ON "feeds"."id" = "feed_tags"."feed_id" GROUP BY tags.id  ORDER BY count desc LIMIT 3
ActiveRecord::StatementInvalid: SQLite3::SQLException: no such column: count: SELECT  COUNT(*) AS count_all, tags.id AS tags_id FROM "tags" INNER JOIN "feed_tags" ON "feed_tags"."tag_id" = "tags"."id" INNER JOIN "feeds" ON "feeds"."id" = "feed_tags"."feed_id" GROUP BY tags.id  ORDER BY count desc LIMIT 3

で、対応。loadしなくなってSQLが発行されるようになってしまったので、一旦配列にloadすることでムダなカウントSQLを発行させなくて済むようになりました。
[3] pry(main)> Tag.select("tags.id,tags.name,count(feeds.id) as count").joins(:feeds).group("tags.id").order("count desc").limit(3).to_a.any?
  Tag Load (2.0ms)  SELECT  tags.id,tags.name,count(feeds.id) as count FROM "tags" INNER JOIN "feed_tags" ON "feed_tags"."tag_id" = "tags"."id" INNER JOIN "feeds" ON "feeds"."id" = "feed_tags"."feed_id" GROUP BY tags.id  ORDER BY count desc LIMIT 3
=> true