🚧
[Rails]has_oneの関連データをdestroyで消してもキャッシュはクリアされない
ActiveRecordのhas_one関連データをdestroyで削除してもキャッシュがクリアされず想定外の動きをしたのでまとめてみました。
ActiveRecordのキャッシュについて
ActiveRecordではパフォーマンスを良くするため、1度取得した関連データはキャッシュされます。
例えば下記のような1対1で関連しているモデルがあるとします。
# Userモデル
class User < ApplicationRecord
has_one :user_setting
end
class UserSetting < ApplicationRecord
belongs_to :user
end
まずはアソシエーションのキャッシュについてコンソールで確認してみます。
irb(main):001:0> user = User.first
User Load (0.7ms) SELECT `users`.* FROM `users` ORDER BY `users`.`id` ASC LIMIT 1
=>
#<e:0x000055a73a5a1ae0
...
# 初回アクセス時はSQLが発行される
irb(main):002:0> user.user_setting
UserSetting Load (1.0ms) SELECT `user_settings`.* FROM `user_settings` WHERE `user_settings`.`user_id` = 1 LIMIT 1
=> #<UserSetting:0x000055a7389f0da0 id: 1, user_id: 1, created_at: Wed, 12 May 2021 00:13:56.876194000 UTC +00:00, updated_at: Wed, 12 May 2021 00:13:56.876194000 UTC +00:00>
# 2度目のアクセス時は1度目の結果がキャッシュされているためSQLは発行されない
irb(main):003:0> user.user_setting
=> #<UserSetting:0x000055a7389f0da0 id: 1, user_id: 1, created_at: Wed, 12 May 2021 00:13:56.876194000 UTC +00:00, updated_at: Wed, 12 May 2021 00:13:56.876194000 UTC +00:00>
destroyで削除したときの挙動
次に、想定外の動きをしたdestroy時の挙動を確認します。
irb(main):001:0> user = User.first
User Load (0.7ms) SELECT `users`.* FROM `users` ORDER BY `users`.`id` ASC LIMIT 1
=>
#<e:0x000055a73a5a1ae0
...
irb(main):002:0> user.user_setting
UserSetting Load (1.0ms) SELECT `user_settings`.* FROM `user_settings` WHERE `user_settings`.`user_id` = 1 LIMIT 1
=> #<UserSetting:0x000055a7389f0da0 id: 1, user_id: 1, created_at: Wed, 12 May 2021 00:13:56.876194000 UTC +00:00, updated_at: Wed, 12 May 2021 00:13:56.876194000 UTC +00:00>
# user_settingをdestroy
irb(main):003:0> user.user_setting.destroy
TRANSACTION (1.1ms) BEGIN
UserSetting Destroy (1.7ms) DELETE FROM `user_settings` WHERE `user_settings`.`id` = 1
TRANSACTION (4.6ms) COMMIT
# user_settingを取得
# →キャッシュが残っているため削除したuser_settingが取得できてしまう
irb(main):004:0> user.user_setting
=> #<UserSetting:0x000055a7389f0da0 id: 1, user_id: 1, created_at: Wed, 12 May 2021 00:13:56.876194000 UTC +00:00, updated_at: Wed, 12 May 2021 00:13:56.876194000 UTC +00:00>
上記のようにdestroyしてから再度アソシエーション経由で取得するとキャッシュが残っているため削除前のデータが取得されてしまいます。
キャッシュを削除するにはuser.reload
かuser.reload_user_setting
を明示的に行う必要があります。
# user.reloadするとuserモデルが再取得される
irb(main):007:0> user.reload
User Load (0.7ms) SELECT `users`.* FROM `users` ORDER BY `users`.`id` ASC LIMIT 1
=>
#<e:0x000055a73a5a1ae0
...
# user自体が再取得されたので、user_settingも再取得されてnilになる
irb(main):008:0> user.user_setting
UserSetting Load (2.1ms) SELECT `user_settings`.* FROM `user_settings` WHERE `user_settings`.`user_id` = 1 LIMIT 1
=> nil
user.reload
だと、userモデル全体がリロードされてしまいます。
user_settingだけをリロードしたいならuser.reload_user_setting
の方が効率が良いです。
# reloadした時点でuser_settingが再取得されます
irb(main):008:0> user.reload_user_setting
UserSetting Load (2.1ms) SELECT `user_settings`.* FROM `user_settings` WHERE `user_settings`.`user_id` = 1 LIMIT 1
=> nil
# user_settingがnilになる
irb(main):008:0> user.user_setting
=> nil
reloadすることでuser.user_setting
が期待通りnilを返却するようになりました。
このようにdestroyしただけだとキャッシュは削除されず、明示的にreloadが必要なので注意が必要です。
(destroyしたら自動的にキャッシュクリアされるとありがたいんだけど、やらない理由があるのかな?)
参考
- Railsガイド
Discussion