0
0

More than 3 years have passed since last update.

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

Posted at

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

前提

carrierwave を使って、version の定義は1つでもある場合

結論

  1. jbuildercache! メソッドがキャッシュ内容として保持しているのは、普段出力される json のレスポンスではなく、cache! メソッドに渡すブロックにある各プロパティと値の hash になります
  2. 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 になっていて、想定外の動きになるかもしれないので、気をつけましょう。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0