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日ががりの作業になってしまいました。まだまだ力不足ですね。
コメントを残す