🚧

[Rails]has_oneの関連データをdestroyで消してもキャッシュはクリアされない

2021/05/12に公開

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.reloaduser.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ガイド

https://railsguides.jp/association_basics.html

Discussion