Railsメモ(20) : counter_cultureでカウント値をキャッシュする
Bulletを使用していたら下図のようなメッセージが表示された。

どうやら原因は下記ビューのartist.songs.sizeとしている部分で、関連するモデルの件数を計算するためにSELECT COUNT(*)をデータの数だけ実行してしまっている。
… <% @artists.each do |artist| %> <tr> <td><%= link_to artist.name, artist_path(artist) %></td> <td><%= artist.songs.size %></td> </tr> <% end %> …

BulletのメッセージではCounter Cacheを使えということで、これを使えば件数をあらかじめ計算してキャッシュしておくのでSELECT COUNT(*)しなくて済むようになる。
ただ、Counter Cacheを使用するとデッドロックが発生しやすかったりするらしいので、今回はcounter_cultureを試してみる。
counter_cultureの設定
Gemfileに下記行を追加してbundle installする。
gem 'counter_culture', '~> 0.1.33'
次に、カウント値を保持するカラムを追加するために下記コマンドを実行する。今回は、多対多のArtistとSongのリレーションに対して、各アーティストの曲数を保持するカラムをArtistモデルに追加する。
$ rails g counter_culture Artist songs_count $ rake db:migrate
なお、自動生成されたマイグレーションファイルは以下のような内容となっている。
class AddSongsCountToArtists < ActiveRecord::Migration def self.up add_column :artists, :songs_count, :integer, :null => false, :default => 0 end def self.down remove_column :artists, :songs_count end end
続いて、app/models/song_artist.rbに記述を追加する。多対多のリレーションの場合は中間テーブルのモデルに対してこのように記述すればよいらしい。
以上で設定は完了。
class SongArtist < ActiveRecord::Base belongs_to :song belongs_to :artist counter_culture :artist, column_name: "songs_count" end
counter_cultureの使い方
既存データに対してcounter_cultureを追加した場合はカウント値がデフォルトの0になっているので手動でカウント値を更新するcounter_culture_fix_countsコマンドを実行する。これでカウント値が正しい値となり、アプリでアクセスすればSELECT COUNT(*)が実行されることなく関連するモデルの件数が表示される。
$ rails c [1] pry(main)> Artist.all.select(:id, :name, :songs_count) +------+----------------------------------+-------------+ | id | name | songs_count | +------+----------------------------------+-------------+ | 1 | 'N Sync | 0 | | 2 | 10,000 Maniacs | 0 | | 3 | 112 | 0 | … [2] pry(main)> SongArtist.counter_culture_fix_counts …計算処理… [3] pry(main)> Artist.all.select(:id, :name, :songs_count) +------+----------------------------------+-------------+ | id | name | songs_count | +------+----------------------------------+-------------+ | 1 | 'N Sync | 9 | | 2 | 10,000 Maniacs | 1 | | 3 | 112 | 9 | …
なお、Artistモデルに対してコマンドを実行するとうまくいかないので注意。
[1] pry(main)> Artist.counter_culture_fix_counts RuntimeError: No counter cache defined on Artist from /home/vagrant/.rbenv/versions/2.2.2/lib/ruby/gems/2.2.0/gems/counter_culture-0.1.33/lib/counter_culture.rb:64:in `counter_culture_fix_counts'
また、counter_cultureはデータを追加、削除したときに自動でカウント値が更新される。以下の例のようにcreateしたときにカウント値が+1され、destroyしたときにカウント値が-1される。
$ rails c
Loading development environment (Rails 4.2.3)
[1] pry(main)> Artist.find(1)
Artist Load (0.8ms) SELECT "artists".* FROM "artists" WHERE "artists"."id" = ? LIMIT 1 [["id", 1]]
+----+---------+-------------------------+-------------------------+-------------+
| id | name | created_at | updated_at | songs_count |
+----+---------+-------------------------+-------------------------+-------------+
| 1 | 'N Sync | 2015-08-16 11:46:45 UTC | 2015-08-16 11:46:45 UTC | 9 |
+----+---------+-------------------------+-------------------------+-------------+
1 row in set
[2] pry(main)> SongArtist.create song_id: 1, artist_id: 1
(0.1ms) begin transaction
SQL (0.5ms) INSERT INTO "song_artists" ("song_id", "artist_id", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["song_id", 1], ["artist_id", 1], ["created_at", "2015-08-16 11:50:11.524598"], ["updated_at", "2015-08-16 11:50:11.524598"]]
Artist Load (0.1ms) SELECT "artists".* FROM "artists" WHERE "artists"."id" = ? LIMIT 1 [["id", 1]]
(11.3ms) commit transaction
SQL (3.2ms) UPDATE "artists" SET "songs_count" = COALESCE("songs_count", 0) + 1 WHERE "artists"."id" = ? [["id", 1]]
+------+---------+-----------+-------------------------+-------------------------+
| id | song_id | artist_id | created_at | updated_at |
+------+---------+-----------+-------------------------+-------------------------+
| 3189 | 1 | 1 | 2015-08-16 11:50:11 UTC | 2015-08-16 11:50:11 UTC |
+------+---------+-----------+-------------------------+-------------------------+
1 row in set
[3] pry(main)> Artist.find(1)
Artist Load (0.2ms) SELECT "artists".* FROM "artists" WHERE "artists"."id" = ? LIMIT 1 [["id", 1]]
+----+---------+-------------------------+-------------------------+-------------+
| id | name | created_at | updated_at | songs_count |
+----+---------+-------------------------+-------------------------+-------------+
| 1 | 'N Sync | 2015-08-16 11:46:45 UTC | 2015-08-16 11:46:45 UTC | 10 |
+----+---------+-------------------------+-------------------------+-------------+
1 row in set
[4] pry(main)> SongArtist.destroy(3189)
SongArtist Load (0.2ms) SELECT "song_artists".* FROM "song_artists" WHERE "song_artists"."id" = ? LIMIT 1 [["id", 3189]]
(0.1ms) begin transaction
SQL (0.8ms) DELETE FROM "song_artists" WHERE "song_artists"."id" = ? [["id", 3189]]
Artist Load (0.1ms) SELECT "artists".* FROM "artists" WHERE "artists"."id" = ? LIMIT 1 [["id", 1]]
(3.3ms) commit transaction
SQL (3.0ms) UPDATE "artists" SET "songs_count" = COALESCE("songs_count", 0) - 1 WHERE "artists"."id" = ? [["id", 1]]
+------+---------+-----------+-------------------------+-------------------------+
| id | song_id | artist_id | created_at | updated_at |
+------+---------+-----------+-------------------------+-------------------------+
| 3189 | 1 | 1 | 2015-08-16 11:50:11 UTC | 2015-08-16 11:50:11 UTC |
+------+---------+-----------+-------------------------+-------------------------+
1 row in set
[5] pry(main)> Artist.find(1)
Artist Load (0.1ms) SELECT "artists".* FROM "artists" WHERE "artists"."id" = ? LIMIT 1 [["id", 1]]
+----+---------+-------------------------+-------------------------+-------------+
| id | name | created_at | updated_at | songs_count |
+----+---------+-------------------------+-------------------------+-------------+
| 1 | 'N Sync | 2015-08-16 11:46:45 UTC | 2015-08-16 11:46:45 UTC | 9 |
+----+---------+-------------------------+-------------------------+-------------+
1 row in set上記の例ではupdated_atの値が更新されていないが、これはcounter_cultureのデフォルトの設定なので、値を更新したい場合は以下のようにtouch: trueを追加する。
counter_culture :artist, column_name: "songs_count", touch: true

- 作者: すがわらまさのり,前島真一,近藤宇智朗,橋立友宏
- 出版社/メーカー: 技術評論社
- 発売日: 2014/06/06
- メディア: 大型本
- この商品を含むブログ (8件) を見る