地方でリモートワーク

プログラミング、先物、fx,仮想通貨なんでもやります

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