【Rails7.0】カウンターキャッシュ名の衝突による余分な更新を解消する
こんにちは。st-1985 です。
今日は Rails(ActiveRecord) のカウンターキャッシュ機能を使用していて発生した現象とその解消方法について書こうと思います。
なお、Rails 7.1 では解消されているので、7.0 での話になります。
確認はできていませんが、Rails 6.x 以前でも同様に発生する可能性があります。
カウンターキャッシュとは
データベースの関連レコードの数を効率的に管理するための機能です。
例えば、DBで複数のサイト(Site)を管理しているとします。
サイトには、それぞれ多数のユーザー(User)がおり、それぞれがさまざまなグループ(UserGroup)に所属しているとします。
各サイトやグループに所属するユーザーの件数を取得する場合、レコードの件数を取得するクエリを実行する事になりますが、
カウンターキャッシュを利用することで、それぞれのユーザー数をリアルタイムで更新し、データベースへのクエリ回数を減らすことができます。
これにより、検索時のパフォーマンスが向上し、ユーザー体験の改善が期待できます。
問題が起こった構成
カウンターキャッシュの説明に準じて以下のような構成になっていたとします。
-
Site
は複数のUser
とUserGroup
を持つ -
UserGroup
はUserGroupAndUser
を介して複数のUser
を持つ -
Site
はusers_count
というカウンターキャッシュ用のカラムを持つ -
UserGroup
にもusers_count
という同じ名前のカウンターキャッシュ用のカラムを持つ
モデル定義は以下です。
# Table name: sites
#
# id :integer not null, primary key
# users_count :integer default(0), not null
# created_at :datetime not null
# updated_at :datetime not null
#
class Site < ApplicationRecord
has_many :users
has_many :user_groups
end
# Table name: users
#
# id :integer not null, primary key
# site_id :integer not null
# created_at :datetime not null
# updated_at :datetime not null
#
class User < ApplicationRecord
has_many :user_group_and_users
has_many :user_groups, through: :user_group_and_users
belongs_to :site, counter_cache: true
end
# Table name: user_groups
#
# id :integer not null, primary key
# site_id :integer not null
# users_count :integer default(0), not null
# created_at :datetime not null
# updated_at :datetime not null
#
class UserGroup < ApplicationRecord
has_many :user_group_and_users
has_many :users, through: :user_group_and_users
belongs_to :site
end
# Table name: user_group_and_users
#
# id :integer not null, primary key
# user_group_id :integer not null
# user_id :integer not null
# created_at :datetime not null
# updated_at :datetime not null
#
class UserGroupAndUser < ApplicationRecord
belongs_to :user_group, counter_cache: :users_count
belongs_to :user
end
発生した現象
以下のコードのように新しくグループを作成し、そこにユーザーを追加したとします。
site = Site.create
user_group = UserGroup.create(site: site)
puts user_group.users_count # => 0
user_group.users.create(site: site)
ユーザーの追加に成功し、カウンターキャッシュの値を取得すると
puts user_group.users_count # => 2
1件の追加だったはずが余分なカウントがされています。
ちなみに user_group.users
のsize
はusers_count
の値を見る為、こちらも余分なカウントがされた状態で表示されます。
puts user_group.users.size # => 2
この余分なカウントはメモリ上のusers_count
のみに影響し、DBには反映されていない為、リロード後に正しい値を取得する事ができます。
puts user_group.reload.users_count # => 1
問題の原因
この問題は、カウンターキャッシュの名前が UserGroup
と Site
の両方で users_count
として使用されている為に、関連が更新される際にSite
のusers_count
をUserGroup
のusers_count
と取り違えて更新してしまうことに起因しているようです。
解決策
-
Rails のバージョンアップ
冒頭で触れた通りこの問題は、Rails 7.1.0beta1で解決されています。
なので、可能であればバージョンアップをするのが一番手っ取り早い解決策だと思います。 -
カウンターキャッシュの名前を一意にする
バージョンアップが難しい場合は名前の衝突を避ける事で回避できます。# DBのカラム変更(マイグレーション) class RenameUsersCountToUserGroupUsersCountInUserGroups < ActiveRecord::Migration[7.0] def change rename_column :user_groups, :users_count, :user_group_users_count end end # モデルの修正 class UserGroupAndUser < SitelicationRecord belongs_to :user_group, counter_cache: :user_group_users_count # ← users_count から変更 belongs_to :user end
カウンターキャッシュの名前を変えたのでコードの変更も忘れないようにしましょう。
まとめ
- カウンターキャッシュの名前の重複していると余分にカウントされる場合がある
- Rails 7.1 にアップデートする事で根本的に解決する
- アップデートが難しい場合は別の名前にする事で回避できる
この記事がお役に立てれば幸いです。
Discussion