はじめに
image_urlというカラムが存在してるModelにCarrierWaveをマウントして画像のアップロード等通常の処理は行えていたので何も気にしてなかったのですが、機能追加に伴って、マウントしたModelをto_jsonすると、なぜか意図しない値が返ってくる不思議な状態で半日ほどハマったのでそのあたりまとめておこうと思います。
先に要点だけ書くと
- マウントするModelを作成するときにはカラム名にはimage_urlというものを含めないほうが良いと思います。
- CarrierWaveのインスタンスメソッドにimage_urlというものが存在するため
- すでにimage_urlというカラム名が存在するModelに対してCarrierWaveをマウント、かつ、そのModelをto_json する時にはRailsでモデルをto_jsonするときにデータを追加するオプションで紹介されてるmethodsという引数で任意のメソッドを指定してうまく対処
という感じです
自分の開発環境
- Mac OS X 10.8.5
- Ruby 2.2.2
- rbenv利用してインストールしてます
- Rails 4.2
- carrierwave-0.10.0
事の経緯
他の人が開発していたものを引き継ぐ形で実装をしてました。
こんな感じのItemというモデルがすでに存在しててて、機能拡張のために画像情報を持たせるようにするためにCarrierWaveを利用することにしました。
# == Schema Information
#
# Table name: items
#
# id :integer not null, primary key
# name :string(255)
# image_url :string(255)
# created_at :datetime not null
# updated_at :datetime not null
#
class Item < ActiveRecord::Base
end
CarrierWave利用できるように既存のModelを修正
CarrierWaveのインストール手順などは割愛しますが最終的にこんな感じになったとします。
class Item < ActiveRecord::Base
mount_uploader :image, ImageUploader
end
class ImageUploader < CarrierWave::Uploader::Base
include CarrierWave::RMagick
storage :file
# 以下省略
end
上記のようにしたことで
item = Item.last
item.image.url
=> "/uploads/item/image/1576/117532-m-01-dl.jpg"
ということが実現できたので特に気にせずにそのまま実装を進めてました。
Itemモデルでto_jsonすると意図した値が取得できない
元々こんな感じで値が格納されてました
Item.last
Item Load (0.4ms) SELECT `items`.* FROM `items` ORDER BY `items`.`id` DESC LIMIT 1
=> #<Item:0x007fc7ea2deec0
id: 1576,
name: "アイテムの名前",
image_url: "http://www.example.co.jp/some_piture.jpg",
ある機能を実現するために、Itemモデルでto_jsonする必要が出てきたのですが、JSON化された値を見るとimage_urlの値がhttp://www.example.co.jp/some_piture.jpg ではなく、CarrierWaveの通じてアップロードされた画像のパス、つまり**/uploads/item/image/1576/117532-m-01-dl.jpg** が返ってきました。
※実際には状況がもうちょっと複雑でItemの先頭はまだCarrierWave通じて画像アップロードされてなかったのでnilが返ってきたので何が生じてるのかわからずに大分ハマりました
原因
find ./vendor/bundle/ruby/2.2.0/gems/carrierwave-0.10.0 -type f -name "*.rb" | xargs grep "image_url"
という感じで検索して気づいたのですがcarrierwave-0.10.0/lib/carrierwave/mount.rbを見ると
# === Added instance methods
#
# Supposing a class has used +mount_uploader+ to mount an uploader on a column
# named +image+, in that case the following methods will be added to the class:
#
# [image] Returns an instance of the uploader only if anything has been uploaded
# [image=] Caches the given file
#
# [image_url] Returns the url to the uploaded file
#
# [image_cache] Returns a string that identifies the cache location of the file
# [image_cache=] Retrieves the file from the cache based on the given cache name
#
# [remote_image_url] Returns previously cached remote url
# [remote_image_url=] Retrieve the file from the remote url
#
# [remove_image] An attribute reader that can be used with a checkbox to mark a file for removal
# [remove_image=] An attribute writer that can be used with a checkbox to mark a file for removal
# [remove_image?] Whether the file should be removed when store_image! is called.
#
# [store_image!] Stores a file that has been assigned with +image=+
# [remove_image!] Removes the uploaded file from the filesystem.
#
# [image_integrity_error] Returns an error object if the last file to be assigned caused an integrity error
# [image_processing_error] Returns an error object if the last file to be assigned caused a processing error
# [image_download_error] Returns an error object if the last file to be remotely assigned caused a download error
#
# [write_image_identifier] Uses the write_uploader method to set the identifier.
# [image_identifier] Reads out the identifier of the file
という感じで、CarrierWaveにimage_urlというインスタンスメソッドがありました。
to_jsonした時には、Itemモデルのimage_urlではなく、CarrierWaveのimage_urlの値が参照されてJSON化されていたようでした。
対策
カラム名を変更すれば解決出来ると思うのですが、色々事情があって、すぐに変更するわけにはいかなさそうだったので、別の対策を考えてました。
Railsでモデルをto_jsonするときにデータを追加するオプションで紹介されてるmethodsという引数で任意のメソッドを指定するという方法を取ることにしました。
Itemモデルを修正
上記の記事だけだと理解がちょっと進まなかったのですがas_json include carrierwave modelを見てたらどういうことかわかりItemモデルをこのように変更しました
class Item < ActiveRecord::Base
mount_uploader :image, ImageUploader
def origin_imaeg_url
self[:image_url]
end
end
このようにすることで、origin_imaeg_urlを通じて、Itemモデルが参照するitemsテーブルのimage_urlを取得することが出来るようになります
Item.to_jsonする時に一工夫する
to_jsonする時にCarrierWaveにimage_urlが参照されてしまうため
item.to_json(methods: :origin_imaeg_url)
という感じで書いてあげることでItemモデルのorigin_imaeg_urlメソッド通じて取得される値もJSONに含まれるようになりました。