こんにちは!kossyです!
今回は、テーブルのレコードの合計値をキャッシュできるGemであるcounter_cultureを試してみたので、
備忘録としてブログに残してみたいと思います。
counter_cultureのユースケース
例えば、一つの支社に何人の従業員が所属しているか表示したいとします。
class Company has_many :employees, dependent: :destroy end class Employee belongs_to :company end
この場合、何人所属しているかを取得するには、以下のようなActiveRecord Queryを走らせる必要があるかと思います。
$ company = Company.last Company Load (4.7ms) SELECT "companys".* FROM "companys" ORDER BY "companys"."id" DESC LIMIT $1 [["LIMIT", 1]] $ company.employees.count (9.8ms) SELECT COUNT(*) FROM "employees" WHERE "employees"."company_id" = $1 [["company_id", 32]] => 4
これはいわゆる、「会社情報詳細画面」向けのデータ取得方法になるかと思うので、クエリが2回で済んでいますが、これが「会社情報一覧画面」になるとどうでしょう。
$ Company.all.map { |company| company.employees.count } Company Load (5.2ms) SELECT "companys".* FROM "companys" (6.0ms) SELECT COUNT(*) FROM "employees" WHERE "employees"."company_id" = $1 [["company_id", 1]] (4.6ms) SELECT COUNT(*) FROM "employees" WHERE "employees"."company_id" = $1 [["company_id", 2]] (4.4ms) SELECT COUNT(*) FROM "employees" WHERE "employees"."company_id" = $1 [["company_id", 3]] (5.3ms) SELECT COUNT(*) FROM "employees" WHERE "employees"."company_id" = $1 [["company_id", 4]] (3.6ms) SELECT COUNT(*) FROM "employees" WHERE "employees"."company_id" = $1 [["company_id", 5]] (3.1ms) SELECT COUNT(*) FROM "employees" WHERE "employees"."company_id" = $1 [["company_id", 6]] (4.2ms) SELECT COUNT(*) FROM "employees" WHERE "employees"."company_id" = $1 [["company_id", 7]] (3.4ms) SELECT COUNT(*) FROM "employees" WHERE "employees"."company_id" = $1 [["company_id", 8]] => [4, 4, 4, 4, 4, 4, 4, 4]
例とコードが極端ではありますが、要は会社一つ一つに対して、countのSQLを走らせないといけないということが言いたかったのです、、、
このようなケースで、合計値(あるいは平均値)をキャッシュ(カラムに値を保存)する方法を提供してくれるのが、counter_coltureの役割になります。
使い方
テーブル構成は以前本ブログで紹介した構成を一部修正して試してみます。
kossy-web-engineer.hatenablog.com
class Department < ApplicationRecord belongs_to :company has_many :employees end class Employee < ApplicationRecord belongs_to :company belongs_to :department enum sex: { male: false, female: true } end
まずはcounter_cultureをインストールします。
# Gemfile gem 'counter_culture'
$ bundle
次に、Employeeモデルにcounter_cultureを有効化するためのメソッドを追加します。
# app/models/employee.rb class Employee < ApplicationRecord belongs_to :company belongs_to :department counter_culture :department # 追加 end
次に、合計値を保存するためのカラムをdepartmentsテーブルに追加します。
$ rails g counter_culture Depaertment employees_count
これで、以下のようなマイグレーションファイルが生成されます。
class AddEmployeesCountToDepaertments < ActiveRecord::Migration[6.0] def self.up add_column :depaertments, :employees_count, :integer, null: false, default: 0 end def self.down remove_column :depaertments, :employees_count end end
注意事項として、仮にGeneratorを使わずに手動でマイグレーションファイルを作成する場合は、
必ずinteger型でNotNull制約を付与し、default値は0で設定するようにします。
これで準備完了です。
動作確認
$ Employee.new(department: Department.first, company: Company.first).save! (0.5ms) BEGIN Company Load (4.6ms) SELECT "companies".* FROM "companies" WHERE "companies"."id" = $1 LIMIT $2 [["id", 1], ["LIMIT", 1]] Department Load (5.9ms) SELECT "departments".* FROM "departments" WHERE "departments"."id" = $1 LIMIT $2 [["id", 32], ["LIMIT", 1]] Employee Create (10.4ms) INSERT INTO "employees" ("id", "first_name", "last_name", "first_name_kana", "last_name_kana", "sex", "hired_at", "company_id", "position_id", "department_id", "created_at", "updated_at") VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) RETURNING "id" [["id", 105], ["first_name", "六海"], ["last_name", "上野"], ["first_name_kana", "ムツミ"], ["last_name_kana", "ウエノ"], ["sex", true], ["hired_at", "2006-11-20"], ["company_id", 1], ["department_id", 32], ["created_at", "2021-06-26 03:55:04.380598"], ["updated_at", "2021-06-26 03:55:04.380598"]] Department Update All (2.7ms) UPDATE "departments" SET "employees_count" = COALESCE("employees_count", 0) + 1 WHERE "departments"."id" = $1 [["id", 32]] (2.7ms) COMMIT => true
注目すべきは、Department Update All (2.7ms) UPDATE "departments" SET "employees_count" = COALESCE("employees_count", 0) + 1 の部分でしょう。
save!したと同時に、employeesのINSERT文とdepartmentsテーブルの該当レコードに対してupdate文が実行されています。
COALESCEは以下のブログでも紹介していますが、「いくつかの値の中で最初にNULLでない値」を返すSQL関数です。
kossy-web-engineer.hatenablog.com
Departmentレコードのemployees_countが1増えているかどうか検証してみます。
# 実行前 $ Department.find(32) Department Load (6.7ms) SELECT "departments".* FROM "departments" WHERE "departments"."id" = $1 LIMIT $2 [["id", 32], ["LIMIT", 1]] => #<Department:0x00005634273a8570 id: 32, name: "沖縄支店", ancestry: "8", ancestry_depth: 1, employees_count: 4, company_id: 1, created_at: Sat, 26 Jun 2021 03:54:50 UTC +00:00, updated_at: Sat, 26 Jun 2021 03:54:50 UTC +00:00> # 実行後 $ Department.find(32) Department Load (6.7ms) SELECT "departments".* FROM "departments" WHERE "departments"."id" = $1 LIMIT $2 [["id", 32], ["LIMIT", 1]] => #<Department:0x00005634273a8570 id: 32, name: "沖縄支店", ancestry: "8", ancestry_depth: 1, employees_count: 5, # 1増えている company_id: 1, created_at: Sat, 26 Jun 2021 03:54:50 UTC +00:00, updated_at: Sat, 26 Jun 2021 03:54:50 UTC +00:00>
きちんと1増えていることが確認できました。
まとめ
ごくごくシンプルに扱うのであれば、上記で十分かなと思いますが、counter_cultureはまだまだ便利なオプションを数多く用意しているので、
一度本家のReadMeに目を通されることをお勧めします。
勉強になりました。