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件) を見る