Rails v7.0.7対応 ActiveSupport (Fix Cache::NullStore with local caching for repeated reads)

# はじめに

Railsのバージョン7.0.7[^1] が公開されました。パッチバージョンの更新なので大きな変更はないですが、一通り確認していきたいと思います。
第一弾として、今回は ActiveSupportの"Fix Cache::NullStore with local caching for repeated reads"[^2]について確認していきます。
　
~~(※ 記事執筆中にv7.0.7.1が公開されました。v7.0.7から大きくは変わっていませんので、v7.0.7が終わり次第、記事にします）~~
時間が取れず、だいぶ時間が空いてしまいましたので、これ以上の確認は実施しません。お待ちしていた方にはお詫び申し上げます。

# 概要

v7.0.6だと`ActiveSupport::Cache::NullStore`で`#with_local_cache`を使っているとき、値を設定していない項目を`#read_multi`で2回読みだすとエラーが発生します。

値が設定されていないのであれば`nil`を返す方が適切であるため、v7.0.7で`nil`を出力するように修正されました。

`#with_local_cache`と`#read_multi`が同時に使用される箇所があれば、バージョンアップに関わらず、動作が想定通りか確認してください。

# 経緯

Railsではキャッシュを利用する仕組みがいくつか用意されています。
この中で、今回のバージョンアップの対象になったのが、`ActiveSupport::Cache::NullStore`です。

`NullStore`は`ActiveSpport::Cache::Store`を継承しているため、値の保存と読出しのメソッドが実装されています。ですが、`NullStore`は保存のメソッドを呼び出しても実際にはキャッシュしません。
開発時やテストのときに、プログラム中ではキャッシュへの保存を実行しているけどキャッシュの値を利用したくない、という場合に便利なクラスです。

値を保存しないとのことですが、`NullStore`は`#with_local_cache`を使うことで一時的にローカルのメモリに保存できるようになります。

v7.0.6では、`NullStore`を使っているときに、1. 値が設定されていない項目を、2. `#read`あるいは`#read_multi`で読み込んだ後、3. さらに`#read_multi`で読み込んだときに`NoMethodError`が発生していました。
`#read`が`nil`を返すので同様に`nil`を返してほしいところですがこのような動作になっていました。

なお、問題となっている部分のコードはv7.0.3から混入していることがコミット履歴から分かります[^3]。
該当のコミット[^4]はv7.0.3のCHANGELOGに記載がないもので、しれっとマージされているものなのだ、と勉強になりました。

# バージョンアップに伴う変更点

`#read_multi`で`NoMethodError`になっていたところを、v7.0.7からはエラーを発生させずに`nil`を出力するようになりました。

# 対処

バージョンアップするしないに関わらず、以下を確認した方がよいです。

1. `#with_local_cache`を利用し、かつ、`#read_multi`が使われている箇所を探してください。なければ問題ありません。
1. `#read_multi`が値が設定されていないキーを読みだしたとき、`NoMethodError`が発生するか`nil`が返されるか、どちらを想定しているか確認してください。
1. `NoMethodError`を想定している場合はv7.0.6であれば問題ありません。もし、v7.0.7にバージョンアップする場合には、エラーが発生しないことを前提とした処理に変更してください。

# 動作確認

## `AcviteSupport::Cache::NullStroe`の基本動作

`NullStore`は値を保存しません。保存した項目を呼び出そうとすると`nil`となります。

```ruby:スクリプト
cache.write("name", "Taro")
result = cache.read("name")
puts "result = " + (result.nil? ? "nil" : result)
```
```sh:結果
result = nil
```

`#with_local_cache`のブロック内では値がキャッシュされます。

```ruby:スクリプト
cache.with_local_cache do
    cache.write("name", "Taro")
    result = cache.read("name")
    puts "result = " + result
end
```
```sh:結果
result = Taro
```

## 未保存の項目を`#read_multi`で読み込む

v7.0.6の場合は2回目の`#read_multi`の呼び出しで`NoMethodError`が生じます。

```ruby:スクリプト
cache.with_local_cache do
    cache.clear
    cache.read_multi("name")

    begin
      cache.read_multi("name")
    rescue Exception => e
      puts "raise: " + e.class.to_s
    end
end
```
```ruby:結果
raise: NoMethodError
```

一方、v7.0.7では`nil`が出力されます

```ruby:スクリプト
cache.with_local_cache do
    cache.clear
    cache.read_multi("name")
    result = cache.read_multi("name")
    puts "result = " + result.to_s
end
```
```sh:結果
result = {"name"=>nil}
```

## リンク

動作確認用のコードと環境は[こちらのリポジトリ](https://github.com/shi-tak/rails_test)にまとめています。

コードの場所は`<バージョン>/lib/samples/active_support_null_store.rb`となります。

[^1]: [CHANGELOG](https://github.com/rails/rails/releases/tag/v7.0.7)
[^2]: [Merge pull request #48868 from fatkodima/backport-45728](https://github.com/rails/rails/commit/b92536265f2b8135a1cd8d8b582388ad9024cc8c?diff=split)
[^3]: [Comparing Changes between 7.0.2.4 and 7.0.3](https://github.com/rails/rails/compare/v7.0.2.4...v7.0.3)
[^4]: [Merge pull request #44366 from avalanche123/patch-1](https://github.com/rails/rails/commit/d4ef8c26a527628d5f7c32151f2c1d59b40e9d74)