読者です 読者をやめる 読者になる 読者になる

もた日記

くだらないことを真面目にやる

Railsメモ(20) : counter_cultureでカウント値をキャッシュする

Bulletを使用していたら下図のようなメッセージが表示された。

f:id:wonder-wall:20150816214909p:plain

どうやら原因は下記ビューの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 %>
 …

f:id:wonder-wall:20150816214919p:plain

BulletのメッセージではCounter Cacheを使えということで、これを使えば件数をあらかじめ計算してキャッシュしておくのでSELECT COUNT(*)しなくて済むようになる。
ただ、Counter Cacheを使用するとデッドロックが発生しやすかったりするらしいので、今回はcounter_cultureを試してみる。


counter_cultureの設定


github.com

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

パーフェクト Ruby on Rails

パーフェクト Ruby on Rails