Rails6 のちょい足しな新機能を試す43（ActiveRecord Cache編）

# はじめに

Rails 6 に追加されそうな新機能を試す第43段。 今回は、 `ActiveRecord Cache` 編です。
Rails 6 では、 `has_many` や `belongs_to` などの関連がある model の reload 時の ActiveRecord のキャッシュに関連する動作が修正されています。 `reload` しているのに、データベースから読み直されず、キャッシュの古い値が取得されてしまう動作がなくなり、一貫性が感じられる動作になっています。

Ruby 2.6.3, Rails 6.0.0.rc1, Rails 5.2.3 で確認しました。Rails 6.0.0.rc1 は `gem install rails --prerelease` でインストールできます。

```shell
$ rails --version
Rails 6.0.0.rc1
```

# プロジェクトを作る

```shell
$ bin/rails new rails6_0_0rc1
$ cd rails6_0_0rc1
```

# モデルを作る

`has_many` や `belongs_to` の関係を作るため、 Author, Book, Edition の3つのモデルを作成します。

本 ( `Book` ) には1人の著者 ( `Author` ) がひもづき ( `belongs_to` )、複数の版 ( `Edition` ) が存在します。

```shell
$ bin/rails g model Author name
$ bin/rails g model Book title author:references
$ bin/rails g model Edition name book:references
```

# `has_many` を追加する

`Author` と `Book` にそれぞれ `has_many` を追加します。

```ruby:app/models/author.rb
class Author < ApplicationRecord
  has_many :books, dependent: :destroy
end
```

```ruby:app/models/book.rb
class Book < ApplicationRecord
  belongs_to :author
  has_many :editions, dependent: :destroy
end
```

# db:migrate を実行する

一旦、ここで、 db:migrate をしておきます。

```shell
$ bin/rails db:create db:migrate
```


# Controller と View を作る

Book controller と index View を作ります。

```shell
$ bin/rails g controller books index
```

# 初期データを登録するメソッドを定義する

`BooksController` に初期データを登録するプライベートメソッド `initialize_data` を作成します。
毎回同じデータでテストできるように、一旦、データを削除してから、 `Author` , `Book` ,  `Edition` のデータを作成するようにします。

```ruby:app/controllers/books_controller.rb
class BooksController < ApplicationController
  ...
  private

  def initialize_data
    Author.first&.destroy
    author = Author.create(name: 'Dave Tomas')
    book = Book.create(title: 'Pragmatic Programer', author: author)
    Edition.create([{ name: '1st', book: book }, { name: '2nd', book: book }])
  end
end
```
# model に値を修正するメソッドを追加する

各モデルにそれぞれの値を更新するメソッドを追加します。
普通にDBの値を更新すると、今回の事象が確認できないので、Threadを作って、Thread の中で更新します。
`Thread#join` を使って更新が終わるまで待つようにします。

Book モデルの title を更新するメソッドを追加します。

```ruby:app/models/book.rb
class Book < ApplicationRecord
  ...
  def self.fix_title
    Thread.start do
      Book.where(title: 'Pragmatic Programer').update(title: 'Pragmatic Programmer')
    end.join
  end
end
```

同じように、Author モデルの name を更新するメソッドを追加します。

```ruby:app/models/author.rb
class Author < ApplicationRecord
  ...
  def self.fix_name
    Thread.start do
      Author.where(name: 'Dave Tomas').update(name: 'Dave Thomas')
    end.join
  end
end
```

Edition モデルにも name を更新するメソッドを追加します。

```ruby:app/models/edition.rb
class Edition < ApplicationRecord
  ...
  def self.fix_name
    Thread.start do
      Edition.where(name: '2nd').update(name: '20th Anniversary Edition')
    end.join
  end
end
```


# `book.reload` のキャッシュの動作を試すメソッドを作る

`book.reload` のキャッシュの動作を試すプライベートメソッドを作ります。
`book.reload` の前に `book` の `title` を更新します。

```ruby:app/controllers/books_controller.rb
class BooksController < ApplicationController
  ...
  private
  ...
  # try book.reload
  def try_book_reload
    book = Book.first
    @old_title = book.title
    Book.fix_title
    @new_title = book.reload.title
  end
end
```

# `book.reload_author` のキャッシュの動作を試すメソッドを作る

`book.reload_author` のキャッシュの動作を試すプライベートメソッドを作ります。
`book.reload_author` の前に `author` の `name` を更新します。
更新の前後で、 `Author.find` を呼び出します。

```ruby:app/controllers/books_controller.rb
class BooksController < ApplicationController
  ...
  private
  ...

  # try book.reload_author
  def try_book_reload_author
    book = Book.first
    author_id = Author.first.id
    @old_author_name = Author.find(author_id).name
    Author.fix_name
    @reloaded_author_name = book.reload_author.name
    @new_author_name = Author.find(author_id).name
  end
end
```

# `book.editions.reload` を試す

`book.editions.reload` のキャッシュの動作を試すプライベートメソッドを作ります。
`book.editions.reload` の前に `edition` の `name` を更新します。


```ruby:app/controllers/books_controller.rb
class BooksController < ApplicationController
  ...
  private
  ...

  # try book.editions.reload
  def try_book_editions_reload
    book = Book.first
    @old_editions = book.editions.reload.map(&:name)
    Edition.fix_name
    @new_editions = book.editions.reload.map(&:name)
  end
end
```

# index メソッドを修正する

index メソッドを修正します。
これまで作ったプライベートメソッドを順に呼び出します。
index は GET メソッドなので、データを登録したり更新したりするのは RESTful ではないので、良くないです。
今回は動作確認のためなので、見逃してください。

```ruby:app/controllers/books_controller.rb
class BooksController < ApplicationController
  def index
    initialize_data

    try_book_reload
    try_book_reload_author
    try_book_editions_reload
  end
  ...
end
```

# View を作成する

Controller で求めた値を表示するように View を修正します。

```erb:app/views/books/index.html.erb
<h1>Books#index</h1>

<table>
  <thead>
    <tr>
      <th>variable</th>
      <th>value</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>@old_title</td>
      <td><%= @old_title %></td>
    </tr>
    <tr>
      <td>@new_title</td>
      <td><%= @new_title %></td>
    </tr>
    <tr>
      <td>@old_author_name</td>
      <td><%= @old_author_name %></td>
    </tr>
    <tr>
      <td>@reloaded_author_name</td>
      <td><%= @reloaded_author_name %></td>
    </tr>
    <tr>
      <td>@new_author_name</td>
      <td><%= @new_author_name %></td>
    </tr>
    <tr>
      <td>@old_editions</td>
      <td><%= @old_editions %></td>
    </tr>
    <tr>
      <td>@new_editions</td>
      <td><%= @new_editions %></td>
    </tr>
  </tbody>
</table>
```

# Rails 5 では

画面を表示すると以下のようになります。
データを更新していますが、 `@new_author_name` の値と `@new_editions` の値は変わっていません。
キャッシュが働いているためです。
![rails5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209941/c04c550b-81aa-5598-27ae-6d19229aac8c.png)

# Rails 6 では

画面を表示すると以下のようになります。
`@new_author_name` の値と `@new_editions` の値は更新後の値に変わっています。

![rails6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209941/a53f5313-e042-deef-743e-2168082eb508.png)


# 試したソース

試したソースは以下にあります。
https://github.com/suketa/rails6_0_0rc1/tree/try043_query_cache


# 参考情報

* [What is new in Rails 6.0](https://bogdanvlviv.com/posts/ruby/rails/what-is-new-in-rails-6_0.html)
* [Fix inconsistent behavior by clearing QueryCache when reloading associations](https://github.com/rails/rails/pull/34094)