carrierwave
のカラムを jbuilder
でキャッシュしたら、あんまり見慣れてない uninitialized constant ImageUploader::Uploader14420101
のようなエラーになったので、原因と対策を共有します。
前提
carrierwave
を使って、version
の定義は1つでもある場合
結論
-
jbuilder
のcache!
メソッドがキャッシュ内容として保持しているのは、普段出力される json のレスポンスではなく、cache!
メソッドに渡すブロックにある各プロパティと値の hash になります -
carrierwave
のカラムをそのままキャッシュするのではなく、json.url image.url
またはjson.thumb_url image.url(:thumb)
のように明確にメソッドを呼び出しましょう
エラーを再現する手順
ますソースは、下記通り
# 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'))
- 1回目アクセスする:キャッシュに書き込まれる
-
curl localhost:3000/users/1.json
-
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)
- 2回目アクセスする:キャッシュから読み込まれる
curl localhost:3000/users/1.json
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
-
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
の戻り値を返す
# 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
メソッドは、キャッシュを読み取って、まだなければ、書き込む
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 }
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)を戻す
# 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
に保持されるのは下記のような値になります
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
...
@versions={:thumb=>#<ImageUploader::Uploader70147768276140:0x00007f9919b18910 @model=#
...
- この謎の数値が付いているクラスは、
carrierwave
定義しているconst_set("Uploader#{uploader.object_id}".tr('-', '_'), uploader)
# 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
になっていて、想定外の動きになるかもしれないので、気をつけましょう。