Railsで集計用のカラムを自動更新する方法

Railsで集計用のカラムを自動更新する方法です。

ユーザーが商品を作成した時に、ユーザーが作成した商品数を管理したい時ありますよね?
ユーザーがユーザーテーブルにproduct_countカラムをもたせて、Railsの機能であるcounter_cacheを使うと自動更新してくれます。
また、商品のlike機能も持たせたいと思います。ユーザーが商品をlikeすると、ユーザーテーブルの持つ、like_countカラムが増えていきます。
ユーザーがloveすると、ユーザーテーブルが持つlove_countカラムが増えていきます。

counter_cache

#models/product.rb
# userテーブルにproducts_countという命名規約にのる場合はtrueだけでよい
belogs_to :user, counter_cache: true
#命名規約に基づかない場合はカラム名を指定する
belongs_to :user, counter_cache: :post_count

counter_culture × enum

class Like < ActiveRecord::Base
belongs_to :user
belongs_to :product
counter_culture :user, column_name: -> (model) { model.unlike? ? nil : "#{model.like_type}_count" }
enum like_type: [ :unlike, :like, :love]
end

上記のように設定することで、カウントするカラムを動的にすることができます。
前述の通り、ユーザーが商品をlikeすると、ユーザーテーブルの持つ、like_countカラムが増えていきます。
ユーザーがloveすると、ユーザーテーブルが持つlove_countカラムが増えていきます。
ユーザーがdislikeしたときのために、nilで回避するコードを書いています。
これがないとデータベースで「そんなカラムないよ!」と怒られてしまいます。

rspec

counter_cacheは特に何も設定しなくても、テストが通りましたが、counter_cultureは設定やコードを書かないとテストが通りません。gemのtest_after_commitをインストールしましょう。
以下はテストの例です。

require 'rails_helper'
RSpec.describe 'userテーブルのcountカラムの更新' do
let!(:user) { create(:user) }
before do
TestAfterCommit.enabled = true
end
after do
TestAfterCommit.enabled = false
end
describe 'post_count' do
before do
create_list(:product, 9, user_id: user.id)
end
context '商品の投稿' do
before do
@product = create(:product, user_id: user.id)
user.reload
end
it 'post_countが増える' do
expect(user.post_count).to eq(10)
end
context '商品の削除' do
before do
@product.destroy
user.reload
end
it 'post_countが減る' do
expect(user.post_count).to eq(9)
end
end
end
end
describe 'like_count' do
context '商品をlikeする' do
before do
@like = create(:like, user_id: user.id)
create_list(:like, 9, user_id: user.id)
user.reload
end
it 'like_countが増える' do
expect(user.like_count).to eq(10)
end
context '商品を削除する' do
before do
@like.destroy
user.reload
end
it 'like_countが減る' do
expect(user.like_count).to eq(9)
end
end
end
end
describe 'like_count' do
context '商品をlikeする' do
before do
@love = create(:like, user_id: user.id, like_type: :love)
create_list(:like, 9, user_id: user.id, like_type: :love)
user.reload
end
it 'like_countが増える' do
expect(user.love_count).to eq(10)
end
context '商品を削除する' do
before do
@love.destroy
user.reload
end
it 'like_countが減る' do
expect(user.love_count).to eq(9)
end
end
end
end
describe 'dislikeする' do
context '商品をdislikeする' do
before do
@dis_like = create(:like, user_id: user.id, like_type: :dislike)
create_list(:like, 9, user_id: user.id, like_type: :dislike)
user.reload
end
it 'like_countが増えない' do
expect(user.like_count).to eq(0)
end
it 'love_countが増えない' do
expect(user.love_count).to eq(0)
end
end
end
end

gemtest_after_commitをインストールするとデフォルトの設定が``TestAfterCommit.enabled = trueになります。
これだと他のテストに影響がでる場合があります。spec_helperファイルにTestAfterCommit.enabled = falseを記述してテスト全体を、gemをインストールする前の状態を保つようにします。
counter_cultureのテストのみTestAfterCommit.enabled = trueに設定します。
しかしtrueにするだけだと、その後のテストもtrueのままになってしまうので、afterでfalseにするのを忘れないようにしてください。

更新されるタイミング

counter_cultureはafter_commitのタイミングで発動します。likeレコードが作成されてからuserテーブルのlike_countに+1のsql,updateが発行されます。

テストデータをいれるときの注意点

動的にカウントするカラム名を指定している場合は,seedデータがうまく反映されないようです。そのさいは、user.likes.countなどのように少々無理やり値を代入してあげる必要があります。

  SQL (0.4ms)  INSERT INTO `likes` (`product_id`, `user_id`, `like_type`, `created_at`, `updated_at`) VALUES (2, 1, 2, '2016-08-24 01:14:13', '2016-08-24 01:14:13')
User Load (0.3ms)  SELECT  `users`.* FROM `users` WHERE `users`.`id` = 1 LIMIT 1
(12.4ms)  COMMIT
SQL (1.1ms)  UPDATE `users` SET `love_count` = COALESCE(`love_count`, 0) + 1 WHERE `users`.`id` = 1

ですので、データを複数ハッシュで渡しても、likeテーブルがハッシュの数だけ作成されていればcountも増えます。

なかなか便利なgemですが、設定やテストで1日ががりの作業になってしまいました。まだまだ力不足ですね。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です