この記事はアピリッツの技術ブログ「DoRuby」から移行した記事です。情報が古い可能性がありますのでご注意ください。
こんにちは。KBMJの本多です。
今回はcounter_cacheの使い方を紹介します。
counter_cacheとは、RailsのActiveRecordでRDB関連の設定の1つです。
これを設定することにより、親子関係のテーブルにおいて、親テーブルが子テーブルの
件数をキャッシュすることができるようになります。
例えばいろんな商品のクチコミサイトがあるとします。
そこでは各商品毎にクチコミを複数件登録することができ、
商品一覧ページにクチコミ件数を表示したり、クチコミ件数が多い順に並び替えたりできる、という設定です。
さて、クチコミ件数が多い順に並び替える時はどんなSQLを発行しましょうか。
商品とクチコミは1対多の関係なので、各テーブルとカラムは以下の通りです。
(今回の説明に必要ないモノは省いています)
・商品(親テーブル)
モデル名:Item
テーブル名:items
カラム:id, name
・クチコミ(子テーブル)
モデル名:Comment
テーブル名:comments
カラム:id, item_id, comment
・クチコミのモデル内での商品との紐付け設定
class Comment < ActiveRecord::Base
belongs_to :item
end
この場合、発行されるSQLはこんな感じです。
select *,
(select count(*) from comments where comments.item_id=items.id ) as count
from items
order by count desc;
Rails的な書き方だと
Item.find(:all,
:select => “*,(select count(*) from comments where comments.item_id=items.id ) as count”,
:order => “count desc”)こうなります。
動けば何でも良いのであればこれで良いのですが、このやり方だとデータ量が増えた時に処理が重くなる原因になります。
対象の商品の全クチコミをカウントするので当然「index?なにそれ?」状態です。
さすがに↑のやり方は重いですが、こういう書き方もできます。
SELECT items.id, count(comments.id) as count
FROM items
left join comments on comments.item_id=items.id
group by items.id
order by count desc;
Rails的な書き方だと
Item.find(:all,
:select => “items.*, count(comments.id) as count”,
:joins => “left join comments on comments.item_id=items.id”,
:group => “items.id”,
:order => “count desc”)こうなります。
子テーブルをleft joinで引っ張ってきて、group化する事でカウント処理を行わないようにしました。
…SQLが目に見えて複雑化していますね。
先程の1件ごとのカウントよりはだいぶマシですが、indexが使えないので後々重くなると思います。
また、group byの指定は使用するDBがMySQLの時のみこの書き方が使用可能です。
postgresの場合はselectする項目(countを除く)をすべて指定する必要があります。かなり面倒です。
今回の例題はitemsテーブルのカラムがid, nameだけなので全部書けば良いですが、実際の開発ではこんなにカラムが少ないわけもなく、
開発現場から悲鳴が聞こえてくるハメになります…。
そこで今回紹介するcounter_cacheの出番です。
先程紹介したテーブルのカラム、及びモデルの設定を以下のように追加します。
・商品(親テーブル)
モデル名:Item
テーブル名:items
カラム:id, name, comments_count
・クチコミ(子テーブル)
モデル名:Comment
テーブル名:comments
カラム:id, item_id, comment
・クチコミのモデル内での商品との紐付け設定
class Comment < ActiveRecord::Base
belongs_to :item, :counter_cache => true
end
itemsテーブルに追加したcomments_countは:default => 0にする必要があります。
これだけでクチコミが増えたり減ったりした時にカウントを計測してitemsテーブルのcomments_countに保存されるようになります。
先程のSQLも以下のように変化します。
select *
from items
order by comments_count desc;
Rails的な書き方だと
Item.find(:all,
:order => “comments_count desc”)こうなります。
これならリスト取得時にクチコミ数をカウントする手間がなくなり、
またcomments_countにindexを貼ることができるため処理速度が改善されます。
是非何かの役立ててください。
それでは。
GOOD RAILS!!