2013年2月4日月曜日

view table を使って 1+N 問題回避

今回はtipsの紹介的な話。

検索結果で表示する内容が簡単な association だけでは取ってこれないような場合の解決方法について考える。

例えば、ユーザの検索結果にユーザの投稿数を表示する、という場合を考える。rails にはcounter cache columnという仕組みがあるためこの例に限っては別の解決法があるが、今回はそれを使わないことにする。

通常これを実装するためのコードは
@user = User.all

- @users.each do |u|
  %tr
     = u.posts.count

のようになるだろう。ここで明らかな問題は 1-N 問題が発生すること。posts.count を通るたびに

select count(*) from posts where user_id = 1

のような sql が実行される。u.posts.any?{|p| p.comments.present? } みたいなのだとさらに増える。

一つの解決法は当然ながら includes を使うことだ。
@user = User.includes( :posts => :comments )
としておけば 1-N 問題が解決される。しかしこれは数を数えるという目的のために comment オブジェクトを大量に作るという点で良くない。rails では残念なことに ActiveRecord の生成は大きなパフォーマンスの低下を招く。

別の方法としては、別途 sql で count した結果を inject してやるというものがある。
def inject_count_comments( posts )
  sql = "select post_id, count(*) from comments where post_id in (?) "
  restult = Comment.connection.execute [sql, posts.map(&:id)]
  posts_by_id = posts.index_by(&:id)
  result.each do |row|
     post = posts_by_id[""]
  end
end

これを毎回書くのは面倒なので、与えられた sql を実行して、model にセットするという汎用のメソッドは作ればすっきりする。model にセットするときの属性名は sql のカラム名をそのまま取るようにすればよい。

呼び出し側としては
posts = Post.some_condition.all
Post.inject_count_commitments( posts)
という二段構成になる点が残念。

さらに別な解決方法として、mysql の view を作るという方法もある。やや大げさすぎる感じもするので、これに値するほど複雑な sql のみ使っている。

最初の例の場合でいうと、以下のような view を作る。

create view posts_counts as
   select user_id, count(*) as count from posts group by user_id;

そして、この view を user の子モデルとして定義する。

class PostsCount < ActiveRecord::Base
end

class User < ActiveRecord::Base
  has_one :posts_count
end

@users = User.includes(:posts_count).all

ただこのやりかたは、Post がコメントをいくつ持っているか?という、明らかに Post の属性の一つであるものが独立したモデルとなってしまっているのがあまりよろしくなく、多様すると app/models や mysql の show tables が混雑してくるので限られた場合のみ使うほうが良いとは思う。コード上はすっきりするが。




0 件のコメント:

コメントを投稿