Carrierwave のカラムを jbuilder でキャッシュする場合気をつけましょう

`carrierwave` のカラムを `jbuilder` でキャッシュしたら、あんまり見慣れてない `uninitialized constant ImageUploader::Uploader14420101` のようなエラーになったので、原因と対策を共有します。

## 前提
`carrierwave` を使って、`version` の定義は１つでもある場合

## 結論
1. `jbuilder` の `cache!` メソッドがキャッシュ内容として保持しているのは、普段出力される json のレスポンスではなく、`cache!` メソッドに渡すブロックにある各プロパティと値の hash になります
2. `carrierwave` のカラムをそのままキャッシュするのではなく、`json.url image.url` または `json.thumb_url image.url(:thumb)` のように明確にメソッドを呼び出しましょう 

## エラーを再現する手順
### ますソースは、下記通り

```rb
# uploader
class ImageUploader < CarrierWave::Uploader::Base
  include CarrierWave::MiniMagick

  def store_dir
    "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
  end

  version :thumb do
    process resize_to_fit: [264, 264]
  end
end

# model
class User < ApplicationRecord
  mount_uploader :image, ImageUploader
end

# view
json.cache! user do
  json.extract!(user, :id, :name, :image)
end

# users_controller
class UsersController < ApplicationController
  def show
    @user = User.find params[:id]
  end
end
```

### アクセスしてみる
- キャッシュを有効にする
    - `rails dev:cache`
- Rails 起動しておく
    - `rails s`
- `User` データを作成おく（**作成された id は 1 とする**）
    - `User.create name: 'cat', image: File.open(Rails.root.join('cat.jpg'))`
- １回目アクセスする：キャッシュに書き込まれる
    - `curl  localhost:3000/users/1.json` 

```rb
Started GET "/users/1.json" for ::1 at 2020-05-08 05:38:14 +0900
Processing by UsersController#show as JSON
  Parameters: {"id"=>"1"}
  User Load (0.1ms)  SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
  ↳ app/controllers/users_controller.rb:3:in `show'
  Rendering users/show.json.jbuilder
Read fragment jbuilder/views/users/show:d262876fbc7d05fd6b3e314a735f0da2/users/1-20200507203130620803 (0.1ms)
Write fragment jbuilder/views/users/show:d262876fbc7d05fd6b3e314a735f0da2/users/1-20200507203130620803 (2.8ms)
  Rendered users/show.json.jbuilder (Duration: 192.8ms | Allocations: 36311)
Completed 200 OK in 195ms (Views: 193.5ms | ActiveRecord: 0.1ms | Allocations: 37246)
```


- ２回目アクセスする：キャッシュから読み込まれる
    - `curl  localhost:3000/users/1.json`

```rb
Started GET "/users/1.json" for ::1 at 2020-05-08 05:43:16 +0900
Processing by UsersController#show as JSON
  Parameters: {"id"=>"1"}
  User Load (0.2ms)  SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
  ↳ app/controllers/users_controller.rb:3:in `show'
  Rendering users/show.json.jbuilder
Read fragment jbuilder/views/users/show:d262876fbc7d05fd6b3e314a735f0da2/users/1-20200507203130620803 (0.8ms)
  Rendered users/show.json.jbuilder (Duration: 2.3ms | Allocations: 2071)
Completed 200 OK in 5ms (Views: 2.9ms | ActiveRecord: 0.2ms | Allocations: 2996)
```

- `rails s` を停止し、再度起動する
- 3 回目アクセスする => **ここでエラーになる**
    -  `curl  localhost:3000/users/1.json`

```rb
Started GET "/users/1.json" for ::1 at 2020-05-08 05:45:05 +0900
   (5.2ms)  SELECT sqlite_version(*)
   (0.3ms)  SELECT "schema_migrations"."version" FROM "schema_migrations" ORDER BY "schema_migrations"."version" ASC
Processing by UsersController#show as JSON
  Parameters: {"id"=>"1"}
  User Load (0.3ms)  SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
  ↳ app/controllers/users_controller.rb:3:in `show'
  Rendering users/show.json.jbuilder
Read fragment jbuilder/views/users/show:d262876fbc7d05fd6b3e314a735f0da2/users/1-20200507203130620803 (4.5ms)
  Rendered users/show.json.jbuilder (Duration: 14.5ms | Allocations: 5196)
Completed 500 Internal Server Error in 106ms (ActiveRecord: 1.1ms | Allocations: 24319)



ActionView::Template::Error (uninitialized constant ImageUploader::Uploader70271484311960
Did you mean?  ImageUploader::Uploader70197099078960):
    1: json.cache! @user do
    2:   json.extract! @user, :id, :name, :image
    3: end

app/views/users/show.json.jbuilder:1
```

## 原因
- jbuilder の `cache!` メソッドは、`_cache_fragment_for` の戻り値を返す

```rb
# https://github.com/rails/jbuilder/blob/5a314bd8732d50a9fdda54d35f5e742d8499e4ba/lib/jbuilder/jbuilder_template.rb#L34-L44
def cache!(key=nil, options={})
  if @context.controller.perform_caching
    value = _cache_fragment_for(key, options) do
      _scope { yield self }
    end

    merge! value
  else
    yield
  end
end
```

- `_cache_fragment_for` メソッドは、キャッシュを読み取って、まだなければ、書き込む

```rb
def _cache_fragment_for(key, options, &block)
  key = _cache_key(key, options)
  _read_fragment_cache(key, options) || _write_fragment_cache(key, options, &block)
end
```

- キャッシュに書き込む内容は、渡されたブロックの戻り値、つまり、`_scope { yield self }`

```rb
def _write_fragment_cache(key, options = nil)
  @context.controller.instrument_fragment_cache :write_fragment, key do
    yield.tap do |value|
      ::Rails.cache.write(key, value, options)
    end
  end
end
```

- `_scope` メソッドは、json レスポンスを生成するための各属性の hash (@attributes)を戻す

```rb
# https://github.com/rails/jbuilder/blob/5a314bd8732d50a9fdda54d35f5e742d8499e4ba/lib/jbuilder.rb#L303-L310
def _scope
  parent_attributes, parent_formatter = @attributes, @key_formatter
  @attributes = BLANK
  yield
  @attributes
ensure
  @attributes, @key_formatter = parent_attributes, parent_formatter
end
```

- `User#image` メソッドの場合は、上記 `@attributes` に保持されるのは下記のような値になります

```rb
irb(main):003:0> user.image
=> #<ImageUploader:0x00007f9919b19e50 @model=#<User id: 1, name: "cat", image: "cat.jpg", created_at: "2020-05-04 11:20:23", updated_at: "2020-05-07 20:31:30">, @mounted_as=:image, @staged=false, @file=#<CarrierWave::Storage::Fog::File:0x
00007f9919b18d48 @uploader=#<ImageUploader:0x00007f9919b19e50 ...>, @base=#<CarrierWave::Storage::Fog:0x00007f9919b19a68 @uploader=#<ImageUploader:0x00007f9919b19e50 ...>>, @path="uploads/user/image/1/cat.jpg", @content_type=nil>, @filename=nil, @cache_id=nil, @identifier="cat.jpg", @versions={:thumb=>#<ImageUploader::Uploader70147768276140:0x00007f9919b18910 @model=#<User id: 1, name: "cat", image: "cat.jpg", created_at: "2020-05-04 11:20:23", updated_at: "2020-05-07 20:
31:30">, @mounted_as=:image, @staged=false, @file=#<CarrierWave::Storage::Fog::File:0x00007f9919b2b628 @uploader=#<ImageUploader::Uploader70147768276140:0x00007f9919b18910 ...>, @base=#<CarrierWave::Storage::Fog:0x00007f9919b182d0 @upload
er=#<ImageUploader::Uploader70147768276140:0x00007f9919b18910 ...>>, @path="uploads/user/image/1/thumb_cat.jpg", @content_type=nil>, @filename=nil, @cache_id=nil, @identifier="cat.jpg", @versions={}, @parent_version=#<ImageUploader:0x00007f9919b19e50 ...>, @storage=#<CarrierWave::Storage::Fog:0x00007f9919b182d0 @uploader=#<ImageUploader::Uploader70147768276140:0x00007f9919b18910 ...>>>}, @storage=#<CarrierWave::Storage::Fog:0x00007f9919b19a68 @uploader=#<ImageUploader:0x0
0007f9919b19e50 ...>>>
```

- 問題あるのは、上記内容の versions のところの **Uploader70147768276140**

```rb
...
@versions={:thumb=>#<ImageUploader::Uploader70147768276140:0x00007f9919b18910 @model=# 
...
```

- この謎の数値が付いているクラスは、`carrierwave` 定義している `const_set("Uploader#{uploader.object_id}".tr('-', '_'), uploader)` 

```rb
# https://github.com/carrierwaveuploader/carrierwave/blob/master/lib/carrierwave/uploader/versions.rb#L56-L82
def version(name, options = {}, &block)
  name = name.to_sym
  build_version(name, options)

  versions[name].class_eval(&block) if block
  versions[name]
end

...

private

def build_version(name, options)
  if !versions.has_key?(name)
    uploader = Class.new(self)
    const_set("Uploader#{uploader.object_id}".tr('-', '_'), uploader)
    uploader.version_names += [name]

```

## まとめ
jbuilder でキャッシュする場合、キャッシュされる値は、json レスポンスの値ではなく、途中結果の `@attributes` になっていて、想定外の動きになるかもしれないので、気をつけましょう。