株式会社スペースリー エンジニアの出口です。
普段は弊社サービスの建物や物件といった情報の管理画面を開発しています。
バックエンドはruby on railsで開発されており、今回は建物一覧のレイテンシ改善のためActiveRecordのcounter_cacheを導入した経緯と実装内容についてまとめます。
概要
サービスに登録されているアパートやマンションといった建物の一覧を表示するページでは、建物の情報に加えて建物に紐づいている物件(個々の部屋)の数や撮影された画像の数などを表示しています。

各モデルのリレーションは下図の通りです。建物と物件が親子関係にあり、建物と物件それぞれに属する画像があるというポリモーフィックの関連付けがされています。

建物一覧では建物の情報の他に紐づく物件数や画像数、画像を持っている物件数などを表示します。しかし、建物一覧のデータ取得のたびに物件数などの値を集計しているためレイテンシが悪化していました。この問題をcounter_cacheを導入することでレイテンシを改善しました。
counter_cacheを選んだ理由
レイテンシ改善のためにはデータ取得の際に集計を行わず、建物のテーブルに集計結果のカラムを作成して値を保持することにしました。 物件などのレコードの作成・削除に合わせて集計結果のカラムの値を更新するにはcounter_cacheかcounter_cultureを利用することで実現できます。 建物一覧の仕様を踏まえた上でのそれぞれの特徴は以下の通りです。
counter_cache
- 条件付きの集計ができない
- ポリモーフィック対応
counter_culture
- 条件付きの集計ができる
- ポリモーフィック非対応
条件付きの集計はありますが、画像は建物と物件のポリモーフィックになっているため、今回の実装ではcounter_cacheを導入することにしました。
実装内容
実装としては下記の対応をしました。
- 集計カラム追加
- モデルファイルの修正
- 既存データの集計
- 条件付き集計カラムの更新
集計カラム追加
counter_cacheを用いて値を保存するためのカラムをRealEstateとBuildingにそれぞれ追加します。
class AddImageableCount < ActiveRecord::Migration[6.1]
def self.up
add_column :real_estates, :real_estate_images_count, :integer, null: false, default: 0, comment: "物件の画像枚数"
add_column :buildings, :real_estates_count, :integer, null: false, default: 0, comment: "物件数"
add_column :buildings, :has_real_estate_image_real_estates_count, :integer, null: false, default: 0, comment: "画像あり物件数"
add_column :buildings, :real_estate_images_count, :integer, null: false, default: 0, comment: "建物の画像枚数"
end
def self.down
remove_column :real_estates, :real_estate_images_count
remove_column :buildings, :real_estates_count
remove_column :buildings, :has_real_estate_image_real_estates_count
remove_column :buildings, :real_estate_images_count
end
end
モデルファイルの修正
集計対象のモデルファイルのbelongs_toにcounter_cacheの記述を追加します。 ポリモーフィックを用いている画像のモデルファイルではcounter_cacheでカラム名を指定します。 これにより物件や画像が作成・削除された場合には親テーブルの集計用カラムの値は増加、減少するようになります。ただし、画像を持っている物件数というような条件付き集計カラムは集計されないため別途対応します。
建物
class Building < ApplicationRecord
has_many :real_estates
has_many :real_estate_images, as: :imageable
# 関係ない項目は省略
end
物件
class RealEstate < ApplicationRecord
belongs_to :building, optional: true, counter_cache: true
has_many :real_estate_images, as: :imageable
# 関係ない項目は省略
end
画像
class RealEstateImage < ApplicationRecord
belongs_to :imageable, polymorphic: true, counter_cache: :real_estate_images_count
# 関係ない項目は省略
end
既存データの集計
counter_cacheを新たに導入した場合、集計対象のレコードが既に存在すると集計し直す必要があります。 これはcounter_cacheが集計対象のレコードの増減の際に集計用のカラムを1ずつ増減させているためです。 再集計はreset_countersのメソッドを利用することで対応できます。 条件付きの集計の場合には集計した値でカラムを更新します。
namespace :oneshot do
task reset_count: :environment do
RealEstate.find_each do |real_estate|
# 画像数
RealEstate.reset_counters(real_estate.id, :real_estate_images)
end
Building.find_each do |building|
# 物件数
Building.reset_counters(building.id, :real_estates)
# 画像数
Building.reset_counters(building.id, :real_estate_images)
# 画像有り物件数
has_real_estate_image_real_estates_count = building.real_estates.joins(:real_estate_images)
.group(:building_id)
.distinct
.count[building.id].to_i
building.update!(has_real_estate_image_real_estates_count: has_real_estate_image_real_estates_count)
end
end
end
条件付き集計カラムの更新
counter_cacheの対象としているレコード(物件または画像)が作成されるとcounter_cacheのupdate_countersが実行され、各カラムのカウントが加算されます。 今回はupdate_countersをオーバーライドすることで条件付き集計カラムの更新を実装しました。 しかし、物件が削除された場合や物件の画像が削除された場合にはbelogs_to.rb内のprivateメソッドであるadd_counter_cache_callbacksを使用するためオーバーライドはできず、別途処理を追加して条件付き集計カラムを更新するようにしました。
class Building < ApplicationRecord
# 関係ない項目は省略
# counter_cacheのupdate_countersをオーバーライド
def self.update_counters(id, counters)
super(id, counters)
building = find(id)
building.update_counts
end
def update_counts
self.has_real_estate_image_real_estates_count = calc_has_real_estate_image_real_estates_count
save
end
def calc_has_real_estate_image_real_estates_count
real_estates.joins(:real_estate_images)
.group(:building_id)
.distinct
.count[id].to_i
end
end
class RealEstate < ApplicationRecord
# 関係ない項目は省略
after_destroy_commit do
building&.update_counts
end
end
class RealEstateImage < ApplicationRecord
# 関係ない項目は省略
after_commit do
if real_estate && RealEstate.exists?(real_estate.id)
real_estate.building&.update_counts
end
end
def real_estate
imageable if imageable_type == "RealEstate"
end
end
まとめ
今回は建物一覧のレイテンシ改善のためにcounter_cacheを導入しました。 集計に条件があったこととテーブルのリレーションにポリモーフィックを用いていたことが重なり、単純にcounter_cacheやcounter_cultureを利用するだけでは実装できず本記事のような実装となりました。

最後に
spacelyでは一緒に働いてくださる方を大募集中です。



